<?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;

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

/**
 * General Output methods
 */
class Output implements SubscriberInterface
{

    private $cfg = array();
    private $debug;

    /**
     * Constructor
     *
     * @param \bdk\Debug $debug debug instance
     * @param array      $cfg   configuration
     */
    public function __construct(Debug $debug, $cfg = array())
    {
        $this->debug = $debug;
        $this->cfg = array(
            'addBR' => false,
            'css' => '',                    // additional "override" css
            'displayListKeys' => true,
            'filepathCss' => __DIR__.'/css/Debug.css',
            'filepathScript' => __DIR__.'/js/Debug.jquery.min.js',
            'onOutput'  => null,            // set to something callable
            'outputAs'  => null,            // 'chromeLogger', 'html', 'script', 'text', or Object, if null, will be determined automatically
            'outputAsDefaultNonHtml' => 'chromeLogger',
            'outputConstants' => true,
            'outputCss' => true,            // applies when outputAs = 'html'
            'outputHeaders' => true,        // ie, ChromeLogger and/or firePHP headers
            'outputMethodDescription' => true, // (or just summary)
            'outputMethods' => true,
            'outputScript' => true,         // applies when outputAs = 'html'
        );
        $this->setCfg($cfg);
    }

    /**
     * Magic getter
     *
     * @param string $prop property to get
     *
     * @return mixed
     */
    public function __get($prop)
    {
        if (\strpos($prop, 'output') === 0) {
            $this->debug->errorHandler->setErrorCaller();
            \trigger_error('output->'.$prop.' is deprecated, use output->'.\lcfirst(\substr($prop, 6)).' instead', E_USER_DEPRECATED);
            $prop = \lcfirst(\substr($prop, 6));
            if ($this->{$prop}) {
                return $this->{$prop};
            }
        }
        $classname = __NAMESPACE__.'\\Output\\'.\ucfirst($prop);
        if (\class_exists($classname)) {
            $this->{$prop} = new $classname($this->debug);
            // note: we don't add as plugin / OutputInterface here
            return $this->{$prop};
        }
    }

    /**
     * Get config val
     *
     * @param string $path what to get
     *
     * @return mixed
     */
    public function getCfg($path = null)
    {
        if ($path == 'outputAs') {
            $ret = $this->cfg['outputAs'];
            if (!$ret) {
                $ret = $this->getDefaultOutputAs();
            }
        } elseif ($path == 'css') {
            $ret = $this->getCss();
        } else {
            $ret = $this->debug->utilities->arrayPathGet($this->cfg, $path);
        }
        return $ret;
    }

    /**
     * Return the log's CSS
     *
     * @return string
     */
    public function getCss()
    {
        $return = \file_get_contents($this->cfg['filepathCss']);
        $return .= "\n";
        if (!empty($this->cfg['css'])) {
            $return .= $this->cfg['css']."\n";
        }
        return $return;
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscriptions()
    {
        return array(
            'debug.output' => array('onOutput', 1),
        );
    }

    /**
     * debug.output subscriber
     *
     * @param Event $event debug.output event object
     *
     * @return void
     */
    public function onOutput(Event $event)
    {
        if (!$event['isTarget']) {
            /*
                All channels share the same data.
                We only need to do this via the channel that called output
            */
            return;
        }
        $this->data = $this->debug->getData();
        $this->closeOpenGroups();
        foreach ($this->data['logSummary'] as &$log) {
            $this->removeHideIfEmptyGroups($log);
            $this->uncollapseErrors($log);
        }
        $this->removeHideIfEmptyGroups($this->data['log']);
        $this->uncollapseErrors($this->data['log']);
        $this->debug->setData($this->data);
    }

    /**
     * Set one or more config values
     *
     * If setting a single value, old value is returned
     *
     * @param string $mixed  key=>value array or key
     * @param mixed  $newVal value
     *
     * @return mixed returns previous value
     */
    public function setCfg($mixed, $newVal = null)
    {
        $ret = null;
        $values = array();
        if (\is_string($mixed)) {
            $key = $mixed;
            $ret = isset($this->cfg[$key])
                ? $this->cfg[$key]
                : null;
            $values = array(
                $key => $newVal,
            );
        } elseif (\is_array($mixed)) {
            $values = $mixed;
        }
        if (isset($values['outputAs'])) {
            $this->setOutputAs($values['outputAs']);
            unset($values['outputAs']); // setOutputAs does the setting
        }
        if (isset($values['onOutput'])) {
            /*
                Replace - not append - subscriber set via setCfg
            */
            if (isset($this->cfg['onOutput'])) {
                $this->debug->eventManager->unsubscribe('debug.output', $this->cfg['onOutput']);
            }
            $this->debug->eventManager->subscribe('debug.output', $values['onOutput']);
        }
        $this->cfg = $this->debug->utilities->arrayMergeDeep($this->cfg, $values);
        return $ret;
    }

    /**
     * Close any unclosed groups
     *
     * We may have forgotten to end a group or the script may have exited
     *
     * @return void
     */
    private function closeOpenGroups()
    {
        $this->data['groupPriorityStack'][] = 'main';
        while ($this->data['groupPriorityStack']) {
            $priority = \array_pop($this->data['groupPriorityStack']);
            foreach ($this->data['groupStacks'][$priority] as $i => $info) {
                if ($info['collect']) {
                    unset($this->data['groupStacks'][$priority][$i]);
                    $meta = array(
                        'channel' => $info['channel'],
                    );
                    if ($priority === 'main') {
                        $this->data['log'][] = array('groupEnd', array(), $meta);
                    } else {
                        $this->data['logSummary'][$priority][] = array('groupEnd', array(), $meta);
                    }
                }
            }
        }
    }

    /**
     * Determine default outputAs
     *
     * @return string
     */
    private function getDefaultOutputAs()
    {
        $ret = 'html';
        $interface = $this->debug->utilities->getInterface();
        if ($interface == 'ajax') {
            $ret = $this->cfg['outputAsDefaultNonHtml'];
        } elseif ($interface == 'http') {
            $contentType = $this->debug->utilities->getResponseHeader();
            if ($contentType && $contentType !== 'text/html') {
                $ret = $this->cfg['outputAsDefaultNonHtml'];
            }
        } else {
            $ret = 'text';
        }
        return $ret;
    }

    /**
     * Remove empty groups with 'hideIfEmpty' meta value
     *
     * @param array $log log or summary
     *
     * @return void
     */
    private function removeHideIfEmptyGroups(&$log)
    {
        $groupStack = array();
        $groupStackCount = 0;
        $removed = false;
        for ($i = 0, $count = \count($log); $i < $count; $i++) {
            $method = $log[$i][0];
            if (\in_array($method, array('group', 'groupCollapsed'))) {
                $entry = $log[$i];
                $groupStack[] = array(
                    'i' => $i,
                    'meta' => !empty($entry[2]) ? $entry[2] : array(),
                    'hasEntries' => false,
                );
                $groupStackCount ++;
            } elseif ($method == 'groupEnd') {
                $group = \end($groupStack);
                if (!$group['hasEntries'] && !empty($group['meta']['hideIfEmpty'])) {
                    // make it go away
                    unset($log[$group['i']]);
                    unset($log[$i]);
                    $removed = true;
                }
                \array_pop($groupStack);
                $groupStackCount--;
            } elseif ($groupStack) {
                $groupStack[$groupStackCount - 1]['hasEntries'] = true;
            }
        }
        if ($removed) {
            $log = \array_values($log);
        }
    }

    /**
     * Set outputAs value
     * instantiate object if necessary & addPlugin if not already subscribed
     *
     * @param OutputInterface|string $outputAs OutputInterface instance, or (short) classname
     *
     * @return void
     */
    private function setOutputAs($outputAs)
    {
        if (\is_object($this->cfg['outputAs'])) {
            /*
                unsubscribe current OutputInterface
                there can only be one 'outputAs' at a time
                if multiple output routes are desired, use debug->addPlugin()
            */
            $this->debug->removePlugin($this->cfg['outputAs']);
            $this->cfg['outputAs'] = null;
        }
        $prop = null;
        $obj = null;
        if (\is_string($outputAs)) {
            $prop = $outputAs;
            $classname = __NAMESPACE__.'\\Output\\'.\ucfirst($outputAs);
            if (\property_exists($this, $prop)) {
                $obj = $this->{$prop};
            } elseif (\class_exists($classname)) {
                $obj = new $classname($this->debug);
            }
        } elseif ($outputAs instanceof OutputInterface) {
            $classname = \get_class($outputAs);
            $prefix = __NAMESPACE__.'\\Output\\';
            if (\strpos($classname, $prefix) == 0) {
                $prop = \substr($classname, \strlen($prefix));
                $prop = \lcfirst($prop);
            }
            $obj = $outputAs;
        }
        if ($obj) {
            $this->debug->addPlugin($obj);
            $this->cfg['outputAs'] = $obj;
            if ($prop) {
                $this->{$prop} = $obj;
            }
        }
    }

    /**
     * Uncollapse groups containing errors.
     *
     * @param array $log log or summary
     *
     * @return void
     */
    private function uncollapseErrors(&$log)
    {
        $groupStack = array();
        for ($i = 0, $count = \count($log); $i < $count; $i++) {
            $method = $log[$i][0];
            if (\in_array($method, array('group', 'groupCollapsed'))) {
                $groupStack[] = $i;
            } elseif ($method == 'groupEnd') {
                \array_pop($groupStack);
            } elseif (\in_array($method, array('error', 'warn'))) {
                foreach ($groupStack as $i2) {
                    $log[$i2][0] = 'group';
                }
            }
        }
    }
}


<?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;

use bdk\PubSub\Event;

/**
 * Clear method
 */
class MethodProfile
{

