tomaj/nette-errbit

View on GitHub
src/errbit-php/Errbit/Notice.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php

/**
 * Errbit PHP Notifier.
 *
 * Copyright © Flippa.com Pty. Ltd.
 * See the LICENSE file for details.
 */

/**
 * Builds a complete payload for the notice sent to Errbit.
 */
class Errbit_Notice {
    private $_exception;
    private $_options;

    /**
     * Create a new notice for the given Exception with the given $options.
     *
     * @param [Exception] $exception
     *   the exception that occurred
     *
     * @param [Array] $options
     *   full configuration + options
     */
    public function __construct($exception, $options = array()) {
        $this->_exception = $exception;
        $this->_options   = array_merge(
            array(
                'url'          => $this->_buildRequestUrl(),
                'parameters'   => !empty($_REQUEST) ? $_REQUEST : array(),
                'session_data' => !empty($_SESSION) ? $_SESSION : array(),
                'cgi_data'     => !empty($_SERVER)  ? $_SERVER  : array()
            ),
            $options
        );

        $this->_filterData();
    }

    /**
     * Convenience method to instantiate a new notice.
     */
    public static function forException($exception, $options = array()) {
        return new self($exception, $options);
    }

    /**
     * Extract a human-readable method/function name from the given stack frame.
     *
     * @param [Array] $frame
     *   a single entry for the backtrace
     *
     * @return [String]
     *   the name of the method/function
     */
    public static function formatMethod($frame) {
        if (!empty($frame['class']) && !empty($frame['type']) && !empty($frame['function'])) {
            return sprintf('%s%s%s()', $frame['class'], $frame['type'], $frame['function']);
        } else {
            return sprintf('%s()', !empty($frame['function']) ? $frame['function'] : '<unknown>');
        }
    }

    /**
     * Get a human readable class name for the Exception.
     *
     * Native PHP errors are named accordingly.
     *
     * @param [Exception] $exception
     *   the exception object
     *
     * @return [String]
     *   the name to display
     */
    public static function className($exception) {
        $name = get_class($exception);

        switch ($name) {
            case 'Errbit_Errors_Notice':
                return 'Notice';
            case 'Errbit_Errors_Warning':
                return 'Warning';
            case 'Errbit_Errors_Error':
                return 'Error';
            case 'Errbit_Errors_Fatal':
                return 'Fatal Error';
            default:
                return $name;
        }
    }

    /**
     * Recursively build an list of the all the vars in the given array.
     *
     * @param [XmlBuilder] $builder
     *   the builder instance to set the data into
     *
     * @param [Array] $array
     *   the stack frame entry
     */
    public static function xmlVarsFor($builder, $array) {
        foreach ($array as $key => $value) {
            if (is_object($value)) {
                $value = (array)$value;
            }

            if (is_array($value)) {
                $builder->tag('var', array('key' => $key), function($var) use ($value) {
                    Errbit_Notice::xmlVarsFor($var, $value);
                });
            } else {
                $builder->tag('var', $value, array('key' => $key));
            }
        }
    }

    /**
     * Perform search/replace filters on a backtrace entry.
     *
     * @param [String] $str
     *   the entry from the backtrace
     *
     * @return [String]
     *   the filtered entry
     */
    public function filterTrace($str) {
        if (empty($this->_options['backtrace_filters'])) {
            return $str;
        }

        foreach ($this->_options['backtrace_filters'] as $pattern => $replacement) {
            $str = preg_replace($pattern, $replacement, $str);
        }

        return $str;
    }

