<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug;

/**
 * Get and parse phpDoc block
 */
class PhpDoc
{

    protected static $cache = array();

    /**
     * Rudimentary doc-block parsing
     *
     * @param string|object|\Reflector $what doc-block string, object, or reflector object
     *
     * @return array
     */
    public static function getParsed($what)
    {
        $hash = null;
        if (\is_object($what)) {
            $hash = self::getHash($what);
            if (isset(self::$cache[$hash])) {
                return self::$cache[$hash];
            }
        }
        $comment = self::getCommentContent($what);
        if (\is_array($comment)) {
            return $comment;
        }
        $return = array(
            'summary' => null,
            'description' => null,
        );
        if (\preg_match('/^@/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
            // we have tags
            $pos = $matches[0][1];
            $strTags = \substr($comment, $pos);
            $return = \array_merge($return, self::parseTags($strTags));
            // remove tags from comment
            $comment = $pos > 0
                ? \substr($comment, 0, $pos-1)
                : '';
        }
        /*
            Do some string replacement
        */
        $comment = \preg_replace('/^\\\@/m', '@', $comment);
        $comment = \str_replace('{@*}', '*/', $comment);
        /*
            split into summary & description
            summary ends with empty whiteline or "." followed by \n
        */
        $split = \preg_split('/(\.[\r\n]+|[\r\n]{2})/', $comment, 2, PREG_SPLIT_DELIM_CAPTURE);
        $split = \array_replace(array('','',''), $split);
        // assume that summary and desc won't be "0"..  remove empty value and merge
        $return = \array_merge($return, \array_filter(array(
            'summary' => \trim($split[0].$split[1]),    // split[1] is the ".\n"
            'desc' => \trim($split[2]),
        )));
        if ($hash) {
            // cache it
            self::$cache[$hash] = $return;
        }
        return $return;
    }

    /**
     * Parse phpDoc tag
     *
     * Notes:
     *    \@method tag:
     *         optional "static" keyword may preceed type & name
     *             'static' returned as a boolean value
     *         parameters:  defaultValue key only returned if defined.
     *                      defaultValue is not parsed
     *
     * @param string $tag    tag type
     * @param string $tagStr tag values (ie "[Type] [name] [<description>]")
     *
     * @return array
     */
    public static function parseTag($tag, $tagStr = '')
    {
        $parsed = array();
        $tagParsers = array(
            array(
                'tags' => array('param','property','property-read', 'property-write', 'var'),
                'parts' => array('type','name','desc'),
                'regex' => '/^'
                    .'(?:(?P<type>[^\$].*?)\s+)?'
                    .'(?:&?\$?(?P<name>\S+)\s+)?'
                    .'(?P<desc>.*)?'
                    .'$/s',
                'callable' => function ($tag, $parsed) {
                    if (\strpos($parsed['desc'], ' ') === false) {
                        if (!$parsed['type']) {
                            $parsed['type'] = $parsed['desc'];
                            $parsed['desc'] = null;
                        } elseif (!$parsed['name']) {
                            $parsed['name'] = \ltrim($parsed['desc'], '&$');
                            $parsed['desc'] = null;
                        }
                    }
                    return $parsed;
                },
            ),
            array(
                'tags' => array('method'),
                'parts' => array('static', 'type', 'name', 'param', 'desc'),
                'regex' => '/'
                    .'(?:(?P<static>static)\s+)?'
                    .'(?:(?P<type>.*?)\s+)?'
                    .'(?P<name>\S+)'
                    .'\((?P<param>((?>[^()]+)|(?R))*)\)'  // see http://php.net/manual/en/regexp.reference.recursive.php
                    .'(?:\s+(?P<desc>.*))?'
                    .'/s',
                'callable' => function ($tag, $parsed) {
                    $parsed['static'] = $parsed['static'] !== null;
                    $parsed['param'] = self::parseParams($parsed['param']);
                    return $parsed;
                },
            ),
            array(
                'tags' => array('return'),
                'parts' => array('type','desc'),
                'regex' => '/^(?P<type>.*?)'
                    .'(?:\s+(?P<desc>.*))?$/s',
            ),
            array(
                'tags' => array('link'),
                'parts' => array('uri', 'desc'),
                'regex' => '/^(?P<uri>\S+)'
                    .'(?:\s+(?P<desc>.*))?$/s',
            ),
            array(
                'tags' => array('see'),
                'parts' => array('uri', 'fqsen', 'desc'),
                'regex' => '/^(?:'
                    .'(?P<uri>https?:\/\/\S+)|(?P<fqsen>\S+)'
                    .')'
                    .'(?:\s+(?P<desc>.*))?$/s',
            ),
            array(
                // default
                'tags' => array(),
                'parts' => array('desc'),
                'regex' => '/^(?P<desc>.*?)$/s',
            ),
        );
        foreach ($tagParsers as $parser) {
            if (\in_array($tag, $parser['tags'])) {
                break;
            }
        }
        \preg_match($parser['regex'], $tagStr, $matches);
        foreach ($parser['parts'] as $part) {
            $parsed[$part] = isset($matches[$part]) && $matches[$part] !== ''
                ? \trim($matches[$part])
                : null;
        }
        if (isset($parser['callable'])) {
            $parsed = \call_user_func($parser['callable'], $tag, $parsed);
        }
        $parsed['desc'] = self::trimDesc($parsed['desc']);
        return $parsed;
    }

    /**
     * Find "parent" phpDoc
     *
     * @param \Reflector $reflector reflectionMethod
     *
     * @return array
     */
    public static function findInheritedDoc(\Reflector $reflector)
    {
        $name = $reflector->getName();
        $reflectionClass = $reflector->getDeclaringClass();
        $interfaces = $reflectionClass->getInterfaceNames();
        foreach ($interfaces as $className) {
            $reflectionClass2 = new \ReflectionClass($className);
            if ($reflectionClass2->hasMethod($name)) {
                return self::getParsed($reflectionClass2->getMethod($name));
            }
        }
        $reflectionClass = $reflectionClass->getParentClass();
        if ($reflectionClass && $reflectionClass->hasMethod($name)) {
            return self::getParsed($reflectionClass->getMethod($name));
        }
    }

    /**
     * Get comment contents
     *
     * @param string|object|\Reflector $what doc-block string, object, or reflector object
     *
     * @return string|array may return array if comment contains an cached inheritdoc
     */
    private static function getCommentContent($what)
    {
        $reflector = null;
        if (\is_object($what)) {
            $reflector = $what instanceof \Reflector
                ? $what
                : new \ReflectionObject($what);
            $docComment = $reflector->getDocComment();
        } else {
            // assume string
            $docComment = $what;
        }
        // remove opening "/**" and closing "*/"
        $docComment = \preg_replace('#^/\*\*(.+)\*/$#s', '$1', $docComment);
        // remove leading "*"s
        $docComment = \preg_replace('#^[ \t]*\*[ ]?#m', '', $docComment);
        $docComment = \trim($docComment);
        if ($reflector) {
            if (\strtolower($docComment) == '{@inheritdoc}') {
                return self::findInheritedDoc($reflector);
            } else {
                $docComment = \preg_replace_callback(
                    '/{@inheritdoc}/i',
                    function () use ($reflector) {
                        $phpDoc =  self::findInheritedDoc($reflector);
                        return $phpDoc['description'];
                    },
                    $docComment
                );
            }
        }
        return $docComment;
    }

    /**
     * PhpDoc won't be different between object instances
     *
     * Generate an identifier for what we're parsing
     *
     * @param mixed $what Object or Reflector
     *
     * @return string|null
     */
    private static function getHash($what)
    {
        $str = null;
        if (!($what instanceof \Reflector)) {
            $str = \get_class($what);
        } elseif ($what instanceof \ReflectionClass) {
            $str = $what->getName();
        } elseif ($what instanceof \ReflectionMethod) {
            $str = $what->getDeclaringClass()->getName().'::'.$what->getName().'()';
        } elseif ($what instanceof \ReflectionFunction) {
            $str = $what->getName().'()';
        } elseif ($what instanceof \ReflectionProperty) {
            $str = $what->getDeclaringClass()->getName().'::'.$what->getName();
        }
        return $str
            ? \md5($str)
            : null;
    }

    /**
     * Parse @method parameters
     *
     * @param string $paramStr parameter string
     *
     * @return array
     */
    private static function parseParams($paramStr)
    {
        $params = self::splitParams($paramStr);
        foreach ($params as $i => $str) {
            \preg_match('/^(?:([^=]*?)\s)?([^\s=]+)(?:\s*=\s*(\S+))?$/', $str, $matches);
            $info = array(
                'type' => $matches[1] ?: null,
                'name' => $matches[2],
            );
            if (!empty($matches[3])) {
                $info['defaultValue'] = $matches[3];
            }
            $params[$i] = $info;
        }
        return $params;
    }

    /**
     * Parse tags
     *
     * @param string $str portion of phpdoc content that contains tags
     *
     * @return array
     */
    private static function parseTags($str)
    {
        $regexNotTag = '(?P<value>(?:(?!^@).)*)';
        $regexTags = '#^@(?P<tag>[\w-]+)[ \t]*'.$regexNotTag.'#sim';
        \preg_match_all($regexTags, $str, $matches, PREG_SET_ORDER);
        $singleTags = array('return');
        foreach ($matches as $match) {
            $value = $match['value'];
            $value = \preg_replace('/\n\s*\*\s*/', "\n", $value);
            $value = \trim($value);
            $value = self::parseTag($match['tag'], $value);
            if (\in_array($match['tag'], $singleTags)) {
                $return[ $match['tag'] ] = $value;
            } else {
                $return[ $match['tag'] ][] = $value;
            }
        }
        return $return;
    }

    /**
     * Split parameter string into individual params
     *
     * @param string $paramStr parameter string
     *
     * @return string[]
     */
    private static function splitParams($paramStr)
    {
        $depth = 0;
        $startPos = 0;
        $chars = \str_split($paramStr);
        $params = array();
        foreach ($chars as $pos => $char) {
            switch ($char) {
                case ',':
                    if ($depth === 0) {
                        $params[] = \trim(\substr($paramStr, $startPos, $pos-$startPos));
                        $startPos = $pos + 1;
                    }
                    break;
                case '[':
                case '(':
                    $depth ++;
                    break;
                case ']':
                case ')':
                    $depth --;
                    break;
            }
        }
        $params[] = \trim(\substr($paramStr, $startPos, $pos+1-$startPos));
        return $params;
    }

    /**
     * Trim leading spaces from each description line
     *
     * @param string $desc string to trim
     *
     * @return string
     */
    private static function trimDesc($desc)
    {
        $lines = \explode("\n", $desc);
        $leadingSpaces = array();
        foreach ($lines as $line) {
            if (\strlen($line)) {
                $leadingSpaces[] = \strspn($line, ' ');
            }
        }
        \array_shift($leadingSpaces);    // first line will always have zero leading spaces
        $trimLen = $leadingSpaces
            ? \min($leadingSpaces)
            : 0;
        if (!$trimLen) {
            return $desc;
        }
        foreach ($lines as $i => $line) {
            $lines[$i] = $i > 0 && \strlen($line)
                ? \substr($line, $trimLen)
                : $line;
        }
        $desc = \implode("\n", $lines);
        return $desc;
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 *
 * @link http://www.github.com/bkdotcom/PHPDebugConsole
 * @link https://developer.mozilla.org/en-US/docs/Web/API/console
 */

namespace bdk\Debug;

use bdk\Debug;
use bdk\PubSub\Event;
use bdk\PubSub\SubscriberInterface;

/**
 * Methods that are internal to the debug class
 *
 * a) Don't want to clutter the debug class
 * b) avoiding a base class as it would necessitate we first load the base or have
 *       an autoloader in place to bootstrap the debug class
 * c) a trait for code not meant to be "reusable" seems like an anti-pattern
 *       doesn't solve the bootstrap/autoload issue
 */
class Internal implements SubscriberInterface
{

    private $debug;
    private $error;     // store error object when logging an error

    private static $profilingEnabled = false;

    /**
     * Constructor
     *
     * @param Debug $debug debug instance
     */
    public function __construct(Debug $debug)
    {
        $this->debug = $debug;
        if ($debug->parentInstance) {
            return;
        }
        $this->debug->eventManager->addSubscriberInterface($this);
        $this->debug->errorHandler->eventManager->subscribe('errorHandler.error', array(function () {
            // this closure lazy-loads the subscriber object
            return $this->debug->errorEmailer;
        }, 'onErrorHighPri'), PHP_INT_MAX);
        $this->debug->errorHandler->eventManager->subscribe('errorHandler.error', array(function () {
            // this closure lazy-loads the subscriber object
            return $this->debug->errorEmailer;
        }, 'onErrorLowPri'), PHP_INT_MAX * -1);
    }

    /**
     * Send an email
     *
     * @param string $toAddr  to
     * @param string $subject subject
     * @param string $body    body
     *
     * @return void
     */
    public function email($toAddr, $subject, $body)
    {
        $addHeadersStr = '';
        $fromAddr = $this->debug->getCfg('emailFrom');
        if ($fromAddr) {
            $addHeadersStr .= 'From: '.$fromAddr;
        }
        \call_user_func($this->debug->getCfg('emailFunc'), $toAddr, $subject, $body, $addHeadersStr);
    }

    /**
     * Serializes and emails log
     *
     * @return void
     */
    public function emailLog()
    {
        /*
            List errors that occured
        */
        $errorStr = $this->buildErrorList();
        /*
            Build Subject
        */
        $subject = 'Debug Log';
        $subjectMore = '';
        if (!empty($_SERVER['HTTP_HOST'])) {
            $subjectMore .= ' '.$_SERVER['HTTP_HOST'];
        }
        if ($errorStr) {
            $subjectMore .= ' '.($subjectMore ? '(Error)' : 'Error');
        }
        $subject = \rtrim($subject.':'.$subjectMore, ':');
        /*
            Build body
        */
        $body = (!isset($_SERVER['REQUEST_URI']) && !empty($_SERVER['argv'])
            ? 'Command: '. \implode(' ', $_SERVER['argv'])
            : 'Request: '.$_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI']
        )."\n\n";
        if ($errorStr) {
            $body .= 'Error(s):'."\n"
                .$errorStr."\n";
        }
        /*
            "attach" serialized log to body
        */
        $data = \array_intersect_key($this->debug->getData(), \array_flip(array(
            'alerts',
            'log',
            'logSummary',
            'requestId',
            'runtime',
        )));
        $data['rootChannel'] = $this->debug->getCfg('channel');
        $body .= $this->debug->utilities->serializeLog($data);
        /*
            Now email
        */
        $this->email($this->debug->getCfg('emailTo'), $subject, $body);
        return;
    }

    /**
     * get error statistics from errorHandler
     * how many errors were captured in/out of console
     * breakdown per error category
     *
     * @return array
     */
    public function errorStats()
    {
        $errors = $this->debug->errorHandler->get('errors');
        $stats = array(
            'inConsole' => 0,
            'inConsoleCategories' => 0,
            'notInConsole' => 0,
            'counts' => array(),
        );
        foreach ($errors as $error) {
            if ($error['isSuppressed']) {
                continue;
            }
            $category = $error['category'];
            if (!isset($stats['counts'][$category])) {
                $stats['counts'][$category] = array(
                    'inConsole' => 0,
                    'notInConsole' => 0,
                );
            }
            $k = $error['inConsole'] ? 'inConsole' : 'notInConsole';
            $stats['counts'][$category][$k]++;
        }
        foreach ($stats['counts'] as $a) {
            $stats['inConsole'] += $a['inConsole'];
            $stats['notInConsole'] += $a['notInConsole'];
            if ($a['inConsole']) {
                $stats['inConsoleCategories']++;
            }
        }
        $order = array(
            'fatal',
            'error',
            'warning',
            'deprecated',
            'notice',
            'strict',
        );
        $stats['counts'] = \array_intersect_key(\array_merge(\array_flip($order), $stats['counts']), $stats['counts']);
        return $stats;
    }

    /**
     * Get calling line/file for error and warn
     *
     * @return array
     */
    public function getErrorCaller()
    {
        $meta = array();
        if ($this->error) {
            // no need to store originating file/line... it's part of error message
            $meta = array(
                'errorType' => $this->error['type'],
                'errorCat' => $this->error['category'],
                'errorHash' => $this->error['hash'],
                'backtrace' => $this->error['backtrace'] ?: array(),
                'sanitize' => $this->error['isHtml'] === false,
                'channel' => 'phpError',
            );
        } else {
            $meta = $this->debug->utilities->getCallerInfo();
            $meta = array(
                'file' => $meta['file'],
                'line' => $meta['line'],
            );
        }
        return $meta;
    }

    /**
     * Return the group & groupCollapsed ("ancestors")
     *
     * @param array   $logEntries log entries
     * @param integer $curDepth   current group depth
     *
     * @return array key => logEntry array
     */
    public static function getCurrentGroups(&$logEntries, $curDepth)
    {
        /*
            curDepth will fluctuate as we go back through log
            minDepth will decrease as we work our way down/up the groups
        */
        $minDepth = $curDepth;
        $entries = array();
        for ($i = \count($logEntries) - 1; $i >= 0; $i--) {
            if ($curDepth < 1) {
                break;
            }
            $method = $logEntries[$i][0];
            if (\in_array($method, array('group', 'groupCollapsed'))) {
                $curDepth--;
                if ($curDepth < $minDepth) {
                    $minDepth--;
                    $entries[$i] = $logEntries[$i];
                }
            } elseif ($method == 'groupEnd') {
                $curDepth++;
            }
        }
        return $entries;
    }

    /**
     * Extracts meta-data from args
     *
     * Extract meta-data added via meta() method..
     * all meta args are merged together and returned
     * meta args are removed from passed args
     *
     * @param array $args        args to check
     * @param array $defaultMeta default meta values
     * @param array $defaultArgs default arg values
     * @param array $argsToMeta  args to convert to meta
     *
     * @return array meta values
     */
    public static function getMetaVals(&$args, $defaultMeta = array(), $defaultArgs = array(), $argsToMeta = array())
    {
        $meta = array();
        foreach ($args as $i => $v) {
            if (\is_array($v) && isset($v['debug']) && $v['debug'] === Debug::META) {
                unset($v['debug']);
                $meta = \array_merge($meta, $v);
                unset($args[$i]);
            }
        }
        $args = \array_values($args);
        if ($defaultArgs) {
            $args = \array_slice($args, 0, \count($defaultArgs));
            $args = \array_combine(
                \array_keys($defaultArgs),
                \array_replace(\array_values($defaultArgs), $args)
            );
        }
        foreach ($argsToMeta as $argk => $metak) {
            if (\is_int($argk)) {
                $argk = $metak;
            }
            $defaultMeta[$metak] = $args[$argk];
            unset($args[$argk]);
        }
        $meta = \array_merge($defaultMeta, $meta);
        return $meta;
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscriptions()
    {
        return array(
            'debug.bootstrap' => array('onBootstrap', PHP_INT_MAX * -1),
            'debug.config' => array('onConfig', PHP_INT_MAX),
            'debug.output' => 'onOutput',
            'errorHandler.error' => 'onError',
            'php.shutdown' => 'onShutdown',
        );
    }

    /**
     * Do we have log entries?
     *
     * @return boolean
     */
    public function hasLog()
    {
        $entryCountInitial = $this->debug->getData('entryCountInitial');
        $entryCountCurrent = $this->debug->getData('log/__count__');
        $haveLog = $entryCountCurrent > $entryCountInitial;
        $lastEntryMethod = $this->debug->getData('log/__end__/0');
        return $haveLog && $lastEntryMethod !== 'clear';
    }

    /**
     * debug.init subscriber
     *
     * @return void
     */
    public function onBootstrap()
    {
        if ($this->debug->parentInstance) {
            // only recored php/request info for root instance
            return;
        }
        $collectWas = $this->debug->setCfg('collect', true);
        $this->debug->groupSummary();
        $this->debug->group('environment', $this->debug->meta(array(
            'hideIfEmpty' => true,
            'level' => 'info',
        )));
        $this->logPhpInfo();
        $this->logServerVals();
        $this->logRequest();    // headers, cookies, post
        $this->debug->groupEnd();
        $this->debug->groupEnd();
        $this->debug->setCfg('collect', $collectWas);
    }

    /**
     * debug.config subscriber
     *
     * @param Event $event event instance
     *
     * @return void
     */
    public function onConfig(Event $event)
    {
        $cfg = $event['config'];
        if (!isset($cfg['debug'])) {
            // no debug config values have changed
            return;
        }
        $cfg = $cfg['debug'];
        if (isset($cfg['file'])) {
            $this->debug->addPlugin($this->debug->output->file);
        }
        if (isset($cfg['onBootstrap'])) {
            if (!$this->debug->parentInstance) {
                // we're initializing
                $this->debug->eventManager->subscribe('debug.bootstrap', $cfg['onBootstrap']);
            } else {
                // boostrap has already occured, so go ahead and call
                \call_user_func($cfg['onBootstrap'], new Event($this->debug));
            }
        }
        if (isset($cfg['onLog'])) {
            /*
                Replace - not append - subscriber set via setCfg
            */
            if (isset($this->cfg['onLog'])) {
                $this->debug->eventManager->unsubscribe('debug.log', $this->cfg['onLog']);
            }
            $this->debug->eventManager->subscribe('debug.log', $cfg['onLog']);
        }
        if (!static::$profilingEnabled) {
            $cfg = $this->debug->getCfg('debug/*');
            if ($cfg['enableProfiling'] && $cfg['collect']) {
                static::$profilingEnabled = true;
                $pathsExclude = array(
                    __DIR__,
                );
                FileStreamWrapper::register($pathsExclude);
            }
        }
    }

    /**
     * errorHandler.error event subscriber
     * adds error to console as error or warn
     *
     * @param Event $error error/event object
     *
     * @return void
     */
    public function onError(Event $error)
    {
        if ($this->debug->getCfg('collect')) {
            /*
                temporarily store error so that we can easily determine error/warn
                 a) came via error handler
                 b) calling info
            */
            $this->error = $error;
            $errInfo = $error['typeStr'].': '.$error['file'].' (line '.$error['line'].')';
            $errMsg = $error['message'];
            if ($error['type'] & $this->debug->getCfg('errorMask')) {
                $this->debug->error($errInfo.': ', $errMsg);
            } else {
                $this->debug->warn($errInfo.': ', $errMsg);
            }
            $error['continueToNormal'] = false; // no need for PHP to log the error, we've captured it here
            $error['inConsole'] = true;
            // Prevent ErrorHandler\ErrorEmailer from sending email.
            // Since we're collecting log info, we send email on shutdown
            $error['email'] = false;
            $this->error = null;
        } elseif ($this->debug->getCfg('output')) {
            $error['email'] = false;
            $error['inConsole'] = false;
        } else {
            $error['inConsole'] = false;
        }
    }

    /**
     * debug.output event subscriber
     *
     * @return void
     */
    public function onOutput()
    {
        if ($this->debug->parentInstance) {
            // only record runtime info for root instance
            return;
        }
        $vals = $this->runtimeVals();
        $this->debug->groupSummary(1);
        $this->debug->info('Built In '.$vals['runtime'].' sec');
        $this->debug->info(
            'Peak Memory Usage'
                .($this->debug->getCfg('output/outputAs') == 'html'
                    ? ' ?⃝'
                    : '')
                .': '
                .$this->debug->utilities->getBytes($vals['memoryPeakUsage']).' / '
                .$this->debug->utilities->getBytes($vals['memoryLimit'])
        );
        $this->debug->groupEnd();
    }

    /**
     * Email Log if emailLog is 'always' or 'onError'
     * output log if not already output
     *
     * @return void
     */
    public function onShutdown()
    {
        $this->runtimeVals();
        if ($this->testEmailLog()) {
            $this->emailLog();
        }
        if (!$this->debug->getData('outputSent')) {
            echo $this->debug->output();
        }
        return;
    }

    /**
     * Publish/Trigger/Dispatch event
     * Event will get published on ancestor channels if propagation not stopped
     *
     * @param string $eventName      event name
     * @param mixed  $eventOrSubject passed to subscribers
     * @param array  $values         values to attach to event
     *
     * @return mixed
     */
    public function publishBubbleEvent($eventName, $eventOrSubject, array $values = array())
    {
        if ($eventOrSubject instanceof Event) {
            $event = $eventOrSubject;
        } else {
            $event = new Event($eventOrSubject, $values);
        }
        $debug = $this->debug;
        while (!$event->isPropagationStopped()) {
            $debug->eventManager->publish($eventName, $event);
            if (!$debug->parentInstance) {
                break;
            }
            $debug = $debug->parentInstance;
        }
        return $event;
    }

    /**
     * Build list of errors for email
     *
     * @return string
     */
    private function buildErrorList()
    {
        $errorStr = '';
        $errors = $this->debug->errorHandler->get('errors');
        \uasort($errors, function ($a1, $a2) {
            return \strcmp($a1['file'].$a1['line'], $a2['file'].$a2['line']);
        });
        $lastFile = '';
        foreach ($errors as $error) {
            if ($error['isSuppressed']) {
                continue;
            }
            if ($error['file'] !== $lastFile) {
                $errorStr .= $error['file'].':'."\n";
                $lastFile = $error['file'];
            }
            $typeStr = $error['type'] === E_STRICT
                ? 'Strict'
                : $error['typeStr'];
            $errorStr .= '  Line '.$error['line'].': ('.$typeStr.') '.$error['message']."\n";
        }
        return $errorStr;
    }

    /**
     * Log some PHP info
     *
     * @return void
     */
    private function logPhpInfo()
    {
        if (!$this->debug->getCfg('logEnvInfo.phpInfo')) {
            return;
        }
        $this->debug->log('PHP Version', PHP_VERSION);
        $this->debug->log('ini location', \php_ini_loaded_file());
        $this->debug->log('memory_limit', $this->debug->utilities->getBytes($this->debug->utilities->memoryLimit()));
        $this->debug->log('session.cache_limiter', \ini_get('session.cache_limiter'));
        if (\session_module_name() === 'files') {
            $this->debug->log('session_save_path', \session_save_path() ?: \sys_get_temp_dir());
        }
        $extensionsCheck = array('curl','mbstring');
        $extensionsCheck = \array_filter($extensionsCheck, function ($extension) {
            return !\extension_loaded($extension);
        });
        if ($extensionsCheck) {
            $this->debug->warn('These common extensions are not loaded:', $extensionsCheck);
        }
        $this->logPhpInfoEr();
    }

    /**
     * Log if
     * PHP's error reporting !== (E_ALL | E_STRICT)
     * PHPDebugConsole is not logging (E_ALL | E_STRICT)
     *
     * @return void
     */
    private function logPhpInfoEr()
    {
        $errorReportingRaw = $this->debug->getCfg('errorReporting');
        $errorReporting = $errorReportingRaw === 'system'
            ? \error_reporting()
            : $errorReportingRaw;
        $msgLines = array();
        $styles = array();
        $styleMono = 'font-family:monospace; opacity:0.8;';
        $styleReset = 'font-family:inherit; white-space:pre-wrap;';
        if (\error_reporting() !== (E_ALL | E_STRICT)) {
            $msgLines[] = 'PHP\'s %cerror_reporting%c is set to `%c'.ErrorLevel::toConstantString().'%c` rather than `%cE_ALL | E_STRICT%c`';
            $styles = array(
                $styleMono, $styleReset, // wraps "error_reporting"
                $styleMono, $styleReset, // wraps actual
                $styleMono, $styleReset, // wraps E_ALL | E_STRICT
            );
            if ($errorReporting === (E_ALL | E_STRICT)) {
                $msgLines[] = 'PHPDebugConsole is disregarding %cerror_reporting%c value (this is configurable)';
                $styles[] = $styleMono;
                $styles[] = $styleReset;
            }
        }
        if ($errorReporting !== (E_ALL | E_STRICT)) {
            if ($errorReportingRaw === 'system') {
                $msgLines[] = 'PHPDebugConsole\'s errorHandler is set to "system" (not all errors will be shown)';
            } elseif ($errorReporting === \error_reporting()) {
                $msgLines[] = 'PHPDebugConsole\'s errorHandler is also using a errorReporting value of '
                    .'`%c'.ErrorLevel::toConstantString($errorReporting).'%c`';
                $styles[] = $styleMono;
                $styles[] = $styleReset;
            } else {
                $msgLines[] = 'PHPDebugConsole\'s errorHandler is using a errorReporting value of '
                    .'`%c'.ErrorLevel::toConstantString($errorReporting).'%c`';
                $styles[] = $styleMono;
                $styles[] = $styleReset;
            }
        }
        if ($msgLines) {
            $args = array(\implode("\n", $msgLines));
            $args = \array_merge($args, $styles);
            $args[] = $this->debug->meta(array(
                'file' => null,
                'line' => null,
            ));
            \call_user_func_array(array($this->debug, 'warn'), $args);
        }
    }

    /**
     * Log Cookie, Post, & Files data
     *
     * @return void
     */
    private function logRequest()
    {
        $this->logRequestHeaders();
        if ($this->debug->getCfg('logEnvInfo.cookies')) {
            $cookieVals = $_COOKIE;
            \ksort($cookieVals, SORT_NATURAL);
            $this->debug->log('$_COOKIE', $cookieVals);
        }
        // don't expect a request body for these methods
        $noBody = !isset($_SERVER['REQUEST_METHOD'])
            || \in_array($_SERVER['REQUEST_METHOD'], array('CONNECT','GET','HEAD','OPTIONS','TRACE'));
        if ($this->debug->getCfg('logEnvInfo.post') && !$noBody) {
            if ($_POST) {
                $this->debug->log('$_POST', $_POST);
            } else {
                $input = \file_get_contents('php://input');
                if ($input) {
                    $this->debug->log('php://input', $input);
                } elseif (isset($_SERVER['REQUEST_METHOD']) && empty($_FILES)) {
                    $this->debug->warn($_SERVER['REQUEST_METHOD'].' request with no body');
                }
            }
            if (!empty($_FILES)) {
                $this->debug->log('$_FILES', $_FILES);
            }
        }
    }

    /**
     * Log Request Headers
     *
     * @return void
     */
    private function logRequestHeaders()
    {
        if (!$this->debug->getCfg('logEnvInfo.headers')) {
            return;
        }
        if (!empty($_SERVER['argv'])) {
            return;
        }
        $headers = $this->debug->utilities->getAllHeaders();
        \ksort($headers, SORT_NATURAL);
        $this->debug->log('request headers', $headers);
    }

    /**
     * Log $_SERVER values specified by `logServerKeys` config option
     *
     * @return void
     */
    private function logServerVals()
    {
        $logEnvInfo = $this->debug->getCfg('logEnvInfo');
        if (!$logEnvInfo['serverVals']) {
            return;
        }
        $logServerKeys = $this->debug->getCfg('logServerKeys');
        if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'GET') {
            $logServerKeys[] = 'REQUEST_METHOD';
        }
        if (isset($_SERVER['CONTENT_LENGTH'])) {
            $logServerKeys[] = 'CONTENT_LENGTH';
            $logServerKeys[] = 'CONTENT_TYPE';
        }
        if (!$logEnvInfo['headers']) {
            $logServerKeys[] = 'HTTP_HOST';
        }
        $logServerKeys = \array_unique($logServerKeys);
        if (!$logServerKeys) {
            return;
        }
        $vals = array();
        foreach ($logServerKeys as $k) {
            if (!\array_key_exists($k, $_SERVER)) {
                $vals[$k] = $this->debug->abstracter->UNDEFINED;
            } elseif ($k == 'REQUEST_TIME') {
                $vals[$k] = \date('Y-m-d H:i:s T', $_SERVER['REQUEST_TIME']);
            } else {
                $vals[$k] = $_SERVER[$k];
            }
        }
        \ksort($vals, SORT_NATURAL);
        $this->debug->log('$_SERVER', $vals);
    }

    /**
     * Get/store values such as runtime & peak memory usage
     *
     * @return array
     */
    private function runtimeVals()
    {
        $vals = $this->debug->getData('runtime');
        if (!$vals) {
            $vals = array(
                'memoryPeakUsage' => \memory_get_peak_usage(true),
                'memoryLimit' => $this->debug->utilities->memoryLimit(),
                'runtime' => $this->debug->timeEnd('debugInit', true),
            );
            $this->debug->setData('runtime', $vals);
        }
        return $vals;
    }

    /**
     * Test if conditions are met to email the log
     *
     * @return boolean
     */
    private function testEmailLog()
    {
        if (!$this->debug->getCfg('emailTo')) {
            return false;
        }
        if ($this->debug->getCfg('output')) {
            // don't email log if we're outputing it
            return false;
        }
        if (!$this->hasLog()) {
            return false;
        }
        if ($this->debug->getCfg('emailLog') === 'always') {
            return true;
        }
        if ($this->debug->getCfg('emailLog') === 'onError') {
            $errors = $this->debug->errorHandler->get('errors');
            $emailMask = $this->debug->errorEmailer->getCfg('emailMask');
            $emailableErrors = \array_filter($errors, function ($error) use ($emailMask) {
                return !$error['isSuppressed'] && ($error['type'] & $emailMask);
            });
            return !empty($emailableErrors);
        }
        return false;
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.1.1
 */

namespace bdk\Debug\Output;

use bdk\Debug\MethodTable;
use bdk\PubSub\Event;

/**
 * Output log via FirePHP
 */
class Firephp extends Base
{

    protected $firephpMethods = array(
        'log' => 'LOG',
        'info' => 'INFO',
        'warn' => 'WARN',
        'error' => 'ERROR',
        'table' => 'TABLE',
        'group' => 'GROUP_START',
        'groupCollapsed' => 'GROUP_START',
        'groupEnd' => 'GROUP_END',
    );
    protected $messageIndex = 0;
    protected $outputEvent;

    const FIREPHP_PROTO_VER = '0.3';
    const MESSAGE_LIMIT = 99999;

    /**
     * Output the log via FirePHP headers
     *
     * @param Event $event debug.output event object
     *
     * @return void
     */
    public function onOutput(Event $event)
    {
        $this->outputEvent = $event;
        $this->channelName = $this->debug->getCfg('channel');
        $this->data = $this->debug->getData();
        $event['headers'][] = array('X-Wf-Protocol-1', 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
        $event['headers'][] = array('X-Wf-1-Plugin-1', 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/'.self::FIREPHP_PROTO_VER);
        $event['headers'][] = array('X-Wf-1-Structure-1', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1');
        $heading = isset($_SERVER['REQUEST_METHOD'])
            ? $_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI']
            : '$: '. \implode(' ', $_SERVER['argv']);
        $this->processLogEntryWEvent('groupCollapsed', array('PHP: '.$heading));
        $this->processAlerts();
        $this->processSummary();
        $this->processLog();
        $this->processLogEntryWEvent('groupEnd');
        $event['headers'][] = array('X-Wf-1-Index', $this->messageIndex);
        $this->data = array();
        return;
    }

    /**
     * Output a log entry to Firephp
     *
     * @param string $method method
     * @param array  $args   args
     * @param array  $meta   meta values
     *
     * @return void
     */
    public function processLogEntry($method, $args = array(), $meta = array())
    {
        $value = null;
        $firePhpMeta = $this->getMeta($method, $meta);
        if ($method == 'alert') {
            list($method, $args) = $this->methodAlert($args, $meta);
            $value = $args[0];
        } elseif (\in_array($method, array('group','groupCollapsed'))) {
            $firePhpMeta['Label'] = $args[0];
        } elseif (\in_array($method, array('profileEnd','table'))) {
            $firePhpMeta['Type'] = \is_array($args[0])
                ? $this->firephpMethods['table']
                : $this->firephpMethods['log'];
            $value = $this->methodTable($args[0], $meta['columns']);
            if ($meta['caption']) {
                $firePhpMeta['Label'] = $meta['caption'];
            }
        } elseif ($method == 'trace') {
            $firePhpMeta['Type'] = $this->firephpMethods['table'];
            $value = $this->methodTable($args[0], array('function','file','line'));
            $firePhpMeta['Label'] = 'trace';
        } elseif (\count($args)) {
            if (\count($args) == 1) {
                $value = $args[0];
                // no label;
            } else {
                $firePhpMeta['Label'] = \array_shift($args);
                $value = \count($args) > 1
                    ? $args // firephp only supports label/value...  we'll pass multiple values as an array
                    : $args[0];
            }
        }
        $value = $this->dump($value);
        if ($this->messageIndex < self::MESSAGE_LIMIT) {
            $this->setFirephpHeader($firePhpMeta, $value);
        } elseif ($this->messageIndex === self::MESSAGE_LIMIT) {
            $this->setFirephpHeader(
                array('Type'=>$this->firephpMethods['warn']),
                'FirePhp\'s limit of '.\number_format(self::MESSAGE_LIMIT).' messages reached!'
            );
        }
        return;
    }

    /**
     * Initialize firephp's meta array
     *
     * @param string $method PHPDebugConsole method
     * @param array  $meta   PHPDebugConsole meta values
     *
     * @return array
     */
    private function getMeta($method, $meta)
    {
        $firePhpMeta = array(
            'Type' => isset($this->firephpMethods[$method])
                ? $this->firephpMethods[$method]
                : $this->firephpMethods['log'],
            // Label
            // File
            // Line
            // Collapsed  (for group)
        );
        if (isset($meta['file'])) {
            $firePhpMeta['File'] = $meta['file'];
            $firePhpMeta['Line'] = $meta['line'];
        }
        if (\in_array($method, array('group','groupCollapsed'))) {
            $firePhpMeta['Collapsed'] = $method == 'groupCollapsed'
                // yes, strings
                ? 'true'
                : 'false';
        }
        return $firePhpMeta;
    }

    /**
     * Build table rows
     *
     * @param array $array   array to debug
     * @param array $columns columns to display
     *
     * @return array
     */
    protected function methodTable($array, $columns = array())
    {
        if (!\is_array($array)) {
            return $this->dump($array);
        }
        $table = array();
        $keys = $columns ?: $this->debug->methodTable->colKeys($array);
        $headerVals = $keys;
        foreach ($headerVals as $i => $val) {
            if ($val === MethodTable::SCALAR) {
                $headerVals[$i] = 'value';
            }
        }
        \array_unshift($headerVals, '');
        $table[] = $headerVals;
        $classNames = array();
        if ($this->debug->abstracter->isAbstraction($array) && $array['traverseValues']) {
            $array = $array['traverseValues'];
        }
        foreach ($array as $k => $row) {
            $values = $this->debug->methodTable->keyValues($row, $keys, $objInfo);
            foreach ($values as $k2 => $val) {
                if ($val === $this->debug->abstracter->UNDEFINED) {
                    $values[$k2] = null;
                }
            }
            $classNames[] = $objInfo['row']
                ? $objInfo['row']['className']
                : '';
            \array_unshift($values, $k);
            $table[] = \array_values($values);
        }
        if (\array_filter($classNames)) {
            \array_unshift($table[0], '');
            // first col is row key.
            // insert classname after key
            foreach ($classNames as $i => $className) {
                \array_splice($table[$i+1], 1, 0, $className);
            }
        }
        return $table;
    }

    /**
     * set FirePHP log entry header(s)
     *
     * @param array $meta  meta information
     * @param mixed $value value
     *
     * @return void
     */
    private function setFirephpHeader($meta, $value = null)
    {
        $msg = \json_encode(array(
            $meta,
            $value,
        ));
        $structureIndex = 1;    // refers to X-Wf-1-Structure-1
        $parts = \explode("\n", \rtrim(\chunk_split($msg, 5000, "\n")));
        $numParts = \count($parts);
        for ($i=0; $i<$numParts; $i++) {
            $part = $parts[$i];
            $this->messageIndex++;
            $headerName = 'X-Wf-1-'.$structureIndex.'-1-'.$this->messageIndex;
            $headerValue = ( $i==0 ? \strlen($msg) : '')
                . '|' . $part . '|'
                . ( $i<$numParts-1 ? '\\' : '' );
            $this->outputEvent['headers'][] = array($headerName, $headerValue);
        }
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.1.1
 */

namespace bdk\Debug\Output;

use bdk\PubSub\Event;

/**
 * Output log as <script> tag
 */
class Script extends Base
{

    protected $consoleMethods = array(
        'assert',
        'count',    // output as log
        'error',
        'group',
        'groupCollapsed',
        'groupEnd',
        'info',
        'log',
        'table',
        'time',     // output as log
        'timeEnd',  // PHPDebugConsole never generates a timeEnd entry
        'trace',
        'warn',
    );

    /**
     * output the log as javascript
     *    which outputs the log to the console
     *
     * @param Event $event event object
     *
     * @return string|void
     */
    public function onOutput(Event $event)
    {
        $this->channelName = $this->debug->getCfg('channel');
        $this->data = $this->debug->getData();
        $errorStats = $this->debug->internal->errorStats();
        $errorStr = '';
        if ($errorStats['inConsole']) {
            $errorStr = 'Errors: ';
            foreach ($errorStats['counts'] as $category => $vals) {
                $errorStr .= $vals['inConsole'].' '.$category.', ';
            }
            $errorStr = \substr($errorStr, 0, -2);
        }
        $str = '';
        $str .= '<script type="text/javascript">'."\n";
        $str .= $this->processLogEntryWEvent('groupCollapsed', array(
            'PHP',
            (isset($_SERVER['REQUEST_METHOD']) && isset($_SERVER['REQUEST_URI'])
                ? $_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI']
                : ''),
            $errorStr,
        ));
        $str .= $this->processAlerts();
        $str .= $this->processSummary();
        $str .= $this->processLog();
        $str .= $this->processLogEntryWEvent('groupEnd');
        $str .= '</script>'."\n";
        $this->data = array();
        $event['return'] .= $str;
    }

    /**
     * Return log entry as javascript console.xxxx
     *
     * @param string $method method
     * @param array  $args   arguments
     * @param array  $meta   meta values
     *
     * @return string
     */
    public function processLogEntry($method, $args = array(), $meta = array())
    {
        if ($method == 'alert') {
            list($method, $args) = $this->methodAlert($args, $meta);
        } elseif ($method == 'assert') {
            \array_unshift($args, false);
        } elseif (\in_array($method, array('count','time'))) {
            $method = 'log';
        } elseif (\in_array($method, array('profileEnd','table'))) {
            $method = 'log';
            if (\is_array($args[0])) {
                $method = 'table';
                $args = array($this->methodTable($args[0], $meta['columns']));
            } elseif ($meta['caption']) {
                \array_unshift($args, $meta['caption']);
            }
        } elseif ($method == 'trace') {
            $method = 'table';
            $args = array($this->methodTable($args[0], array('function','file','line')));
        } elseif (\in_array($method, array('error','warn'))) {
            if (isset($meta['file'])) {
                $args[] = $meta['file'].': line '.$meta['line'];
            }
        }
        if (!\in_array($method, $this->consoleMethods)) {
            $method = 'log';
        }
        foreach ($args as $k => $arg) {
            $args[$k] = \json_encode($this->dump($arg));
        }
        $str = 'console.'.$method.'('.\implode(',', $args).');'."\n";
        $str = \str_replace(\json_encode($this->debug->abstracter->UNDEFINED), 'undefined', $str);
        return $str;
    }

    /**
     * Dump undefined
     *
     * Returns the undefined constant, which we can replace with "undefined" after json_encoding
     *
     * @return string
     */
    protected function dumpUndefined()
    {
        return $this->debug->abstracter->UNDEFINED;
    }

    /**
     * Handle alert method
     *
     * @param array $args arguments
     * @param array $meta meta info
     *
     * @return array array($method, $args)
     */
    protected function methodAlert($args, $meta)
    {
        $args = array('%c'.$args[0], '');
        $method = $meta['class'];
        $styleCommon = 'padding:5px; line-height:26px; font-size:125%; font-weight:bold;';
        switch ($method) {
            case 'danger':
                // Just use log method... Chrome adds backtrace to error(), which we don't want
                $method = 'log';
                $args[1] = $styleCommon
                    .'background-color: #ffbaba;'
                    .'border: 1px solid #d8000c;'
                    .'color: #d8000c;';
                break;
            case 'info':
                $args[1] = $styleCommon
                    .'background-color: #d9edf7;'
                    .'border: 1px solid #bce8f1;'
                    .'color: #31708f;';
                break;
            case 'success':
                $method = 'info';
                $args[1] = $styleCommon
                    .'background-color: #dff0d8;'
                    .'border: 1px solid #d6e9c6;'
                    .'color: #3c763d;';
                break;
            case 'warning':
                // Just use log method... Chrome adds backtrace to warn(), which we don't want
                $method = 'log';
                $args[1] = $styleCommon
                    .'background-color: #fcf8e3;'
                    .'border: 1px solid #faebcc;'
                    .'color: #8a6d3b;';
                break;
        }
        return array($method, $args);
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3.0
 */

namespace bdk\Debug\Output;

use bdk\ErrorHandler;
use bdk\Debug\Output\Html as OutputHtml;

/**
 * Output a summary of errors
 */
class HtmlErrorSummary
{

    protected $outputHtml;
    protected $errorHandler;
    protected $stats = array();

    /**
     * Constructor
     *
     * @param OutputHtml   $outputHtml   OutputHtml instance
     * @param ErrorHandler $errorHandler ErrorHandler instance
     */
    public function __construct(OutputHtml $outputHtml, ErrorHandler $errorHandler)
    {
        $this->outputHtml = $outputHtml;
        $this->errorHandler = $errorHandler;
    }

    /**
     * Returns an error summary
     *
     * @param array $stats error statistics
     *
     * @return string html
     */
    public function build($stats)
    {
        $this->stats = $stats;
        return ''
            .$this->buildFatal()
            .$this->buildInConsole()
            .$this->buildNotInConsole();
    }

    /**
     * If lastError was fatal, output the error
     *
     * @return string
     */
    protected function buildFatal()
    {
        $haveFatal = isset($this->stats['counts']['fatal']);
        if (!$haveFatal) {
            return '';
        }
        $lastError = $this->errorHandler->get('lastError');
        $isHtml = $lastError['isHtml'];
        $backtrace = $lastError['backtrace'];
        $html = '
Fatal Error
';
        $html .= '
';
        if (\count($backtrace) > 1) {
            // more than one trace frame
            $table = $this->outputHtml->buildTable(
                $backtrace,
                array(
                    'attribs' => 'trace table-bordered',
                    'caption' => 'trace',
                    'columns' => array('file','line','function'),
                )
            );
            $html .= '
'.$lastError['message'].'
';
            $html .= '
'.$table.'
';
            if (!$isHtml) {
                $html = \str_replace($lastError['message'], \htmlspecialchars($lastError['message']), $html);
            }
        } else {
            $keysKeep = array('typeStr','message','file','line');
            $lastError = \array_intersect_key($lastError, \array_flip($keysKeep));
            $html .= '
'.$this->outputHtml->dump($lastError).'
';
            if ($isHtml) {
                $html = \str_replace(\htmlspecialchars($lastError['message']), $lastError['message'], $html);
            }
        }
        if (!\extension_loaded('xdebug')) {
            $html .= '
Want to see a backtrace here?  Install xdebug PHP extension.
';
        }
        $html .= '
';
        return $html;
    }

    /**
     * Returns summary for errors that were logged to console (while collect = true)
     *
     * @return string
     */
    protected function buildInConsole()
    {
        if (!$this->stats['inConsole']) {
            return '';
        }
        $haveFatal = isset($this->stats['counts']['fatal']);
        if ($haveFatal) {
            $countNonFatal = $this->stats['inConsole'] - $this->stats['counts']['fatal']['inConsole'];
            $header = \sprintf(
                'There %s %d additional %s',
                $countNonFatal === 1 ? 'was' : 'were',
                $countNonFatal,
                $countNonFatal === 1 ? 'error' : 'errors'
            );
        } elseif ($this->stats['inConsoleCategories'] === 1) {
            return $this->buildInConsoleOneCat();
        } else {
            $header = 'There were '.$this->stats['inConsole'].' errors';
        }
        $html = '
'.$header.':
'."\n";
        $html .= '
';
        foreach ($this->stats['counts'] as $category => $a) {
            if (!$a['inConsole'] || $category == 'fatal') {
                continue;
            }
            $html .= '
'.$category.': '.$a['inConsole'].'
';
        }
        $html .= '
';
        return $html;
    }

    /**
     * Returns summary for errors that were logged to console (while collect = true)
     *
     * Assumes only 1 category of error was logged
     * However, multiple errors in this category may have been logged
     *
     * @return string
     */
    protected function buildInConsoleOneCat()
    {
        // find category
        foreach ($this->stats['counts'] as $category => $catStats) {
            if ($catStats['inConsole']) {
                break;
            }
        }
        if ($category == 'fatal') {
            return '';
        }
        $catStrings = array(
            'deprecated' => array(
                'header' => 'Deprecated',
                'msg' => 'There were %d deprecated notices',
            ),
            'strict' => array(
                'header' => 'Strict',
                'msg' => 'There were %d strict errors',
            ),
            'error' => array(
                'header' => 'Errors',
                'msg' => 'There were %d errors',
            ),
            'notice' => array(
                'header' => 'Notices',
                'msg' => 'There were %d notices',
            ),
            'warning' => array(
                'header' => 'Warnings',
                'msg' => 'There were %d warnings',
            ),
        );
        $countInCat = $catStats['inConsole'];
        if ($countInCat == 1) {
            $header = \ucfirst($category);
            $error = $this->getErrorsInCategory($category)[0];
            $msg = $error['file']. '(line '.$error['line'].'): '
                .($error['isHtml']
                    ? $error['message']
                    : \htmlspecialchars($error['message']));
        } else {
            $header = $catStrings[$category]['header'];
            $msg = \sprintf($catStrings[$category]['msg'], $countInCat);
        }
        $html = '
'.$header.'
'
            .'
'
                .'
'.$msg.'
'
                .'
';
        return $html;
    }

    /**
     * Returns summary for errors that occurred while log collect = false
     *
     * @return string
     */
    protected function buildNotInConsole()
    {
        if (!$this->stats['notInConsole']) {
            return '';
        }
        $errors = $this->errorHandler->get('errors');
        $lis = array();
        foreach ($errors as $err) {
            if (\array_intersect_assoc(array(
                // at least one of these is true
                'category' => 'fatal',
                'inConsole' => true,
                'isSuppressed' => true,
            ), $err->getValues())) {
                continue;
            }
            $lis[] = '
'.$err['typeStr'].': '.$err['file'].' (line '.$err['line'].'): '
                .($err['isHtml']
                    ? $err['message']
                    : \htmlspecialchars($err['message']))
                .'
';
        }
        if (!$lis) {
            return '';
        }
        $count = \count($lis);
        $header = \sprintf(
            '%s %s captured while not collecting debug log',
            $this->stats['inConsole'] || isset($this->stats['counts']['fatal'])
                ? 'Additionally, there'
                : 'There',
            $count === 1
                ? 'was 1 error'
                : 'were '.$count.' errors'
        );
        $html = '
'.$header.'
'
            .'
'."\n"
            .\implode("\n", $lis)."\n"
            .'
';
        return $html;
    }

    /**
     * Get the error for the given category
     *
     * @param string $category error category
     *
     * @return Event[]
     */
    protected function getErrorsInCategory($category)
    {
        $errors = $this->errorHandler->get('errors');
        $errorsInCat = array();
        foreach ($errors as $err) {
            if ($err['category'] == $category && $err['inConsole']) {
                $errorsInCat[] = $err;
            }
        }
        return $errorsInCat;
    }
}


<?php
/**
 * Manage event subscriptions
 *
 * @package   bdk\PubSub
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 * @link      http://www.github.com/bkdotcom/PubSub
 */

namespace bdk\PubSub;

/**
 * Event publish/subscribe event manager
 */
class Manager
{

    private $subscribers = array();
    private $sorted = array();

    /**
     * Constructor
     */
    public function __construct()
    {
        /*
            As a convenience, make shutdown subscribeable
        */
        \register_shutdown_function(function () {
            $this->publish('php.shutdown');
        });
    }

    /**
     * Subscribe to all of the event subscribers defined in passed obj
     *
     * @param SubscriberInterface $interface object implementing subscriber interface
     *
     * @return array a normalized list of subscriptions added.
     *      each returned is array(eventName, callable, priority)
     */
    public function addSubscriberInterface(SubscriberInterface $interface)
    {
        $subscribers = $this->getInterfaceSubscribers($interface);
        foreach ($subscribers as $row) {
            $this->subscribe($row[0], $row[1], $row[2]);
        }
        return $subscribers;
    }

    /**
     * Gets the subscribers of a specific event or all subscribers sorted by descending priority.
     *
     * If event name is not specified, subscribers for all events will be returned
     *
     * @param string $eventName The name of the event
     *
     * @return array The event subscribers for the specified event, or all event subscribers by event name
     */
    public function getSubscribers($eventName = null)
    {
        if ($eventName !== null) {
            if (!isset($this->subscribers[$eventName])) {
                return array();
            }
            if (!isset($this->sorted[$eventName])) {
                $this->sortSubscribers($eventName);
            }
            return $this->sorted[$eventName];
        }
        // return all subscribers
        foreach (\array_keys($this->subscribers) as $eventName) {
            if (!isset($this->sorted[$eventName])) {
                $this->sortSubscribers($eventName);
            }
        }
        return \array_filter($this->sorted);
    }

    /**
     * Checks whether an event has any registered subscribers.
     *
     * @param string $eventName The name of the event
     *
     * @return boolean
     */
    public function hasSubscribers($eventName = null)
    {
        if ($eventName !== null) {
            return !empty($this->subscribers[$eventName]);
        }
        foreach ($this->subscribers as $subscribers) {
            if ($subscribers) {
                return true;
            }
        }
        return false;
    }

    /**
     * Publish/Trigger/Dispatch event
     *
     * @param string $eventName      event name
     * @param mixed  $eventOrSubject passed to subscribers
     * @param array  $values         values to attach to event
     *
     * @return Event
     */
    public function publish($eventName, $eventOrSubject = null, array $values = array())
    {
        if ($eventOrSubject instanceof Event) {
            $event = $eventOrSubject;
        } else {
            $event = new Event($eventOrSubject, $values);
        }
        $subscribers = $this->getSubscribers($eventName);
        if ($subscribers) {
            $this->doPublish($eventName, $subscribers, $event);
        }
        return $event;
    }

    /**
     * Unsubscribe from all of the event subscribers defined in passed obj
     *
     * @param SubscriberInterface $interface object implementing subscriber interface
     *
     * @return array a normalized list of subscriptions removed.
     *      each returned is array(eventName, callable, priority)
     */
    public function removeSubscriberInterface(SubscriberInterface $interface)
    {
        $subscribers = $this->getInterfaceSubscribers($interface);
        foreach ($subscribers as $row) {
            $this->unsubscribe($row[0], $row[1], $row[2]);
        }
        return $subscribers;
    }

    /**
     * Subscribe to event
     *
     * @param string   $eventName event name
     * @param callable $callable  callable
     * @param integer  $priority  The higher this value, the earlier we handle event
     *
     * @return void
     */
    public function subscribe($eventName, $callable, $priority = 0)
    {
        unset($this->sorted[$eventName]);           // clear the sorted cache
        $this->subscribers[$eventName][$priority][] = $callable;
    }

    /**
     * Removes an event subscriber from the specified event.
     *
     * @param string   $eventName The event we're unsubscribing from
     * @param callable $callable  The subscriber to remove
     *
     * @return void
     */
    public function unsubscribe($eventName, $callable)
    {
        if (!isset($this->subscribers[$eventName])) {
            return;
        }
        if ($this->isClosureFactory($callable)) {
            // factory / lazy subscriber
            $callable[0] = $callable[0]();
        }
        foreach ($this->subscribers[$eventName] as $priority => $subscribers) {
            foreach ($subscribers as $k => $v) {
                if ($v !== $callable && $this->isClosureFactory($v)) {
                    $v[0] = $v[0]();
                }
                if ($v === $callable) {
                    unset($subscribers[$k], $this->sorted[$eventName]);
                } else {
                    $subscribers[$k] = $v;
                }
            }
            if ($subscribers) {
                $this->subscribers[$eventName][$priority] = $subscribers;
            } else {
                unset($this->subscribers[$eventName][$priority]);
            }
        }
    }

    /**
     * Calls the subscribers of an event.
     *
     * @param string     $eventName   The name of the event to publish
     * @param callable[] $subscribers The event subscribers
     * @param Event      $event       The event object to pass to the subscribers
     *
     * @return void
     */
    protected function doPublish($eventName, $subscribers, Event $event)
    {
        foreach ($subscribers as $callable) {
            if ($event->isPropagationStopped()) {
                break;
            }
            \call_user_func($callable, $event, $eventName, $this);
        }
    }

    /**
     * Does val appear to be a "closure factory"?
     * array & array[0] instanceof Closure
     *
     * @param mixed $val value to check
     *
     * @return boolean
     */
    private function isClosureFactory($val)
    {
        return \is_array($val) && isset($val[0]) && $val[0] instanceof \Closure;
    }

    /**
     * Calls the passed object's getSubscriptions() method and returns a normalized list of subscriptions
     *
     * @param SubscriberInterface $interface object implementing subscriber interface
     *
     * @return array
     */
    private function getInterfaceSubscribers(SubscriberInterface $interface)
    {
        $subscribers = array();
        foreach ($interface->getSubscriptions() as $eventName => $mixed) {
            if (\is_string($mixed)) {
                // methodName
                $subscribers[] = array($eventName, array($interface, $mixed), 0);
            } elseif (\count($mixed) == 2 && \is_int($mixed[1])) {
                // array('methodName', priority)
                $subscribers[] = array($eventName, array($interface, $mixed[0]), $mixed[1]);
            } else {
                foreach ($mixed as $mixed2) {
                    // methodName
                    // or array(methodName[, priority])
                    if (\is_string($mixed2)) {
                        $callable = array($interface, $mixed2);
                        $priority = 0;
                    } else {
                        $callable = array($interface, $mixed2[0]);
                        $priority = isset($mixed2[1]) ? $mixed2[1] : 0;
                    }
                    $subscribers[] = array($eventName, $callable, $priority);
                }
            }
        }
        return $subscribers;
    }

    /**
     * Sorts the internal list of subscribers for the given event by priority.
     *
     * @param string $eventName The name of the event
     *
     * @return void
     */
    private function sortSubscribers($eventName)
    {
        \krsort($this->subscribers[$eventName]);
        $this->sorted[$eventName] = array();
        foreach ($this->subscribers[$eventName] as $priority => $subscribers) {
            foreach ($subscribers as $k => $subscriber) {
                if ($this->isClosureFactory($subscriber)) {
                    $subscriber[0] = $subscriber[0]();
                    $this->subscribers[$eventName][$priority][$k] = $subscriber;
                }
                $this->sorted[$eventName][] = $subscriber;
            }
        }
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug;

/**
 * Streamwrapper which injects `declare(ticks=1)`
 *
 * @see http://php.net/manual/en/class.streamwrapper.php
 *
 * phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
 * phpcs:disable Generic.NamingConventions.CamelCapsFunctionName.ScopeNotCamelCaps
 */
class FileStreamWrapper
{
    /**
     * @var resource
     */
    public $context;

    private $bufferPrepend = '';

    private $declaredTicks = false;

    private $filepath;

    /**
     * @var resource
     */
    private $handle;

    /**
     * @var array paths to exclude from adding tick declaration
     */
    private static $pathsExclude = array();

    public static $filesModified = array();

    /**
     * @var string
     */
    const PROTOCOL = 'file';

    /**
     * Register this stream wrapper
     *
     * @param array $pathsExclude paths/directories to exclude
     *
     * @return void
     *
     * @throws \UnexpectedValueException
     */
    public static function register($pathsExclude = array())
    {
        $result = \stream_wrapper_unregister(static::PROTOCOL);
        if ($result === false) {
            throw new \UnexpectedValueException('Failed to unregister');
        }
        if ($pathsExclude) {
            self::$pathsExclude = $pathsExclude;
        }
        \stream_wrapper_register(static::PROTOCOL, \get_called_class());
        /*
            Disable OPcache
                a) want to make sure we modify required files
                b) don't want to cache modified files
        */
        \ini_set('opcache.enable', 0);
    }

    /**
     * Restore previous wrapper
     *
     * @return void
     *
     * @throws \UnexpectedValueException
     */
    private static function restorePrev()
    {
        $result = \stream_wrapper_restore(static::PROTOCOL);
        if ($result === false) {
            throw new \UnexpectedValueException('Failed to restore');
        }
    }

    /**
     * Close the directory
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.dir-closedir.php
     */
    public function dir_closedir()
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        \closedir($this->handle);
        self::register();
        $this->handle = null;
        return true;
    }

    /**
     * Opens a directory for reading
     *
     * @param string  $path    Specifies the URL that was passed to opendir().
     * @param integer $options Whether or not to enforce safe_mode (0x04).
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.dir-opendir.php
     */
    public function dir_opendir($path, $options = 0)
    {
        if ($this->handle) {
            return false;
        }
        // "use" our function params so things don't complain
        array($options);
        self::restorePrev();
        $this->handle = \opendir($path);
        self::register();
        return $this->handle !== false;
    }

    /**
     * Read a single filename of a directory
     *
     * @return string|boolean
     *
     * @see http://php.net/manual/en/streamwrapper.dir-readdir.php
     */
    public function dir_readdir()
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $success = \readdir($this->handle);
        self::register();
        return $success;
    }

    /**
     * Reset directory name pointer
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php
     */
    public function dir_rewinddir()
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        \rewinddir($this->handle);
        self::register();
        return true;
    }

    /**
     * Create a directory
     *
     * @param string  $path    Directory which should be created.
     * @param integer $mode    The value passed to mkdir().
     * @param integer $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE.
     *
     * @return boolean
     */
    public function mkdir($path, $mode, $options = 0)
    {
        self::restorePrev();
        $success = \mkdir($path, $mode, (bool) ($options & STREAM_MKDIR_RECURSIVE));
        self::register();
        return $success;
    }

    /**
     * Rename a file
     *
     * @param string $pathFrom existing path
     * @param string $pathTo   The URL which the path_from should be renamed to.
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.rename.php
     */
    public function rename($pathFrom, $pathTo)
    {
        self::restorePrev();
        $success = \rename($pathFrom, $pathTo);
        self::register();
        return $success;
    }

    /**
     * Remove a directory
     *
     * @param string  $path    directory to remove
     * @param integer $options bitwise mask of values
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.rmdir.php
     */
    public function rmdir($path, $options)
    {
        // "use" our function params so things don't complain
        array($options);
        self::restorePrev();
        $success = \rmdir($path);
        self::register();
        return $success;
    }

    /**
     * Retrieve the underlying resource
     *
     * @param integer $castAs STREAM_CAST_FOR_SELECT when stream_select() is calling stream_cast()
     *                        STREAM_CAST_AS_STREAM when stream_cast() is called for other uses
     *
     * @return resource|boolean
     *
     * @see http://php.net/manual/en/streamwrapper.stream-cast.php
     */
    public function stream_cast($castAs)
    {
        if ($this->handle && $castAs & STREAM_CAST_AS_STREAM) {
            return $this->handle;
        }
        return false;
    }

    /**
     * Close a file
     *
     * @see http://php.net/manual/en/streamwrapper.stream-close.php
     *
     * @return void
     */
    public function stream_close()
    {
        if (!$this->handle) {
            return;
        }
        self::restorePrev();
        \fclose($this->handle);
        $this->handle = null;
        self::register();
    }

    /**
     * Tests for end-of-file on a file pointer
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.stream-eof.php
     */
    public function stream_eof()
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $result = \feof($this->handle);
        self::register();
        return $result;
    }

    /**
     * Flush the output
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.stream-flush.php
     */
    public function stream_flush()
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $success = \fflush($this->handle);
        self::register();
        return $success;
    }

    /**
     * Advisory file locking
     *
     * @param integer $operation is one of the following:
     *       LOCK_SH to acquire a shared lock (reader).
     *       LOCK_EX to acquire an exclusive lock (writer).
     *       LOCK_UN to release a lock (shared or exclusive).
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.stream-lock.php
     */
    public function stream_lock($operation)
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $success = \flock($this->handle, $operation);
        self::register();
        return $success;
    }

    /**
     * Change file options
     *
     * @param string  $path   filepath or URL
     * @param integer $option What meta value is being set
     * @param mixed   $value  Meta value
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.stream-metadata.php
     */
    public function stream_metadata($path, $option, $value)
    {
        self::restorePrev();
        switch ($option) {
            case STREAM_META_TOUCH:
                if (!empty($value)) {
                    $success = \touch($path, $value[0], $value[1]);
                } else {
                    $success = \touch($path);
                }
                break;
            case STREAM_META_OWNER_NAME:
                // Fall through
            case STREAM_META_OWNER:
                $success = \chown($path, $value);
                break;
            case STREAM_META_GROUP_NAME:
                // Fall through
            case STREAM_META_GROUP:
                $success = \chgrp($path, $value);
                break;
            case STREAM_META_ACCESS:
                $success = \chmod($path, $value);
                break;
            default:
                $success = false;
        }
        self::register();
        return $success;
    }

    /**
     * Opens file or URL
     *
     * @param string   $path       Specifies the file/URL that was passed to the original function.
     * @param string   $mode       The mode used to open the file, as detailed for fopen().
     * @param integers $options    Holds additional flags set by the streams API. I
     * @param string   $openedPath the full path of the file/resource that was actually opened
     *
     * @return boolean
     *
     * @see    http://php.net/manual/en/streamwrapper.stream-open.php
     * @throws \UnexpectedValueException
     */
    public function stream_open($path, $mode, $options, &$openedPath)
    {
        if ($this->handle) {
            return false;
        }
        $useIncludePath = (bool) $options & STREAM_USE_PATH;
        $context = $this->context;
        if ($context === null) {
            $context = \stream_context_get_default();
        }
        self::restorePrev();
        if (\strpos($mode, 'r') !== false && !\file_exists($path)) {
            return false;
        } elseif (\strpos($mode, 'x') !== false && \file_exists($path)) {
            return false;
        }
        $handle = \fopen($path, $mode, $useIncludePath, $context);
        self::register();
        if ($handle === false) {
            return false;
        }
        /*
            Determine opened path
        */
        $meta = \stream_get_meta_data($handle);
        if (!isset($meta['uri'])) {
            throw new \UnexpectedValueException('Uri not in meta data');
        }
        $this->filepath = $openedPath = $meta['uri'];
        $this->handle = $handle;
        return true;
    }

    /**
     * Read from stream
     *
     * @param integer $count How many bytes of data from the current position should be returned.
     *
     * @return string
     *
     * @see http://php.net/manual/en/streamwrapper.stream-read.php
     */
    public function stream_read($count)
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $buffer = \fread($this->handle, $count);
        $bufferLen = \strlen($buffer);
        $backtrace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
        $isRequire = !\in_array($backtrace[1]['function'], array('file_get_contents'));
        if (!$this->declaredTicks && $isRequire) {
            foreach (self::$pathsExclude as $excludePath) {
                if (\strpos($this->filepath, $excludePath.DIRECTORY_SEPARATOR) === 0) {
                    $this->declaredTicks = true;
                }
            }
        }
        if (!$this->declaredTicks && $isRequire) {
            // insert declare(ticks=1);  without adding any new lines
            $buffer = \preg_replace(
                '/^(<\?php\s*)$/m',
                '$0 declare(ticks=1);',
                $buffer,
                1
            );
            $this->declaredTicks = true;
            self::$filesModified[] = $this->filepath;
        }
        $buffer = $this->bufferPrepend.$buffer;
        $bufferLenAfter = \strlen($buffer);
        $diff = $bufferLenAfter - $bufferLen;
        $this->bufferPrepend = '';
        if ($diff) {
            $this->bufferPrepend = \substr($buffer, $count);
            $buffer = \substr($buffer, 0, $count);
        }
        self::register();
        return $buffer;
    }

    /**
     * Seek to specific location in a stream
     *
     * @param integer $offset The stream offset to seek to
     * @param integer $whence [SEEK_SET] | SEEK_CUR | SEEK_END
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.stream-seek.php
     */
    public function stream_seek($offset, $whence = SEEK_SET)
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $result = \fseek($this->handle, $offset, $whence);
        $success = $result !== -1;
        self::register();
        return $success;
    }

    /**
     * Retrieve information about a file resource
     *
     * @return array
     *
     * @see http://php.net/manual/en/streamwrapper.stream-stat.php
     */
    public function stream_stat()
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $array = \fstat($this->handle);
        self::register();
        return $array;
    }

    /**
     * Retrieve the current position of a stream
     *
     * @return integer
     *
     * @see http://php.net/manual/en/streamwrapper.stream-tell.php
     */
    public function stream_tell()
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $position = \ftell($this->handle);
        self::register();
        return $position;
    }

    /**
     * Truncates a file to the given size
     *
     * @param integer $size Truncate to this size
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.stream-truncate.php
     */
    public function stream_truncate($size)
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $success = \ftruncate($this->handle, $size);
        self::register();
        return $success;
    }

    /**
     * Write to stream
     *
     * @param string $data data to write
     *
     * @return integer
     *
     * @see http://php.net/manual/en/streamwrapper.stream-write.php
     */
    public function stream_write($data)
    {
        if (!$this->handle) {
            return false;
        }
        self::restorePrev();
        $length = \fwrite($this->handle, $data);
        self::register();
        return $length;
    }

    /**
     * Unlink a file
     *
     * @param string $path filepath
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/streamwrapper.unlink.php
     */
    public function unlink($path)
    {
        self::restorePrev();
        $success = \unlink($path);
        self::register();
        return $success;
    }

    /**
     * Retrieve information about a file
     *
     * @param string  $path  The file path or URL to stat
     * @param integer $flags Holds additional flags set by the streams API.
     *
     * @return array
     *
     * @see http://php.net/manual/en/streamwrapper.url-stat.php
     */
    public function url_stat($path, $flags)
    {
        self::restorePrev();
        if (!\file_exists($path)) {
            $info = false;
        } elseif ($flags & STREAM_URL_STAT_LINK) {
            $info = $flags & STREAM_URL_STAT_QUIET
                ? @\lstat($path)
                : \lstat($path);
        } else {
            $info = $flags & STREAM_URL_STAT_QUIET
                ? @\stat($path)
                : \stat($path);
        }
        self::register();
        return $info;
    }
}


<?php
/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.1.1
 *
 * @see https://craig.is/writing/chrome-logger/techspecs
 */

namespace bdk\Debug\Output;

use bdk\PubSub\Event;
use bdk\Debug;

/**
 * Output log as via ChromeLogger headers
 *
 * ChromeLogger supports the following methods/log-types:
 * log, warn, error, info, group, groupEnd, groupCollapsed, and table
 */
class ChromeLogger extends Base
{

    const HEADER_NAME = 'X-ChromeLogger-Data';

    protected $consoleMethods = array(
        'assert',
        'count',    // output as log
        'error',
        'group',
        'groupCollapsed',
        'groupEnd',
        'info',
        'log',
        'table',
        'time',     // output as log
        'timeEnd',  // PHPDebugConsole never generates a timeEnd entry
        'trace',
        'warn',
    );

    /**
     * @var array header data
     */
    protected $json = array(
        'version' => Debug::VERSION,
        'columns' => array('log', 'backtrace', 'type'),
        'rows' => array()
    );

    /**
     * Output the log as chromelogger headers
     *
     * @param Event $event debug.output event object
     *
     * @return void
     */
    public function onOutput(Event $event)
    {
        $this->channelName = $this->debug->getCfg('channel');
        $this->data = $this->debug->getData();
        $this->processAlerts();
        $this->processSummary();
        $this->processLog();
        if ($this->json['rows']) {
            \array_unshift($this->json['rows'], array(
                array('PHP', isset($_SERVER['REQUEST_METHOD'])
                    ? $_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI']
                    : '$: '. \implode(' ', $_SERVER['argv'])
                ),
                null,
                'groupCollapsed',
            ));
            \array_push($this->json['rows'], array(
                array(),
                null,
                'groupEnd',
            ));
            $encoded = $this->encode($this->json);
            if (\strlen($encoded) > 250000) {
                $this->debug->warn('chromeLogger: output limit exceeded');
            } else {
                $event['headers'][] = array(self::HEADER_NAME, $encoded);
            }
        }
        $this->data = array();
        $this->json['rows'] = array();
    }

    /**
     * Process log entry
     *
     * Transmogrify log entry to chromelogger format
     *
     * @param string $method method
     * @param array  $args   arguments
     * @param array  $meta   meta values
     *
     * @return void
     */
    public function processLogEntry($method, $args = array(), $meta = array())
    {
        if ($method === 'alert') {
            list($method, $args) = $this->methodAlert($args, $meta);
        } elseif ($method == 'assert') {
            \array_unshift($args, false);
        } elseif (\in_array($method, array('count','time'))) {
            $method = 'log';
        } elseif (\in_array($method, array('profileEnd','table'))) {
            $method = 'log';
            if (\is_array($args[0])) {
                $method = 'table';
                $args = array($this->methodTable($args[0], $meta['columns']));
            } elseif ($meta['caption']) {
                \array_unshift($args, $meta['caption']);
            }
        } elseif ($method === 'trace') {
            $method = 'table';
            $args = array($this->methodTable($args[0], array('function','file','line')));
        }
        if (!\in_array($method, $this->consoleMethods)) {
            $method = 'log';
        }
        foreach ($args as $i => $arg) {
            $args[$i] = $this->dump($arg);
        }
        $this->json['rows'][] = array(
            $args,
            isset($meta['file']) ? $meta['file'].': '.$meta['line'] : null,
            $method === 'log' ? '' : $method,
        );
    }

    /**
     * Encode data for header
     *
     * @param array $data log data
     *
     * @return string encoded data for header
     */
    protected function encode($data)
    {
        return \base64_encode(\utf8_encode(\json_encode($data)));
    }

    /**
     * Handle alert method
     *
     * @param array $args arguments
     * @param array $meta meta info
     *
     * @return array array($method, $args)
     */
    protected function methodAlert($args, $meta)
    {
        $args = array('%c'.$args[0], '');
        $method = $meta['class'];
        $styleCommon = 'padding:5px; line-height:26px; font-size:125%; font-weight:bold;';
        switch ($method) {
            case 'danger':
                // Just use log method... Chrome adds backtrace to error(), which we don't want
                $method = 'log';
                $args[1] = $styleCommon
                    .'background-color: #ffbaba;'
                    .'border: 1px solid #d8000c;'
                    .'color: #d8000c;';
                break;
            case 'info':
                $args[1] = $styleCommon
                    .'background-color: #d9edf7;'
                    .'border: 1px solid #bce8f1;'
                    .'color: #31708f;';
                break;
            case 'success':
                $method = 'info';
                $args[1] = $styleCommon
                    .'background-color: #dff0d8;'
                    .'border: 1px solid #d6e9c6;'
                    .'color: #3c763d;';
                break;
            case 'warning':
                // Just use log method... Chrome adds backtrace to warn(), which we don't want
                $method = 'log';
                $args[1] = $styleCommon
                    .'background-color: #fcf8e3;'
                    .'border: 1px solid #faebcc;'
                    .'color: #8a6d3b;';
                break;
        }
        return array($method, $args);
    }
}


<?php
/**
 * Route PHPDebugConsole method calls to
 * WAMP (Web Application Messaging Protocol) router
 *
 * This plugin requires bdk/wamp-publisher (not included)
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2019 Brad Kent
 * @version   v2.3
 */

namespace bdk\Debug\Output;

use bdk\Debug;
use bdk\PubSub\Event;
use bdk\WampPublisher;

/**
 * PHPDebugConsole plugin for routing debug messages thru WAMP router
 */
class Wamp implements OutputInterface
{

    public $debug;
    protected $name = 'wamp';
    public $requestId;
    public $topic = 'bdk.debug';
    public $wamp;
    protected $channelName = '';

    /**
     * Constructor
     *
     * @param Debug         $debug Debug instance
     * @param WampPublisher $wamp  WAMP Publisher instance
     */
    public function __construct(Debug $debug, WampPublisher $wamp)
    {
        $this->debug = $debug;
        $this->wamp = $wamp;
        $this->requestId = $this->debug->getData('requestId');
    }

    /**
     * {@inheritdoc}
     */
    public function dump($val)
    {
    }

    /**
     * Return a list of event subscribers
     *
     * @return array The event names to subscribe to
     */
    public function getSubscriptions()
    {
        if (!$this->isConnected()) {
            $this->debug->alert('WAMP publisher not connected to WAMP router');
            return array();
        }
        $this->publishMeta();
        $this->processExistingData();
        return array(
            'debug.log' => array('onLog', PHP_INT_MAX * -1),
            'errorHandler.error' => 'onError',    // assumes errorhandler is using same dispatcher.. as should be
            'php.shutdown' => array('onShutdown', PHP_INT_MAX * -1),
        );
    }

    /**
     * Is WAMP publisher connected?
     *
     * @return boolean
     */
    public function isConnected()
    {
        return $this->wamp->connected;
    }

    /**
     * errorHandler.error event subscriber
     *
     * Used to capture errors that aren't sent to log (ie debug capture is false)
     *
     * @param Event $event event object
     *
     * @return void
     */
    public function onError(Event $event)
    {
        if ($event['inConsole'] || !$event['isFirstOccur']) {
            return;
        }
        $this->processLogEntry(
            'errorNotConsoled',
            array(
                $event['typeStr'].': '.$event['file'].' (line '.$event['line'].'): '.$event['message']
            ),
            array(
                'channel' => 'phpError',
                'class' => $event['type'] & $this->debug->getCfg('errorMask')
                    ? 'danger'
                    : 'warning',
            )
        );
    }

    /**
     * debug.log event subscriber
     *
     * @param Event $event event object
     *
     * @return void
     */
    public function onLog(Event $event)
    {
        $this->processLogEntryWEvent($event['method'], $event['args'], $event['meta']);
    }

    /**
     * php.shutdown event subscriber
     *
     * @return void
     */
    public function onShutdown()
    {
        // publish a "we're done" message
        $this->processLogEntry(
            'endOutput',
            array(),
            array(
                'responseCode' => \http_response_code(),
                'channel' => $this->debug->getCfg('channel'),
            )
        );
    }

    /**
     * Publish WAMP message to topic
     *
     * @param string $method debug method
     * @param array  $args   arguments
     * @param array  $meta   meta values
     *
     * @return void
     */
    public function processLogEntry($method, $args = array(), $meta = array())
    {
        $meta = \array_merge(array(
            'format' => 'raw',
            'requestId' => $this->requestId,
        ), $meta);
        if ($meta['channel'] == $this->debug->getCfg('channel')) {
            unset($meta['channel']);
        }
        if ($meta['format'] == 'raw') {
            $args = $this->crateValues($args);
        }
        if (!empty($meta['backtrace'])) {
            $meta['backtrace'] = $this->crateValues($meta['backtrace']);
        }
        $this->wamp->publish($this->topic, array($method, $args, $meta));
    }

    /**
     * JSON doesn't handle binary well (at all)
     *     a) strings with invalid utf-8 can't be json_encoded
     *     b) "javascript has a unicode problem" / will munge strings
     *   base64_encode all strings!
     *
     * Associative arrays get JSON encoded to js objects...
     *     Javascript doesn't maintain order for object properties
     *     in practice this seems to only be an issue with int/numeric keys
     *     store property order
     *
     * @param array $values array structure
     *
     * @return array
     */
    private function crateValues($values)
    {
        $prevIntK = null;
        $storeKeyOrder = false;
        foreach ($values as $k => $v) {
            if (!$storeKeyOrder && \is_int($k)) {
                if ($k < $prevIntK) {
                    $storeKeyOrder = true;
                }
                $prevIntK = $k;
            }
            if (\is_array($v)) {
                $values[$k] = self::crateValues($v);
            } elseif (\is_string($v) && !$this->debug->utf8->isUtf8($v)) {
                $values[$k] = '_b64_:'.\base64_encode($v);
            }
        }
        if ($storeKeyOrder) {
            $values['__debug_key_order__'] = \array_keys($values);
        }
        return $values;
    }

    /**
     * Publish pre-existing log entries
     *
     * @return void
     */
    private function processExistingData()
    {
        $data = $this->debug->getData();
        $channelName = $this->debug->getCfg('channel');
        foreach ($data['alerts'] as $entry) {
            $this->processLogEntryWEvent($entry[0], $entry[1], $entry[2]);
        }
        foreach ($data['logSummary'] as $priority => $entries) {
            $this->processLogEntryWEvent(
                'groupSummary',
                array(),
                array(
                    'channel' => $channelName,
                    'priority'=> $priority,
                )
            );
            foreach ($entries as $entry) {
                $this->processLogEntryWEvent($entry[0], $entry[1], $entry[2]);
            }
            $this->processLogEntryWEvent(
                'groupEnd',
                array(),
                array(
                    'channel' => $channelName,
                    'closesSummary'=>true,
                )
            );
        }
        foreach ($data['log'] as $entry) {
            $this->processLogEntryWEvent($entry[0], $entry[1], $entry[2]);
        }
    }

    /**
     * Process/publish a log entry
     *
     * @param string $method method
     * @param array  $args   args
     * @param array  $meta   meta values
     *
     * @return void
     */
    protected function processLogEntryWEvent($method, $args = array(), $meta = array())
    {
        if (!isset($meta['channel'])) {
            $meta['channel'] = $this->channelName;
        }
        $event = $this->debug->eventManager->publish(
            'debug.outputLogEntry',
            $this,
            array(
                'method' => $method,
                'args' => $args,
                'meta' => $meta,
            )
        );
        if (!$event->isPropagationStopped()) {
            $this->processLogEntry($event['method'], $event['args'], $event['meta']);
        }
    }

    /**
     * Publish initial meta data
     *
     * @return void
     */
    private function publishMeta()
    {
        $debugClass = \get_class($this->debug);
        $metaVals = array(
            'debug_version' => $debugClass::VERSION,
        );
        $keys = array(
            'HTTP_HOST',
            'HTTPS',
            'REMOTE_ADDR',
            'REQUEST_METHOD',
            'REQUEST_TIME',
            'REQUEST_URI',
            'SERVER_ADDR',
            'SERVER_NAME',
        );
        foreach ($keys as $k) {
            $metaVals[$k] = isset($_SERVER[$k])
                ? $_SERVER[$k]
                : null;
        }
        if (!isset($metaVals['REQUEST_URI']) && !empty($_SERVER['argv'])) {
            $metaVals['REQUEST_URI'] = '$: '. \implode(' ', $_SERVER['argv']);
        }
        $this->processLogEntry(
            'meta',
            array(
                $metaVals,
            ),
            array(
                'channel' => $this->debug->getCfg('channel'),
            )
        );
    }
}