    /**
     * @var string Ignore methods in these namespaces
     */
    protected $nsIgnoreRegex;

    /**
     * @var array profile data
     */
    protected $data = array();

    protected $funcStack = array();
    protected $isProfiling = false;
    protected $rootStack = array();
    protected $timeLastTick = null;

    /**
     * Constructor
     *
     * @param array $namespacesIgnore array of namespaces who's methods will be excluded from profile
     */
    public function __construct($namespacesIgnore = array())
    {
        $namespacesIgnore = \array_merge(array(__NAMESPACE__), $namespacesIgnore);
        $this->nsIgnoreRegex = \str_replace('\\', '\\\\', '#^('.\implode('|', $namespacesIgnore).')(\\|$)#');
        $this->start();
    }

    /**
     * End profiling and return data
     *
     * @return array profile data
     */
    public function end()
    {
        \unregister_tick_function(array($this, 'tickFunction'));
        while ($this->funcStack) {
            $this->popStack();
        }
        // sort by totalTime descending
        \uasort($this->data, function ($valA, $valB) {
            return ($valA['totalTime'] < $valB['totalTime']) ? 1 : -1;
        });
        $data =  \array_map(function ($row) {
            $row['totalTime'] = \round($row['totalTime'], 6);
            $row['ownTime'] = \round($row['ownTime'], 6);
            return $row;
        }, $this->data);
        $this->data = array();
        $this->funcStack = array();
        $this->isProfiling = false;
        $this->rootStack = array();
        return $data;
    }

    /**
     * Set initial stack info
     *
     * @return boolean
     */
    public function start()
    {
        if ($this->isProfiling) {
            return false;
        }
        $backtrace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
        $backtrace = $this->backtraceRemoveInternal($backtrace);
        foreach ($backtrace as $frame) {
            $class = isset($frame['class']) ? $frame['class'].'::' : '';
            $this->rootStack[] = $class.$frame['function'];
        }
        \register_tick_function(array($this, 'tickFunction'));
        $this->isProfiling = true;
        $this->timeLastTick = \microtime(true);
        return true;
    }

    /**
     * Tick function
     *
     * @return void
     */
    public function tickFunction()
    {
        $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
        $stackCount = \count($trace) - \count($this->rootStack) - 1;
        $stackCountInternal = \count($this->funcStack);
        $class = isset($trace[1]['class']) ? $trace[1]['class'] : null;
        if ($stackCount === 0 && $this->data) {
            $function = \ltrim($class.'::'.$trace[1]['function'], ':');
            if ($function !== $this->rootStack[0]) {
                // We've traveled up the stack above where we started
                \array_shift($this->rootStack);
                $this->pushStack($function);
            }
            $this->timeLastTick = \microtime(true);
            return;
        }
        if (\array_filter(array(
            $stackCount < 1,
            $stackCount === $stackCountInternal,        // no change in stack
            \preg_match($this->nsIgnoreRegex, $class)
        ))) {
            $this->timeLastTick = \microtime(true);
            return;
        }
        if ($stackCount > $stackCountInternal) {
            $diff = $stackCount - $stackCountInternal;
            for ($i = $diff; $i > 0; $i--) {
                $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : null;
                if (\preg_match($this->nsIgnoreRegex, $class)) {
                    break;
                }
                $function = \ltrim($class.'::'.$trace[$i]['function'], ':');
                $this->pushStack($function);
            }
        } else {
            $this->popStack();
        }
        $this->timeLastTick = \microtime(true);
    }

    /**
     * Remove internal frames from backtrace
     *
     * @param array $backtrace backtrace
     *
     * @return array
     */
    protected function backtraceRemoveInternal($backtrace)
    {
        $count = \count($backtrace);
        for ($i = $count - 1; $i > 0; $i--) {
            $frame = $backtrace[$i];
            if (isset($frame['class']) && \strpos($frame['class'], __NAMESPACE__) === 0) {
                break;
            }
        }
        $i++;
        return \array_slice($backtrace, $i);
    }

    /**
     * Remove function from stack and add time to profile
     *
     * @return string name of popped function
     */
    protected function popStack()
    {
        $stackInfo = \array_pop($this->funcStack);
        $funcPopped = $stackInfo['function'];
        $timeElapsed = \microtime(true) - $stackInfo['tsStart'];
        $this->data[$funcPopped]['ownTime'] += $timeElapsed - $stackInfo['subTime'];
        $this->data[$funcPopped]['totalTime'] += $timeElapsed;
        if ($this->data[$funcPopped]['calls'] === 0) {
            $this->data[$funcPopped]['calls'] ++;
        }
        if ($this->funcStack) {
            $this->funcStack[\count($this->funcStack)-1]['subTime'] += $timeElapsed;
        }
        return $stackInfo['function'];
    }

    /**
     * Add function to call stack
     *
     * @param string $funcName fully qualified function name
     *
     * @return void
     */
    protected function pushStack($funcName)
    {
        $this->funcStack[] = array(
            'function' => $funcName,
            'tsStart' => $this->timeLastTick,
            'subTime' => 0,         // how much time spent in nested functions
        );
        if (!isset($this->data[$funcName])) {
            $this->data[$funcName] = array(
                'calls' => 0,
                'totalTime' => 0,   // time spent in function and nested func
                'ownTime' => 0,     // time spent in function excluding nested funcs
            );
        }
        $this->data[$funcName]['calls'] ++;
    }
}


<?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;

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

/**
 * Configuration manager
 */
class Config
{

    protected $cfg = array();
    protected $cfgLazy = array();  // store config for child classes that haven't been loaded yet
    protected $debug;
    protected $configKeys;

    /**
     * Constructor
     *
     * @param \bdk\Debug $debug debug object
     * @param array      $cfg   configuration
     */
    public function __construct(Debug $debug, &$cfg)
    {
        $this->cfg = &$cfg;
        $this->debug = $debug;
        $this->cfgLazy['errorEmailer']['emailBacktraceDumper'] = function ($backtrace) use ($debug) {
            return $debug->output->text->dump($backtrace);
        };
    }

    /**
     * Get debug or child configuration value(s)
     *
     * @param string $path what to get
     *
     * @return mixed
     */
    public function getCfg($path = '')
    {
        $path = $this->normalizePath($path);
        $path = \array_filter(\preg_split('#[\./]#', $path), 'strlen');
        if (empty($path)) {
            return $this->getCfgAll();
        }
        $classname = \array_shift($path);
        if ($classname == 'debug') {
            return $this->debug->utilities->arrayPathGet($this->cfg, $path);
        } elseif (isset($this->debug->{$classname}) && \is_object($this->debug->{$classname})) {
            $pathRel = \implode('/', $path);
            return $this->debug->{$classname}->getCfg($pathRel);
        }
        if (isset($this->cfgLazy[$classname]) && $path) {
            // want a config value of obj that has not yet been instantiated...
            // value may in cfgLazy
            $val = $this->debug->utilities->arrayPathGet($this->cfgLazy[$classname], $path);
            if ($val !== null) {
                return $val;
            }
        }
        if (isset($this->cfg['services'][$classname])) {
            // getting value of uninitialized obj
            // inititalize obj and retry
            $pathRel = \implode('/', $path);
            return $this->debug->{$classname}->getCfg($pathRel);
        }
        return null;
    }

    /**
     * Get config for lazy-loaded class
     *
     * @param string $name name of property being lazy loaded
     *
     * @return array
     */
    public function getCfgLazy($name)
    {
        if (!isset($this->cfgLazy[$name])) {
            return array();
        }
        $return = $this->cfgLazy[$name];
        unset($this->cfgLazy[$name]);
        return $return;
    }