    /**
     * Build the full XML document for the notice.
     *
     * @return [String]
     *   the XML
     */
    public function asXml() {
        $exception = $this->_exception;
        $options   = $this->_options;
        $builder   = new Errbit_XmlBuilder();
        $self      = $this;

        return $builder->tag(
            'notice',
            array('version' => Errbit::API_VERSION),
            function($notice) use ($exception, $options, $self) {
                $notice->tag('api-key',  $options['api_key']);
                $notice->tag('notifier', function($notifier) {
                    $notifier->tag('name',    Errbit::PROJECT_NAME);
                    $notifier->tag('version', Errbit::VERSION);
                    $notifier->tag('url',     Errbit::PROJECT_URL);
                });

                $notice->tag('error', function($error) use ($exception, $self) {
                    $klass = Errbit_Notice::className($exception);
                    $error->tag('class',     $self->filterTrace($klass));
                    $error->tag('message',   $self->filterTrace(sprintf('%s: %s', $klass, $exception->getMessage())));
                    $error->tag('backtrace', function($backtrace) use ($exception, $self) {
                        $trace = $exception->getTrace();

                        $file1 = $exception->getFile();
                        $backtrace->tag('line', array(
                            'number' => $exception->getLine(),
                            'file' => !empty($file1) ? $self->filterTrace($file1) : '<unknown>',
                            'method' =>  "<unknown>"
                        ));

                        // if there is no trace we should add an empty element
                        if (empty($trace)) {
                            $backtrace->tag(
                                'line',
                                array(
                                     'number' => '',
                                     'file' => '',
                                     'method' => ''
                                )
                            );
                        } else {
                            foreach ($trace as $frame) {
                                $backtrace->tag(
                                    'line',
                                    array(
                                        'number' => isset($frame['line']) ? $frame['line'] : 0,
                                        'file'   => isset($frame['file']) ? $self->filterTrace($frame['file']) : '<unknown>',
                                        'method' => $self->filterTrace(Errbit_Notice::formatMethod($frame))
                                    )
                                );
                            }
                        }
                    });
                });

                if (!empty($options['url'])
                    || !empty($options['controller'])
                    || !empty($options['action'])
                    || !empty($options['parameters'])
                    || !empty($options['session_data'])
                    || !empty($options['cgi_data'])) {
                    $notice->tag('request', function($request) use ($options) {
                        $request->tag('url',       !empty($options['url']) ? $options['url'] : '');
                        $request->tag('component', !empty($options['controller']) ? $options['controller'] : '');
                        $request->tag('action',    !empty($options['action']) ? $options['action'] : '');
                        if (!empty($options['parameters'])) {
                            $request->tag('params', function($params) use ($options) {
                                Errbit_Notice::xmlVarsFor($params, $options['parameters']);
                            });
                        }

                        if (!empty($options['session_data'])) {
                            $request->tag('session', function($session) use ($options) {
                                Errbit_Notice::xmlVarsFor($session, $options['session_data']);
                            });
                        }

                        if (!empty($options['cgi_data'])) {
                            $request->tag('cgi-data', function($cgiData) use ($options) {
                                Errbit_Notice::xmlVarsFor($cgiData, $options['cgi_data']);
                            });
                        }
                    });
                }

                $notice->tag('server-environment', function($env) use ($options) {
                    $env->tag('project-root',     $options['project_root']);
                    $env->tag('environment-name', $options['environment_name']);
                    $env->tag('hostname',         $options['hostname']);
                });
            }
        )->asXml();
    }

    // -- Private Methods

    private function _filterData() {
        if (empty($this->_options['params_filters'])) {
            return;
        }

        foreach (array('parameters', 'session_data', 'cgi_data') as $name) {
            $this->_filterParams($name);
        }
    }

    private function _filterParams($name) {
        if (empty($this->_options[$name])) {
            return;
        }

        foreach ($this->_options['params_filters'] as $pattern) {
            foreach ($this->_options[$name] as $key => $value) {
                if (preg_match($pattern, $key)) {
                    $this->_options[$name][$key] = '[FILTERED]';
                }
            }
        }
    }

    private function _buildRequestUrl() {
        if (!empty($_SERVER['REQUEST_URI'])) {
            return sprintf(
                '%s://%s%s%s',
                $this->_guessProtocol(),
                $this->_guessHost(),
                $this->_guessPort(),
                $_SERVER['REQUEST_URI']
            );
        }
    }

    private function _guessProtocol() {
        if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
            return $_SERVER['HTTP_X_FORWARDED_PROTO'];
        } elseif (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) {
            return 'https';
        } else {
            return 'http';
        }
    }

    private function _guessHost() {
        if (!empty($_SERVER['HTTP_HOST'])) {
            return $_SERVER['HTTP_HOST'];
        } elseif (!empty($_SERVER['SERVER_NAME'])) {
            return $_SERVER['SERVER_NAME'];
        } else {
            return '127.0.0.1';
        }
    }

    private function _guessPort() {
        if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], array(80, 443))) {
            return sprintf(':%d', $_SERVER['SERVER_PORT']);
        }
    }
}