    /**
     * Set one or more config values
     *
     *    setCfg('key', 'value')
     *    setCfg('level1.level2', 'value')
     *    setCfg(array('k1'=>'v1', 'k2'=>'v2'))
     *
     * Setting/updating 'key' will also set 'collect' and 'output'
     *
     * Triggers a debug.config event that contains all changed values
     *
     * @param string|array $pathOrVals key/path or cfg array
     * @param mixed        $val        (optional) value
     *
     * @return mixed
     */
    public function setCfg($pathOrVals, $val = null)
    {
        if (\is_array($pathOrVals)) {
            $cfg = $this->normalizeArray($pathOrVals);
        } else {
            $path = $this->normalizePath($pathOrVals);
            $cfg = $this->keyValToArray($path, $val);
        }
        $cfg = $this->setCopyValues($cfg);
        $return = $this->doSetCfg($cfg);
        if (isset($this->cfgLazy['output']['outputAs'])) {
            $lazyPlugins = array('chromeLogger','firephp','html','script','text');
            if (\is_object($this->cfgLazy['output']['outputAs']) || !\in_array($this->cfgLazy['output']['outputAs'], $lazyPlugins)) {
                // output is likely a dependency
                $outputAs = $this->cfgLazy['output']['outputAs'];
                unset($this->cfgLazy['output']['outputAs']);
                // this will autoload output, which will pull in cfgLazy...
                //   we then set outputAs
                $this->debug->output->setCfg('outputAs', $outputAs);
            }
        }
        if (\is_string($pathOrVals)) {
            $return = $this->debug->utilities->arrayPathGet($return, $path);
        }
        if ($cfg) {
            $this->debug->eventManager->publish(
                'debug.config',
                $this->debug,
                array('config'=>$cfg)
            );
        }
        return $return;
    }

    /**
     * Test $_REQUEST['debug'] against passed key
     * return collect & output values
     *
     * @param string $key secret key
     *
     * @return array
     */
    private function debugKeyValues($key)
    {
        $values = array();
        // update 'collect and output'
        $requestKey = null;
        if (isset($_REQUEST['debug'])) {
            $requestKey = $_REQUEST['debug'];
        } elseif (isset($_COOKIE['debug'])) {
            $requestKey = $_COOKIE['debug'];
        }
        $isValidKey = $requestKey == $key;
        if ($isValidKey) {
            // only enable collect / don't disable it
            $values['collect'] = true;
        }
        $values['output'] = $isValidKey;
        return $values;
    }

    /**
     * Set cfg values for Debug and child classes
     *
     * @param array $cfg config values grouped by class
     *
     * @return array previous values
     */
    private function doSetCfg($cfg)
    {
        $return = array();
        foreach ($cfg as $k => $v) {
            if ($k == 'debug') {
                $return[$k] = \array_intersect_key($this->cfg, $v);
                $this->setDebugCfg($v);
            } elseif (isset($this->debug->{$k}) && \is_object($this->debug->{$k})) {
                $return[$k] = \array_intersect_key($this->getCfg($k.'/*'), $v);
                $this->debug->{$k}->setCfg($v);
            } elseif (isset($this->cfgLazy[$k])) {
                $return[$k] = \array_intersect_key($this->cfgLazy[$k], $v);
                $this->cfgLazy[$k] = \array_merge($this->cfgLazy[$k], $v);
            } else {
                $return[$k] = array();
                $this->cfgLazy[$k] = $v;
            }
        }
        return $return;
    }

    /**
     * Get config for debug.
     * If no path specified, config for debug and dependencies is returned
     *
     * @return mixed
     */
    private function getCfgAll()
    {
        $cfg = array();
        foreach (\array_keys($this->configKeys) as $classname) {
            if ($classname === 'debug') {
                $cfg['debug'] = $this->cfg;
            } elseif (isset($this->debug->{$classname})) {
                $cfg[$classname] = $this->debug->{$classname}->getCfg();
            } elseif (isset($this->cfgLazy[$classname])) {
                $cfg[$classname] = $this->cfgLazy[$classname];
            }
        }
        return $cfg;
    }

    /**
     * get available config keys for objects
     *
     * @return array
     */
    private function getConfigKeys()
    {
        if (isset($this->configKeys)) {
            return $this->configKeys;
        }
        $this->configKeys = array(
            'debug' => array(
                // any key not found falls under 'debug'...
            ),
            'abstracter' => array(
                'cacheMethods',
                'collectConstants',
                'collectMethods',
                'objectsExclude',
                'objectSort',
                'useDebugInfo',
            ),
            'errorEmailer' => array(
                'emailBacktraceDumper',
                // 'emailFrom',
                // 'emailFunc',
                'emailMask',
                'emailMin',
                'emailThrottledSummary',
                'emailThrottleFile',
                'emailThrottleRead',
                'emailThrottleWrite',
                // 'emailTo',
                'emailTraceMask',
            ),
            'errorHandler' => \array_keys($this->debug->errorHandler->getCfg()),
            'output' => array(
                'addBR',
                'css',
                'displayListKeys',
                'filepathCss',
                'filepathScript',
                'onOutput',
                'outputAs',
                'outputAsDefaultNonHtml',
                'outputConstants',
                'outputCss',
                'outputHeaders',
                'outputMethodDescription',
                'outputMethods',
                'outputScript',
            ),
        );
        return $this->configKeys;
    }

    /**
     * Convert key/path & val to array
     *
     * @param string $key key or path
     * @param mixed  $val value
     *
     * @return array
     */
    private function keyValToArray($key, $val)
    {
        $new = array();
        $path = \preg_split('#[\./]#', $key);
        $ref = &$new;
        foreach ($path as $k) {
            $ref[$k] = array(); // initialize this level
            $ref = &$ref[$k];
        }
        $ref = $val;
        return $new;
    }

    /**
     * Normalizes cfg..  groups values by class
     *
     * converts
     *   array(
     *      'collectMethods' => false,
     *   )
     * to
     *   array(
     *       'abstracter' => array(
     *           'collectMethods' => false,
     *       )
     *   )
     *
     * @param array $cfg config array
     *
     * @return array
     */
    private function normalizeArray($cfg)
    {
        $return = array(
            'debug' => array(),  // initialize with debug... we want debug values first
        );
        $configKeys = $this->getConfigKeys();
        foreach ($cfg as $k => $v) {
            $translated = false;
            foreach ($configKeys as $objName => $objKeys) {
                if ($k == $objName && \is_array($v)) {
                    $return[$objName] = isset($return[$objName])
                        ? \array_merge($return[$objName], $v)
                        : $v;
                    $translated = true;
                    break;
                } elseif (\is_array($v) && isset($configKeys[$k])) {
                    continue;
                } elseif (\in_array($k, $objKeys)) {
                    $return[$objName][$k] = $v;
                    $translated = true;
                    break;
                }
            }
            if (!$translated) {
                $return['debug'][$k] = $v;
            }
        }
        if (!$return['debug']) {
            unset($return['debug']);
        }
        return $return;
    }

    /**
     * Normalize string path
     * Returns either
     *     ''         empty string = all config values grouped by class
     *     '{class}'         we want all config values for class
     *     '{class}/{key}    want specific value from this class'
     *   {class} may be debug
     *
     * @param string $path string path
     *
     * @return string
     */
    private function normalizePath($path)
    {
        $path = \array_filter(\preg_split('#[\./]#', $path), 'strlen');
        if (\count($path) == 0 || $path[0] == '*') {
            return '';
        }
        $configKeys = $this->getConfigKeys();
        $found = false;
        foreach ($configKeys as $objName => $objKeys) {
            if (\in_array($path[0], $objKeys) && $objName !== 'debug') {
                $found = true;
                \array_unshift($path, $objName);
                break;
            }
            if ($path[0] == $objName && !empty($path[1])) {
                $found = true;
                break;
            }
        }
        if (!$found) {
            // we didn't find our key... assume debug
            \array_unshift($path, 'debug');
        }
        if (\end($path) == '*') {
            \array_pop($path);
        }
        return \implode('/', $path);
    }

    /**
     * some config values exist in multiple modules
     *
     * @param array $values values
     *
     * @return array
     */
    private function setCopyValues($values)
    {
        if (isset($values['debug']['emailLog']) && $values['debug']['emailLog'] === true) {
            $values['debug']['emailLog'] = 'onError';
        }
        foreach (array('emailFrom','emailFunc','emailTo') as $key) {
            if (isset($values['debug'][$key]) && !isset($values['errorEmailer'][$key])) {
                // also set for errorEmailer
                $values['errorEmailer'][$key] = $values['debug'][$key];
            }
        }
        return $values;
    }

    /**
     * Set Debug config
     *
     * @param array $cfg Debug config values
     *
     * @return void
     */
    private function setDebugCfg($cfg)
    {
        if (isset($cfg['key'])) {
            $cfg = \array_merge($cfg, $this->debugKeyValues($cfg['key']));
        }
        if (isset($cfg['logEnvInfo']) && \is_bool($cfg['logEnvInfo'])) {
            $keys = \array_keys($this->cfg['logEnvInfo']);
            $cfg['logEnvInfo'] = \array_fill_keys($keys, $cfg['logEnvInfo']);
        }
        if (isset($cfg['logServerKeys'])) {
            // don't append, replace
            $this->cfg['logServerKeys'] = array();
        }
        $this->cfg = $this->debug->utilities->arrayMergeDeep($this->cfg, $cfg);
    }
}


<?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.0
 */

namespace bdk\Debug\Output;

use bdk\PubSub\Event;

/**
 * Output log to file
 */
class File extends Text
{

    protected $file;
    protected $fileHandle;

    /**
     * {@inheritdoc}
     */
    public function getSubscriptions()
    {
        $file = $this->debug->getCfg('file');
        $this->setFile($file);
        return array(
            'debug.config' => 'onConfig',
            'debug.log' => 'onLog',
        );
    }

    /**
     * debug.config event subscriber
     *
     * @param Event $event debug.config event object
     *
     * @return void
     */
    public function onConfig(Event $event)
    {
        $file = $this->debug->getCfg('file');
        $this->setFile($file);
    }

    /**
     * debug.log event subscriber
     *
     * @param Event $event debug.log event object
     *
     * @return void
     */
    public function onLog(Event $event)
    {
        if (!$this->fileHandle) {
            return;
        }
        $method = $event['method'];
        if ($method == 'groupUncollapse') {
            return;
        }
        $args = $event['args'];
        $meta = $event['meta'];
        $isSummaryBookend = $method == 'groupSummary' || !empty($meta['closesSummary']);
        if ($isSummaryBookend) {
            \fwrite($this->fileHandle, "=========\n");
            return;
        }
        if ($args) {
            $str = $this->processLogEntryWEvent($method, $args, $meta);
            \fwrite($this->fileHandle, $str);
        } elseif ($method == 'groupEnd' && $this->depth > 0) {
            $this->depth --;
        }
    }

    /**
     * Set file we will write to
     *
     * @param string $file file path
     *
     * @return void
     */
    protected function setFile($file)
    {
        if ($file == $this->file) {
            // no change
            return;
        }
        if ($this->fileHandle) {
            // close existing file
            \fclose($this->fileHandle);
            $this->fileHandle = null;
        }
        $this->file = $file;
        if (empty($file)) {
            return;
        }
        $fileExists = \file_exists($file);
        $this->fileHandle = \fopen($file, 'a');
        if ($this->fileHandle) {
            \fwrite($this->fileHandle, '***** '.\date('Y-m-d H:i:s').' *****'."\n");
            if (!$fileExists) {
                // we just created file
                \chmod($file, 0660);
            }
        }
    }
}


<?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\Output;

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

/**
 * Base output plugin
 */
abstract class Base implements OutputInterface
{

    public $debug;
    protected $data = array();
    protected $channelName = null;    // should be set by onOutput
    protected $channelNameRoot = null;
    protected $channelRegex;
    protected $isRootInstance = false;
    protected $dumpType;
    protected $dumpTypeMore;
    protected $name = '';

    /**
     * Constructor
     *
     * @param \bdk\Debug $debug debug instance
     */
    public function __construct(Debug $debug)
    {
        $this->debug = $debug;
        if (!$this->name) {
            $name = \get_called_class();
            $idx = \strrpos($name, '\\');
            if ($idx) {
                $name = \substr($name, $idx + 1);
                $name = \lcfirst($name);
            }
            $this->name = $name;
        }
        $this->channelName = $this->debug->getCfg('channel');
        $this->channelNameRoot = $this->debug->rootInstance->getCfg('channel');
        $this->channelRegex = '#^'.\preg_quote($this->channelName, '#').'(\.|$)#';
        $this->isRootInstance = $this->debug->rootInstance === $this->debug;
    }

    /**
     * Magic getter
     *
     * @param string $prop property to get
     *
     * @return mixed
     */
    public function __get($prop)
    {
        $getter = 'get'.\ucfirst($prop);
        if (\method_exists($this, $getter)) {
            return $this->{$getter}();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function dump($val)
    {
        $typeMore = null;
        $type = $this->debug->abstracter->getType($val, $typeMore);
        if ($typeMore == 'raw') {
            $val = $this->debug->abstracter->getAbstraction($val);
            $typeMore = null;
        } elseif ($typeMore == 'abstraction') {
            $typeMore = null;
        }
        $method = 'dump'.\ucfirst($type);
        $return = $this->{$method}($val);
        $this->dumpType = $type;
        $this->dumpTypeMore = $typeMore;
        return $return;
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscriptions()
    {
        return array(
            'debug.output' => 'onOutput',
        );
    }

    /**
     * debug.output subscriber
     *
     * @param Event $event debug.output event object
     *
     * @return void
     */
    abstract public function onOutput(Event $event);

    /**
     * Process log entry without publishing `debug.outputLogEntry` event
     *
     * @param string $method method
     * @param array  $args   args
     * @param array  $meta   meta values
     *
     * @return mixed
     */
    abstract public function processLogEntry($method, $args = array(), $meta = array());

    /**
     * Test channel for inclussion
     *
     * @param string $channel channel name to test against
     *
     * @return boolean
     */
    protected function channelTest($channel)
    {
        return $this->isRootInstance || \preg_match($this->channelRegex, $channel);
    }

    /**
     * Get name property
     *
     * @return string
     */
    protected function getName()
    {
        return $this->name;
    }

    /**
     * Is value a timestamp?
     *
     * @param mixed $val value to check
     *
     * @return string|false
     */
    protected function checkTimestamp($val)
    {
        $secs = 86400 * 90; // 90 days worth o seconds
        $tsNow = \time();
        if ($val > $tsNow - $secs && $val < $tsNow + $secs) {
            return \date('Y-m-d H:i:s', $val);
        }
        return false;
    }

    /**
     * Dump array
     *
     * @param array $array array to dump
     *
     * @return array
     */
    protected function dumpArray($array)
    {
        foreach ($array as $key => $val) {
            $array[$key] = $this->dump($val);
        }
        return $array;
    }

    /**
     * Dump boolean
     *
     * @param boolean $val boolean value
     *
     * @return boolean
     */
    protected function dumpBool($val)
    {
        return (bool) $val;
    }

    /**
     * Dump callable
     *
     * @param array $abs array/callable abstraction
     *
     * @return string
     */
    protected function dumpCallable($abs)
    {
        return 'callable: '.$abs['values'][0].'::'.$abs['values'][1];
    }

    /**
     * Dump float value
     *
     * @param float $val float value
     *
     * @return float|string
     */
    protected function dumpFloat($val)
    {
        $date = $this->checkTimestamp($val);
        return $date
            ? $val.' ('.$date.')'
            : $val;
    }

    /**
     * Dump integer value
     *
     * @param integer $val integer value
     *
     * @return integer|string
     */
    protected function dumpInt($val)
    {
        return $this->dumpFloat($val);
    }

    /**
     * Dump null value
     *
     * @return null
     */
    protected function dumpNull()
    {
        return null;
    }

    /**
     * Dump object
     *
     * @param array $abs object abstraction
     *
     * @return mixed
     */
    protected function dumpObject($abs)
    {
        if ($abs['isRecursion']) {
            $return = '(object) '.$abs['className'].' *RECURSION*';
        } elseif ($abs['isExcluded']) {
            $return = '(object) '.$abs['className'].' (not inspected)';
        } else {
            $return = array(
                '___class_name' => $abs['className'],
            );
            foreach ($abs['properties'] as $name => $info) {
                $vis = (array) $info['visibility'];
                foreach ($vis as $i => $v) {
                    if (\in_array($v, array('magic','magic-read','magic-write'))) {
                        $vis[$i] = '✨ '.$v;    // "sparkles": there is no magic-want unicode char
                    } elseif ($v == 'private' && $info['inheritedFrom']) {
                        $vis[$i] = '🔒 '.$v;
                    }
                }
                if ($info['isExcluded']) {
                    $vis[] = 'excluded';
                }
                $name = '('.\implode(' ', $vis).') '.$name;
                $return[$name] = $this->dump($info['value']);
            }
        }
        return $return;
    }

    /**
     * Dump recursion (array recursion)
     *
     * @return string
     */
    protected function dumpRecursion()
    {
        return 'array *RECURSION*';
    }

    /**
     * Dump resource
     *
     * @param array $abs resource abstraction
     *
     * @return string
     */
    protected function dumpResource($abs)
    {
        return $abs['value'];
    }

    /**
     * Dump string
     *
     * @param string $val string value
     *
     * @return string
     */
    protected function dumpString($val)
    {
        if (\is_numeric($val)) {
            $date = $this->checkTimestamp($val);
            return $date
                ? $val.' ('.$date.')'
                : $val;
        } else {
            return $this->debug->utf8->dump($val);
        }
    }

    /**
     * Dump undefined
     *
     * @return null
     */
    protected function dumpUndefined()
    {
        return null;
    }

    /**
     * Handle alert method
     *
     * @param array $args arguments
     * @param array $meta meta info
     *
     * @return array array($method, $args)
     */
    protected function methodAlert($args, $meta)
    {
        $classToMethod = array(
            'danger' => 'error',
            'info' => 'info',
            'success' => 'info',
            'warning' => 'warn',
        );
        $msg = \str_replace('
', ", \n", $args[0]);
        $method = $meta['class'];
        if (isset($classToMethod[$method])) {
            $method = $classToMethod[$method];
        }
        return array($method, array($msg));
    }

    /**
     * Normalize table data
     *
     * Ensures each row has all key/values and that they're in the same order
     * if any row is an object, each row will get a ___class_name value
     *
     * This builds table rows usable by ChromeLogger, Text, and


<?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.0
 */

namespace bdk\Debug;

/**
 * Validate Utf8 / "highlight" non-utf8, control, & whitespace characters
 */
class Utf8
{
    private static $special = array(
        'regex:#[^\S \r\n\t]#u',        // with /u modifier, \s is equivalent to \p{Z}
        'regex:#[^\P{C}\r\n\t]#u',      // invisible control characters and unused code points. (includes zwsp & BOM)
        // "\xe2\x80\x8b",  // zero-width Space (included in \p{Cf})
        // "\xef\xbb\xbf",  // UTF-8 BOM        (included in \p{Cf})
        "\xef\xbf\xbd",  // "Replacement Character"
    );
    private static $charDesc = array(
        0x00 => 'NUL',
        0x01 => 'SOH',
        0x02 => 'STX',
        0x03 => 'ETX',
        0x04 => 'EOT',
        0x05 => 'ENQ',
        0x06 => 'ACK',
        0x07 => 'BEL',
        0x08 => 'BS',
        0x09 => 'HT',     // not treated special by default
        0x0A => 'LF',     // not treated special by default
        0x0B => 'VT',
        0x0C => 'FF',
        0x0D => 'CR',     // not treated special by default
        0x0E => 'SO',
        0x0F => 'SI',
        0x10 => 'DLE',
        0x11 => 'DC1',
        0x12 => 'DC2',
        0x13 => 'DC3',
        0x14 => 'DC4',
        0x15 => 'NAK',
        0x16 => 'SYN',
        0x17 => 'ETB',
        0x18 => 'CAN',
        0x19 => 'EM',
        0x1A => 'SUB',
        0x1B => 'ESC',
        0x1C => 'FS',
        0x1D => 'GS',
        0x1E => 'RS',
        0x1F => 'US',
        0x7F => 'DEL',
        0xA0 => 'NBSP',
        0x1680 => 'Ogham Space Mark',
        0x2000 => 'En Quad',
        0x2001 => 'Em Quad',
        0x2002 => 'En Space',
        0x2003 => 'Em Space',
        0x2004 => 'Three-Per-Em Space',
        0x2005 => 'Four-Per-Em Space',
        0x2006 => 'Six-Per-Em Space',
        0x2007 => 'Figure Space',
        0x2008 => 'Punctuation Space',
        0x2009 => 'Thin Space',
        0x200A => 'Hair Space',
        0x200B => 'Zero Width Space',   // not included in Separator Category
        0x2028 => 'Line Separator',
        0x2029 => 'Paragraph Separator',
        0x202F => 'Narrow No-Break Space',
        0x205F => 'Medium Mathematical Space',
        0x3000 => 'Ideographic Space',
        0xFEFF => 'BOM / Zero Width No-Break Space', // not included in Separator Category
        0xFFFD => 'Replacement Character',
    );

    private static $useHtml = false;
    private static $curI;
    private static $sanitizeNonBinary = true; // htmlspecialchars non-binary
    private static $stats = array();
    private static $str = '';

    /**
     * Add additional characters to be treated as special chars
     *
     * @param array|string $special character or array of characters ore regular-expressions
     *
     * @return void
     */
    public static function addSpecial($special)
    {
        $special = (array) $special;
        foreach ($special as $char) {
            self::$special[] = $char;
        }
    }

    /**
     * Check UTF-8 string (or single-character) against list of special characters or regular-expressions
     *
     * @param string $str String to check
     *
     * @return boolean
     */
    public static function hasSpecial($str)
    {
        foreach (self::$special as $special) {
            if (\strpos($special, 'regex:') === 0) {
                if (\preg_match(\substr($special, 6), $str) > 0) {
                    return true;
                }
            } elseif (\strpos($str, $special) !== false) {
                return true;
            }
        }
    }

    /**
     * Highlight non-UTF-8, control, & "special" characters
     *
     * control & non-utf-8 chars are displayed as hex
     * "special" unicode-characters are displayed with the \uxxxx representation
     *
     * @param string  $str               string containing binary
     * @param boolean $useHtml           (false) add html markup
     * @param boolean $sanitizeNonBinary (false) apply htmlspecialchars to non-special chars?
     *
     * @return string
     */
    public static function dump($str, $useHtml = false, $sanitizeNonBinary = false)
    {
        self::$useHtml = $useHtml;
        self::$sanitizeNonBinary = $sanitizeNonBinary;
        self::setStr($str);
        $controlCharAs = 'other'; // how should we treat ascii control chars?
        $curBlockType = 'utf8'; // utf8, utf8special, other
        $newBlockType = null;
        $curBlockStart = 0; // string offset
        $strNew = '';
        while (self::$curI < self::$stats['strLen']) {
            $curI = self::$curI;
            $isUtf8 = self::isOffsetUtf8($isSpecial, true);
            if ($isUtf8 && $isSpecial && $controlCharAs !== 'utf8special' && \ord($str[$curI]) < 0x80) {
                if ($controlCharAs == 'other') {
                    $isUtf8 = false;
                } elseif ($controlCharAs == 'utf8') {
                    $isSpecial = false;
                }
            }
            if ($isUtf8) {
                if ($isSpecial) {
                    // control-char or special
                    if ($curBlockType !== 'utf8special') {
                        $newBlockType = 'utf8special';
                    }
                } else {
                    // plain-ol-utf8
                    if ($curBlockType !== 'utf8') {
                        $newBlockType = 'utf8';
                    }
                }
            } else {
                // not a valid utf-8 character
                if ($curBlockType !== 'other') {
                    $newBlockType = 'other';
                }
            }
            if ($newBlockType) {
                $len = $curI - $curBlockStart;
                self::incStat($curBlockType, $len);
                $subStr = \substr(self::$str, $curBlockStart, $len);
                $strNew .= self::dumpBlock($subStr, $curBlockType);
                $curBlockStart = $curI;
                $curBlockType = $newBlockType;
                $newBlockType = null;
            }
        }
        $len = self::$stats['strLen'] - $curBlockStart;
        self::incStat($curBlockType, $len);
        if (self::$stats['strLen']) {
            $percentOther = (self::$stats['bytesOther']) / self::$stats['strLen'] * 100;
            if ($percentOther > 33) {
                $strNew = self::dumpBlock($str, 'other', array('prefix'=>false));
            } else {
                $subStr = \substr(self::$str, $curBlockStart, $len);
                $strNew .= self::dumpBlock($subStr, $curBlockType);
            }
        }
        return $strNew;
    }

    /**
     * Determine if string is UTF-8 encoded
     *
     * In addition, if valid UTF-8, will also report whether string contains
     * control, or other speical characters that could otherwise go unnoticed
     *
     * @param string  $str     string to check
     * @param boolean $special does valid utf-8 string control or "exotic" whitespace type character
     *
     * @return boolean
     */
    public static function isUtf8($str, &$special = false)
    {
        self::setStr($str);
        $special = false;
        while (self::$curI < self::$stats['strLen']) {
            $isUtf8 = self::isOffsetUtf8($isSpecial); // special is only checking control chars
            if (!$isUtf8) {
                return false;
            }
            if ($isSpecial) {
                $special = true;
            }
        }
        $special = $special || self::hasSpecial($str);
        return true;
    }

    /**
     * Returns decimal code-point for multi-byte character
     *
     * Use dechex to convert to hex (ie \uxxxx)
     *
     * @param string  $str    A string or single character
     * @param integer $offset (0) Zero-based offset will be updated for offset of next char
     * @param string  $char   will be populated with the character found at offset
     *
     * @return integer
     */
    public static function ordUtf8($str, &$offset = 0, &$char = null)
    {
        $code = \ord($str[$offset]);
        $numBytes = 1;
        if ($code >= 0x80) {            // otherwise 0xxxxxxx
            if ($code < 0xe0) {         // 110xxxxx
                $numBytes = 2;
                $code -= 0xC0;
            } elseif ($code < 0xf0) {   // 1110xxxx
                $numBytes = 3;
                $code -= 0xE0;
            } elseif ($code < 0xf8) {
                $numBytes = 4;          // 11110xxx
                $code -= 0xF0;
            }
            for ($i = 1; $i < $numBytes; $i++) {
                $code2 = \ord($str[$offset + $i]) - 0x80;        // 10xxxxxx
                $code = $code * 64 + $code2;
            }
        }
        $char = \substr($str, $offset, $numBytes);
        $offset = $offset + $numBytes;
        return $code;
    }

    /**
     * Attempt to convert string to UTF-8 encoding
     *
     * @param string $str string to convert
     *
     * @return string
     */
    public static function toUtf8($str)
    {
        if (\extension_loaded('mbstring') && \function_exists('iconv')) {
            $encoding = \mb_detect_encoding($str, \mb_detect_order(), true);
            if (!$encoding) {
                $str_conv = false;
                if (\function_exists('iconv')) {
                    $str_conv = \iconv('cp1252', 'UTF-8', $str);
                }
                if ($str_conv === false) {
                    $str_conv = \htmlentities($str, ENT_COMPAT);
                    $str_conv = \html_entity_decode($str_conv, ENT_COMPAT, 'UTF-8');
                }
                $str = $str_conv;
            } elseif (!\in_array($encoding, array('ASCII','UTF-8'))) {
                $str_new = \iconv($encoding, 'UTF-8', $str);
                if ($str_new !== false) {
                    $str = $str_new;
                }
            }
        }
        return $str;
    }

    /**
     * Format a block of text
     *
     * @param string $str       string to output
     * @param string $blockType "utf8", "utf8special", or "other"
     * @param array  $options   options
     *
     * @return [type] [description]
     */
    private static function dumpBlock($str, $blockType, $options = array())
    {
        if ($str === '') {
            return '';
        }
        $options = \array_merge(array(
            'prefix' => true,
        ), $options);
        if ($blockType == 'utf8' && self::$sanitizeNonBinary) {
            $str = \htmlspecialchars($str);
        } elseif ($blockType == 'utf8special') {
            $strNew = '';
            $i = 0;
            $length = \strlen($str);
            while ($i < $length) {
                $ord = self::ordUtf8($str, $i, $char);
                $ordHex = \dechex($ord);
                $ordHex = \str_pad($ordHex, 4, "0", STR_PAD_LEFT);
                if (self::$useHtml) {
                    $chars = \str_split($char);
                    $utf8Hex = \array_map('bin2hex', $chars);
                    $utf8Hex = '\x'.\implode(' \x', $utf8Hex);
                    $title = $utf8Hex;
                    if (isset(self::$charDesc[$ord])) {
                        $title = self::$charDesc[$ord].': '.$utf8Hex;
                    }
                    $strNew = '\u'.$ordHex.'';
                } else {
                    $strNew .= '\u{'.$ordHex.'}';
                }
            }
            $str = $strNew;
        } elseif ($blockType == 'other') {
            if (!$options['prefix']) {
                $str = \bin2hex($str);
                $str = \trim(\chunk_split($str, 2, ' '));
            } else {
                $chars = \str_split($str);
                foreach ($chars as $i => $char) {
                    $ord = \ord($char);
                    $hex = \bin2hex($char); // could use dechex($ord), but would require padding
                    if (self::$useHtml && isset(self::$charDesc[$ord])) {
                        if ($ord < 0x20 || $ord == 0x7f) {
                            // lets use the control pictures
                            $chr = $ord == 0x7f
                                ? "\xe2\x90\xa1"            // "del" char
                                : "\xe2\x90".\chr($ord+128); // chars for 0x00 - 0x1F
                            $chars[$i] = ''.$chr.'';
                        } else {
                            $chars[$i] = '\x'.$hex.'';
                        }
                    } else {
                        $chars[$i] = '\x'.$hex;
                    }
                }
                $str = \implode(' ', $chars);
            }
            if (self::$useHtml) {
                $str = ''.$str.'';
            }
        }
        return $str;
    }

    /**
     * Increment statistic
     *
     * @param string  $stat stat to increment
     * @param integer $inc  increment ammount
     *
     * @return void
     */
    private static function incStat($stat, $inc)
    {
        if ($stat == 'utf8special') {
            $stat = 'bytesSpecial';
        } else {
            $stat = 'bytes'.\ucfirst($stat);
        }
        self::$stats[$stat] += $inc;
    }

    /**
     * Is the byte or byte-sequence beginning at the current offset a valid utf-8 character?
     *
     * Increments the current offset
     *
     * @param boolean $special      populated with whether offset is a control or "special" character
     * @param boolean $checkSpecial test for user-defined special chars?
     *
     * @return boolean [description]
     */
    private static function isOffsetUtf8(&$special = false, $checkSpecial = false)
    {
        $i = self::$curI;
        $special = false;
        $byte1 = \ord(self::$str[$i]);
        $byte2 = $i + 1 < self::$stats['strLen'] ? \ord(self::$str[$i+1]) : null;
        $byte3 = $i + 2 < self::$stats['strLen'] ? \ord(self::$str[$i+2]) : null;
        $byte4 = $i + 3 < self::$stats['strLen'] ? \ord(self::$str[$i+3]) : null;
        if ($byte1 < 0x80) {                 # 0bbbbbbb
            if (($byte1 < 0x20 || $byte1 == 0x7f) && !\in_array($byte1, array(0x09,0x0a,0x0d))) {
                $special = true;
            }
            self::$curI += 1;    // advance to next byte
        } elseif (($byte1 & 0xe0) == 0xc0) { # 110bbbbb 10bbbbbb
            if ($i + 1 >= self::$stats['strLen'] ||
                ($byte2 & 0xc0) !== 0x80 ||
                ($byte1 & 0xfe) === 0xc0  // overlong
            ) {
                self::$curI += 1;
                return false;
            }
            self::$curI += 2;    // skip the next byte
        } elseif (($byte1 & 0xf0) == 0xe0) { # 3-byte sequence 1110bbbb 10bbbbbb 10bbbbbb
            if ($i + 2 >= self::$stats['strLen'] ||
                ($byte2 & 0xc0) !== 0x80 ||
                ($byte3 & 0xc0) !== 0x80 ||
                $byte1 === 0xe0 && ($byte2 & 0xe0) === 0x80 ||  // overlong
                $byte1 === 0xed && ($byte2 & 0xe0) === 0xa0     // UTF-16 surrogate (U+D800 - U+DFFF)
            ) {
                self::$curI += 1;
                return false;
            }
            self::$curI += 3;    // skip the next 2 bytes
        } elseif (($byte1 & 0xf8) == 0xf0) { # 4-byte sequence: 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
            if ($i + 3 >= self::$stats['strLen'] ||
                ($byte2 & 0xc0) !== 0x80 ||
                ($byte3 & 0xc0) !== 0x80 ||
                ($byte4 & 0xc0) !== 0x80 ||
                $byte1 === 0xf0 && ($byte2 & 0xf0) === 0x80 ||      // overlong
                $byte1 === 0xf4 && $byte2 > 0x8f || $byte1 > 0xf4   // > U+10FFFF
            ) {
                self::$curI += 1;
                return false;
            }
            self::$curI += 4;    // skip the next 3 bytes
        } else {                            # Does not match any model
            self::$curI += 1;
            return false;
        }
        if ($checkSpecial) {
            $subStr = \substr(self::$str, $i, self::$curI-$i);
            $special = $special || self::hasSpecial($subStr);
        }
        return true;
    }

    /**
     * Reset string statistics
     *
     * @param string $str string being inspected/output
     *
     * @return void
     */
    private static function setStr($str)
    {
        self::$str = $str;
        self::$curI = 0;
        self::$stats = array(
            'bytesOther' => 0,
            'bytesSpecial' => 0,        // special UTF-8
            'bytesUtf8' => 0,           // includes ASCII
            'strLen' => \strlen($str),
        );
    }
}


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

namespace bdk\ErrorHandler;

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

/**
 * Email error details on error
 *
 * Emails an error report on error and throttles said email so does not excessively send email
 */
class ErrorEmailer implements SubscriberInterface
{

    protected $cfg = array();
    protected $throttleData = array();
    protected $errTypes = array();  // populated onError

    /**
     * Constructor
     *
     * @param array $cfg config
     */
    public function __construct($cfg = array())
    {
        $this->cfg = array(
            'emailBacktraceDumper' => null, // callable that receives backtrace array & returns string
            'emailFrom' => null,            // null = use php's default (php.ini: sendmail_from)
            'emailFunc' => 'mail',
            'emailMask' => E_ERROR | E_PARSE | E_COMPILE_ERROR | E_WARNING | E_USER_ERROR | E_USER_NOTICE,
            'emailMin' => 15,               // 0 = no throttle
            'emailThrottledSummary' => true,    // if errors have been throttled, should we email a summary email of throttled errors?
                                                //    (first occurance of error is never throttled)
            'emailThrottleFile' => __DIR__.'/error_emails.json',
            'emailThrottleRead' => null,    // callable that returns throttle data
            'emailThrottleWrite' => null,   // callable that writes throttle data.  receives single array param
            'emailTo' => !empty($_SERVER['SERVER_ADMIN'])
                ? $_SERVER['SERVER_ADMIN']
                : null,
            'emailTraceMask' => E_ERROR | E_WARNING | E_USER_ERROR | E_USER_NOTICE,
        );
        $this->setCfg($cfg);
        return;
    }

    /**
     * Retrieve a configuration value
     *
     * @param string $key what to get
     *
     * @return mixed
     */
    public function getCfg($key = null)
    {
        if (!\strlen($key)) {
            return $this->cfg;
        }
        if (isset($this->cfg[$key])) {
            return $this->cfg[$key];
        }
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscriptions()
    {
        return array(
            'errorHandler.error' => array('onErrorHighPri', PHP_INT_MAX),
            'errorHandler.error' => array('onErrorLowPri', PHP_INT_MAX * -1),
        );
    }

    /**
     * load throttle stats for passed error
     *
     * @param Event $error error event
     *
     * @return void
     */
    public function onErrorHighPri(Event $error)
    {
        $this->throttleDataRead();
        $hash = $error['hash'];
        $error['email'] = ($error['type'] & $this->cfg['emailMask'])
            && $error['isFirstOccur']
            && $this->cfg['emailTo'];
        $error['stats'] = array(
            'tsEmailed'  => 0,
            'countSince' => 0,
            'emailedTo'  => '',
        );
        if (isset($this->throttleData['errors'][$hash])) {
            $stats = \array_intersect_key($this->throttleData['errors'][$hash], $error['stats']);
            $error['stats'] = \array_merge($error['stats'], $stats);
        }
        return;
    }

    /**
     * Email error
     *
     * @param Event $error error event
     *
     * @return void
     */
    public function onErrorLowPri(Event $error)
    {
        if ($error['email'] && $this->cfg['emailMin'] > 0) {
            $throttleSuccess = $this->throttleDataSet($error);
            $tsCutoff = \time() - $this->cfg['emailMin'] * 60;
            $error['email'] = $throttleSuccess && $error['stats']['tsEmailed'] <= $tsCutoff;
        }
        if ($error['email']) {
            $this->emailErr($error);
        }
    }

    /**
     * Set one or more config values
     *
     * If setting a single value via method a or b, old value is returned
     *
     *    setCfg('key', 'value')
     *    setCfg(array('k1'=>'v1', 'k2'=>'v2'))
     *
     * @param string $mixed  key=>value array or key
     * @param mixed  $newVal value
     *
     * @return mixed
     */
    public function setCfg($mixed, $newVal = null)
    {
        $ret = null;
        if (\is_string($mixed)) {
            $ret = isset($this->cfg[$mixed])
                ? $this->cfg[$mixed]
                : null;
            $this->cfg[$mixed] = $newVal;
        } elseif (\is_array($mixed)) {
            $ret = \array_intersect_key($this->cfg, $mixed);
            $this->cfg = \array_merge($this->cfg, $mixed);
        }
        return $ret;
    }

    /**
     * Clear throttle data
     *
     * @return void
     */
    public function throttleDataClear()
    {
        $this->throttleData = array(
            'tsGarbageCollection' => \time(),
            'errors' => array(),
        );
        $this->throttleDataWrite();
    }

    /**
     * Get formatted backtrace string for error
     *
     * @param Event $error error event
     *
     * @return string
     */
    protected function backtraceStr(Event $error)
    {
        $backtrace = $error['backtrace']
            ? $error['backtrace'] // backtrace provided
            : $error->getSubject()->backtrace();
        if (\count($backtrace) < 2) {
            return '';
        }
        if ($backtrace && $error['vars']) {
            $backtrace[0]['vars'] = $error['vars'];
        }
        if ($this->cfg['emailBacktraceDumper']) {
            $str = \call_user_func($this->cfg['emailBacktraceDumper'], $backtrace);
        } else {
            $search = array(
                ")\n\n",
            );
            $replace = array(
                ")\n",
            );
            $str = \print_r($backtrace, true);
            $str = \preg_replace('#\bArray\n\(#', 'array(', $str);
            $str = \preg_replace('/\barray\s+\(\s+\)/s', 'array()', $str); // single-lineify empty arrays
            $str = \str_replace($search, $replace, $str);
            $str = \substr($str, 0, -1);
        }
        return $str;
    }

    /**
     * Send an email
     *
     * @param string $toAddr  To
     * @param string $subject Subject
     * @param string $body    Body
     *
     * @return void
     */
    protected function email($toAddr, $subject, $body)
    {
        $addHeadersStr = '';
        $fromAddr = $this->cfg['emailFrom'];
        if ($fromAddr) {
            $addHeadersStr .= 'From: '.$fromAddr;
        }
        \call_user_func($this->cfg['emailFunc'], $toAddr, $subject, $body, $addHeadersStr);
    }

    /**
     * Email this error
     *
     * @param Event $error error event
     *
     * @return void
     */
    protected function emailErr(Event $error)
    {
        $dateTimeFmt = 'Y-m-d H:i:s (T)';
        $errMsg = $error['message'];
        if ($error['isHtml']) {
            $errMsg = \strip_tags($errMsg);
            $errMsg = \htmlspecialchars_decode($errMsg);
        }
        $countSince = $error['stats']['countSince'];
        $isCli = $this->isCli();
        $subject = $isCli
            ? 'Error: '.\implode(' ', $_SERVER['argv'])
            : 'Website Error: '.$_SERVER['SERVER_NAME'];
        $subject .= ': '.$errMsg.($countSince ? ' ('.$countSince.'x)' : '');
        $emailBody = '';
        if (!empty($countSince)) {
            $dateTimePrev = \date($dateTimeFmt, $error['stats']['tsEmailed']);
            $emailBody .= 'Error has occurred '.$countSince.' times since last email ('.$dateTimePrev.').'."\n\n";
        }
        $emailBody .= ''
            .'datetime: '.\date($dateTimeFmt)."\n"
            .'errormsg: '.$errMsg."\n"
            .'errortype: '.$error['type'].' ('.$error['typeStr'].')'."\n"
            .'file: '.$error['file']."\n"
            .'line: '.$error['line']."\n"
            .'';
        if (!$isCli) {
            $emailBody .= ''
                .'remote_addr: '.$_SERVER['REMOTE_ADDR']."\n"
                .'http_host: '.$_SERVER['HTTP_HOST']."\n"
                .'referer: '.(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : 'null')."\n"
                .'request_uri: '.$_SERVER['REQUEST_URI']."\n"
                .'';
        }
        if (!empty($_POST)) {
            $emailBody .= 'post params: '.\var_export($_POST, true)."\n";
        }
        if ($error['type'] & $this->cfg['emailTraceMask']) {
            $backtraceStr = $this->backtraceStr($error);
            $emailBody .= "\n".($backtraceStr
                ? 'backtrace: '.$backtraceStr
                : 'no backtrace');
        }
        $this->email($this->cfg['emailTo'], $subject, $emailBody);
        return;
    }

    /**
     * Write string to file / creates file if doesn't exist
     *
     * @param string $file filepath
     * @param string $str  string to write
     *
     * @return integer|boolean number of bytes written or false on error
     */
    protected function fileWrite($file, $str)
    {
        $return = false;
        $dir = \dirname($file);
        if (!\file_exists($dir)) {
            \mkdir($dir, 0755, true);
        }
        if (\is_writable($file) || !\file_exists($file) && \is_writeable($dir)) {
            $return = \file_put_contents($file, $str);
        }
        return $return;
    }

    /**
     * Is script running from command line (or cron)?
     *
     * @return boolean
     */
    protected static function isCli()
    {
        return \defined('STDIN') || isset($_SERVER['argv']) || !\array_key_exists('REQUEST_METHOD', $_SERVER);
    }

    /**
     * Remove errors in throttleData that haven't occured recently
     * If error(s) have occured since they were last emailed, a summary email will be sent
     *
     * @return void
     */
    protected function throttleDataGarbageCollection()
    {
        $tsNow     = \time();
        $tsCutoff  = $tsNow - $this->cfg['emailMin'] * 60;
        if ($this->throttleData['tsGarbageCollection'] > $tsCutoff) {
            // we've recently performed garbage collection
            return;
        }
        // garbage collection time
        $emailBody = '';
        $sendEmailSummary = false;
        $this->throttleData['tsGarbageCollection'] = $tsNow;
        foreach ($this->throttleData['errors'] as $k => $err) {
            if ($err['tsEmailed'] > $tsCutoff) {
                continue;
            }
            // it's been a while since this error was emailed
            if ($err['emailedTo'] != $this->cfg['emailTo']) {
                // it was emailed to a different address
                if ($err['countSince'] < 1 || $err['tsEmailed'] < $tsNow - 60*60*24) {
                    unset($this->throttleData['errors'][$k]);
                }
                continue;
            }
            unset($this->throttleData['errors'][$k]);
            if ($err['countSince'] > 0) {
                $dateLastEmailed = \date('Y-m-d H:i:s', $err['tsEmailed']);
                $emailBody .= ''
                    .'File: '.$err['file']."\n"
                    .'Line: '.$err['line']."\n"
                    .'Error: '.$this->errTypes[ $err['errType'] ].': '.$err['errMsg']."\n"
                    .'Has occured '.$err['countSince'].' times since '.$dateLastEmailed."\n\n";
                $sendEmailSummary = $this->cfg['emailThrottledSummary'];
            }
        }
        if ($sendEmailSummary) {
            $this->email($this->cfg['emailTo'], 'Website Errors: '.$_SERVER['SERVER_NAME'], $emailBody);
        }
    }

    /**
     * Load & populate $this->throttleData if not alrady imported
     *
     * Uses cfg[emailThrottleRead] callable if set, otherwise, reads from cfg['emailThrottleFile']
     *
     * @return void
     */
    protected function throttleDataRead()
    {
        if ($this->throttleData) {
            // already imported
            return;
        }
        $throttleData = array();
        if ($this->cfg['emailThrottleRead'] && \is_callable($this->cfg['emailThrottleRead'])) {
            $throttleData = \call_user_func($this->cfg['emailThrottleRead']);
        } elseif ($this->cfg['emailThrottleFile'] && \is_readable($this->cfg['emailThrottleFile'])) {
            $throttleData = \file_get_contents($this->cfg['emailThrottleFile']);
            $throttleData = \json_decode($throttleData, true);
        }
        if (!\is_array($throttleData)) {
            $throttleData = array();
        }
        $this->throttleData = \array_merge(array(
            'tsGarbageCollection' => \time(),
            'errors' => array(),
        ), $throttleData);
        return;
    }

    /**
     * Adds/Updates this error's throttle data
     *
     * @param Event $error error event
     *
     * @return boolean
     */
    protected function throttleDataSet(Event $error)
    {
        $tsNow = \time();
        $hash = $error['hash'];
        $tsCutoff = $tsNow - $this->cfg['emailMin'] * 60;
        if ($error['stats']['tsEmailed'] > $tsCutoff) {
            // This error was recently emailed
            $this->throttleData['errors'][$hash]['countSince']++;
        } else {
            // hasn't been emailed recently
            $this->throttleData['errors'][$hash] = array(
                'file'       => $error['file'],
                'line'       => $error['line'],
                'errType'    => $error['type'],
                'errMsg'     => $error['message'],
                'tsEmailed'  => $tsNow,
                'emailedTo'  => $this->cfg['emailTo'],
                'countSince' => 0,
            );
        }
        if (empty($this->errTypes)) {
            $this->errTypes = $error->getSubject()->get('errTypes');
        }
        return $this->throttleDataWrite();
    }

    /**
     * Export/Save/Write throttle data
     *
     * Uses cfg[emailThrottleWrite] callable if set, otherwise, writes to cfg['emailThrottleFile']
     *
     * @return boolean
     */
    protected function throttleDataWrite()
    {
        $return = true;
        $this->throttleDataGarbageCollection();
        if ($this->cfg['emailThrottleWrite'] && \is_callable($this->cfg['emailThrottleWrite'])) {
            $return = \call_user_func($this->cfg[''], $this->throttleData);
            if (!$return) {
                \error_log('ErrorEmailer: emailThrottleWrite() returned false');
            }
        } elseif ($this->cfg['emailThrottleFile']) {
            $wrote = $this->fileWrite($this->cfg['emailThrottleFile'], \json_encode($this->throttleData, JSON_PRETTY_PRINT));
            if (!$wrote) {
                $return = false;
                \error_log('Unable to write to '.$this->cfg['emailThrottleFile']);
            }
        }
        return $return;
    }
}


<?php
/**
 * This file is part of bdk\PubSub
 *
 * @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;

use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;

/**
 * Event
 *
 * Events are passed to event subscribres/listeners
 */
class Event implements ArrayAccess, IteratorAggregate
{
    /**
     * @var boolean Whether event subscribers should be called
     */
    private $propagationStopped = false;

    /**
     * @var mixed Event subject: usually object or callable
     */
    protected $subject = null;

    /**
     * @var array Array of key/values
     */
    protected $values = array();

    /**
     * Construct an event with optional subject and values
     *
     * @param mixed $subject The subject of the event, usually an object
     * @param array $values  Values to store in the event
     */
    public function __construct($subject = null, array $values = array())
    {
        $this->subject = $subject;
        $this->values = $values;
    }

    /**
     * Magic Method
     *
     * @return array
     */
    public function __debugInfo()
    {
        return array(
            'propagationStopped' => $this->propagationStopped,
            'subject' => \is_object($this->subject)
                ? \get_class($this->subject)
                : $this->subject,
            'values' => $this->values,
        );
    }

    /**
     * Getter for subject property.
     *
     * @return mixed $subject The observer subject
     */
    public function getSubject()
    {
        return $this->subject;
    }

    /**
     * Get value by key.
     *
     * @param string $key Value name
     *
     * @return mixed
     */
    public function getValue($key)
    {
        return $this->offsetGet($key);
    }

    /**
     * Get all stored values
     *
     * @return array
     */
    public function getValues()
    {
        return $this->values;
    }

    /**
     * Has value.
     *
     * @param string $key Value name
     *
     * @return boolean
     */
    public function hasValue($key)
    {
        return \array_key_exists($key, $this->values);
    }

    /**
     * Returns whether further event subscribers should be called
     *
     * @see Event::stopPropagation()
     *
     * @return boolean Whether propagation is stopped for this event
     */
    public function isPropagationStopped()
    {
        return $this->propagationStopped;
    }

    /**
     * Set event value
     *
     * @param string $key   Value name
     * @param mixed  $value Value
     *
     * @return $this
     */
    public function setValue($key, $value)
    {
        $this->values[$key] = $value;
        return $this;
    }

    /**
     * Clears existing values and sets new values
     *
     * @param array $values values
     *
     * @return $this
     */
    public function setValues(array $values = array())
    {
        $this->values = $values;
        return $this;
    }

    /**
     * Stops the propagation of the event
     *
     * No further event subscribers will be called after stopPropagation is called
     *
     * @return void
     */
    public function stopPropagation()
    {
        $this->propagationStopped = true;
    }

    /**
     * ArrayAccess hasValue.
     *
     * @param string $key Array key
     *
     * @return boolean
     */
    public function offsetExists($key)
    {
        return $this->hasValue($key);
    }

    /**
     * ArrayAccess getValue.
     *
     * @param string $key Array key
     *
     * @return mixed
     */
    public function &offsetGet($key)
    {
        if ($this->hasValue($key)) {
            return $this->values[$key];
        }
        $null = null;
        return $null;
    }

    /**
     * ArrayAccess setValue
     *
     * @param string $key   Array key to set
     * @param mixed  $value Value
     *
     * @return void
     */
    public function offsetSet($key, $value)
    {
        $this->setValue($key, $value);
    }

    /**
     * ArrayAccess interface
     *
     * @param string $key Array key
     *
     * @return void
     */
    public function offsetUnset($key)
    {
        unset($this->values[$key]);
    }

    /**
     * IteratorAggregate interface
     *
     * Iterate over the object like an array.
     *
     * @return ArrayIterator
     */
    public function getIterator()
    {
        return new ArrayIterator($this->values);
    }
}