PageRenderTime 232ms CodeModel.GetById 198ms app.highlight 27ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/Raven/Client.php

http://github.com/getsentry/raven-php
PHP | 967 lines | 660 code | 119 blank | 188 comment | 114 complexity | c4195e0f8f19c8aaff3c97175d052e84 MD5 | raw file
  1<?php
  2/*
  3 * This file is part of Raven.
  4 *
  5 * (c) Sentry Team
  6 *
  7 * For the full copyright and license information, please view the LICENSE
  8 * file that was distributed with this source code.
  9 */
 10
 11/**
 12 * Raven PHP Client
 13 *
 14 * @package raven
 15 */
 16
 17class Raven_Client
 18{
 19    const VERSION = '0.16.0.dev0';
 20    const PROTOCOL = '6';
 21
 22    const DEBUG = 'debug';
 23    const INFO = 'info';
 24    const WARN = 'warning';
 25    const WARNING = 'warning';
 26    const ERROR = 'error';
 27    const FATAL = 'fatal';
 28
 29    const MESSAGE_LIMIT = 1024;
 30
 31    public $severity_map;
 32    public $extra_data;
 33
 34    public $store_errors_for_bulk_send = false;
 35
 36    public function __construct($options_or_dsn=null, $options=array())
 37    {
 38        if (is_array($options_or_dsn)) {
 39            $options = array_merge($options_or_dsn, $options);
 40        }
 41
 42        if (!is_array($options_or_dsn) && !empty($options_or_dsn)) {
 43            $dsn = $options_or_dsn;
 44        } elseif (!empty($_SERVER['SENTRY_DSN'])) {
 45            $dsn = @$_SERVER['SENTRY_DSN'];
 46        } elseif (!empty($options['dsn'])) {
 47            $dsn = $options['dsn'];
 48        } else {
 49            $dsn = null;
 50        }
 51
 52        if (!empty($dsn)) {
 53            $options = array_merge($options, self::parseDSN($dsn));
 54        }
 55
 56        $this->logger = Raven_Util::get($options, 'logger', 'php');
 57        $this->server = Raven_Util::get($options, 'server');
 58        $this->secret_key = Raven_Util::get($options, 'secret_key');
 59        $this->public_key = Raven_Util::get($options, 'public_key');
 60        $this->project = Raven_Util::get($options, 'project', 1);
 61        $this->auto_log_stacks = (bool) Raven_Util::get($options, 'auto_log_stacks', false);
 62        $this->name = Raven_Util::get($options, 'name', Raven_Compat::gethostname());
 63        $this->site = Raven_Util::get($options, 'site', $this->_server_variable('SERVER_NAME'));
 64        $this->tags = Raven_Util::get($options, 'tags', array());
 65        $this->release = Raven_Util::get($options, 'release', null);
 66        $this->environment = Raven_Util::get($options, 'environment', null);
 67        $this->trace = (bool) Raven_Util::get($options, 'trace', true);
 68        $this->timeout = Raven_Util::get($options, 'timeout', 2);
 69        $this->message_limit = Raven_Util::get($options, 'message_limit', self::MESSAGE_LIMIT);
 70        $this->exclude = Raven_Util::get($options, 'exclude', array());
 71        $this->severity_map = null;
 72        $this->shift_vars = (bool) Raven_Util::get($options, 'shift_vars', true);
 73        $this->http_proxy = Raven_Util::get($options, 'http_proxy');
 74        $this->extra_data = Raven_Util::get($options, 'extra', array());
 75        $this->send_callback = Raven_Util::get($options, 'send_callback', null);
 76        $this->curl_method = Raven_Util::get($options, 'curl_method', 'sync');
 77        $this->curl_path = Raven_Util::get($options, 'curl_path', 'curl');
 78        $this->curl_ipv4 = Raven_util::get($options, 'curl_ipv4', true);
 79        $this->ca_cert = Raven_Util::get($options, 'ca_cert', $this->get_default_ca_cert());
 80        $this->verify_ssl = Raven_Util::get($options, 'verify_ssl', true);
 81        $this->curl_ssl_version = Raven_Util::get($options, 'curl_ssl_version');
 82        $this->trust_x_forwarded_proto = Raven_Util::get($options, 'trust_x_forwarded_proto');
 83        // a list of prefixes used to coerce absolute paths into relative
 84        $this->prefixes = Raven_Util::get($options, 'prefixes', null);
 85        // app path is used to determine if code is part of your application
 86        $this->app_path = Raven_Util::get($options, 'app_path', null);
 87
 88        $this->processors = $this->setProcessorsFromOptions($options);
 89
 90        $this->_lasterror = null;
 91        $this->_user = null;
 92        $this->context = new Raven_Context();
 93
 94        if ($this->curl_method == 'async') {
 95            $this->_curl_handler = new Raven_CurlHandler($this->get_curl_options());
 96        }
 97    }
 98
 99    public static function getDefaultProcessors()
100    {
101        return array(
102            'Raven_SanitizeDataProcessor',
103        );
104    }
105
106    /**
107     * Sets the Raven_Processor sub-classes to be used when data is processed before being
108     * sent to Sentry.
109     *
110     * @param $options
111     * @return array
112     */
113    public function setProcessorsFromOptions($options)
114    {
115        $processors = array();
116        foreach (Raven_util::get($options, 'processors', self::getDefaultProcessors()) as $processor) {
117            $new_processor = new $processor($this);
118
119            if (isset($options['processorOptions']) && is_array($options['processorOptions'])) {
120                if (isset($options['processorOptions'][$processor]) && method_exists($processor, 'setProcessorOptions')) {
121                    $new_processor->setProcessorOptions($options['processorOptions'][$processor]);
122                }
123            }
124            $processors[] = $new_processor;
125        }
126        return $processors;
127    }
128
129    /**
130     * Parses a Raven-compatible DSN and returns an array of its values.
131     *
132     * @param string    $dsn    Raven compatible DSN: http://raven.readthedocs.org/en/latest/config/#the-sentry-dsn
133     * @return array            parsed DSN
134     */
135    public static function parseDSN($dsn)
136    {
137        $url = parse_url($dsn);
138        $scheme = (isset($url['scheme']) ? $url['scheme'] : '');
139        if (!in_array($scheme, array('http', 'https'))) {
140            throw new InvalidArgumentException('Unsupported Sentry DSN scheme: ' . (!empty($scheme) ? $scheme : '<not set>'));
141        }
142        $netloc = (isset($url['host']) ? $url['host'] : null);
143        $netloc.= (isset($url['port']) ? ':'.$url['port'] : null);
144        $rawpath = (isset($url['path']) ? $url['path'] : null);
145        if ($rawpath) {
146            $pos = strrpos($rawpath, '/', 1);
147            if ($pos !== false) {
148                $path = substr($rawpath, 0, $pos);
149                $project = substr($rawpath, $pos + 1);
150            } else {
151                $path = '';
152                $project = substr($rawpath, 1);
153            }
154        } else {
155            $project = null;
156            $path = '';
157        }
158        $username = (isset($url['user']) ? $url['user'] : null);
159        $password = (isset($url['pass']) ? $url['pass'] : null);
160        if (empty($netloc) || empty($project) || empty($username) || empty($password)) {
161            throw new InvalidArgumentException('Invalid Sentry DSN: ' . $dsn);
162        }
163
164        return array(
165            'server'     => sprintf('%s://%s%s/api/%s/store/', $scheme, $netloc, $path, $project),
166            'project'    => $project,
167            'public_key' => $username,
168            'secret_key' => $password,
169        );
170    }
171
172    public function getLastError()
173    {
174        return $this->_lasterror;
175    }
176
177    /**
178     * Given an identifier, returns a Sentry searchable string.
179     */
180    public function getIdent($ident)
181    {
182        // XXX: We don't calculate checksums yet, so we only have the ident.
183        return $ident;
184    }
185
186    /**
187     * Deprecated
188     */
189    public function message($message, $params=array(), $level=self::INFO,
190                            $stack=false, $vars = null)
191    {
192        return $this->captureMessage($message, $params, $level, $stack, $vars);
193    }
194
195    /**
196     * Deprecated
197     */
198    public function exception($exception)
199    {
200        return $this->captureException($exception);
201    }
202
203    /**
204     * Log a message to sentry
205     */
206    public function captureMessage($message, $params=array(), $level_or_options=array(),
207                            $stack=false, $vars = null)
208    {
209        // Gracefully handle messages which contain formatting characters, but were not
210        // intended to be used with formatting.
211        if (!empty($params)) {
212            $formatted_message = vsprintf($message, $params);
213        } else {
214            $formatted_message = $message;
215        }
216
217        if ($level_or_options === null) {
218            $data = array();
219        } elseif (!is_array($level_or_options)) {
220            $data = array(
221                'level' => $level_or_options,
222            );
223        } else {
224            $data = $level_or_options;
225        }
226
227        $data['message'] = $formatted_message;
228        $data['sentry.interfaces.Message'] = array(
229            'message' => $message,
230            'params' => $params,
231        );
232
233        return $this->capture($data, $stack, $vars);
234    }
235
236    /**
237     * Log an exception to sentry
238     */
239    public function captureException($exception, $culprit_or_options=null, $logger=null, $vars=null)
240    {
241        $has_chained_exceptions = version_compare(PHP_VERSION, '5.3.0', '>=');
242
243        if (in_array(get_class($exception), $this->exclude)) {
244            return null;
245        }
246
247        if (!is_array($culprit_or_options)) {
248            $data = array();
249            if ($culprit_or_options !== null) {
250                $data['culprit'] = $culprit_or_options;
251            }
252        } else {
253            $data = $culprit_or_options;
254        }
255
256        // TODO(dcramer): DRY this up
257        $message = $exception->getMessage();
258        if (empty($message)) {
259            $message = get_class($exception);
260        }
261
262        $exc = $exception;
263        do {
264            $exc_data = array(
265                'value' => $exc->getMessage(),
266                'type' => get_class($exc),
267                'module' => $exc->getFile() .':'. $exc->getLine(),
268            );
269
270            /**'exception'
271             * Exception::getTrace doesn't store the point at where the exception
272             * was thrown, so we have to stuff it in ourselves. Ugh.
273             */
274            $trace = $exc->getTrace();
275            $frame_where_exception_thrown = array(
276                'file' => $exc->getFile(),
277                'line' => $exc->getLine(),
278            );
279
280            array_unshift($trace, $frame_where_exception_thrown);
281
282            // manually trigger autoloading, as it's not done in some edge cases due to PHP bugs (see #60149)
283            if (!class_exists('Raven_Stacktrace')) {
284                spl_autoload_call('Raven_Stacktrace');
285            }
286
287            $exc_data['stacktrace'] = array(
288                'frames' => Raven_Stacktrace::get_stack_info(
289                    $trace, $this->trace, $this->shift_vars, $vars, $this->message_limit, $this->prefixes,
290                    $this->app_path
291                ),
292            );
293
294            $exceptions[] = $exc_data;
295        } while ($has_chained_exceptions && $exc = $exc->getPrevious());
296
297        $data['message'] = $message;
298        $data['exception'] = array(
299            'values' => array_reverse($exceptions),
300        );
301        if ($logger !== null) {
302            $data['logger'] = $logger;
303        }
304
305        if (empty($data['level'])) {
306            if (method_exists($exception, 'getSeverity')) {
307                $data['level'] = $this->translateSeverity($exception->getSeverity());
308            } else {
309                $data['level'] = self::ERROR;
310            }
311        }
312
313        return $this->capture($data, $trace, $vars);
314    }
315
316    /**
317     * Log an query to sentry
318     */
319    public function captureQuery($query, $level=self::INFO, $engine = '')
320    {
321        $data = array(
322            'message' => $query,
323            'level' => $level,
324            'sentry.interfaces.Query' => array(
325                'query' => $query
326            )
327        );
328
329        if ($engine !== '') {
330            $data['sentry.interfaces.Query']['engine'] = $engine;
331        }
332        return $this->capture($data, false);
333    }
334
335    protected function is_http_request()
336    {
337        return isset($_SERVER['REQUEST_METHOD']) && PHP_SAPI !== 'cli';
338    }
339
340    protected function get_http_data()
341    {
342        $env = $headers = array();
343
344        foreach ($_SERVER as $key => $value) {
345            if (0 === strpos($key, 'HTTP_')) {
346                $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))))] = $value;
347            } elseif (in_array($key, array('CONTENT_TYPE', 'CONTENT_LENGTH')) && $value !== '') {
348                $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))))] = $value;
349            } else {
350                $env[$key] = $value;
351            }
352        }
353
354        $result = array(
355            'method' => $this->_server_variable('REQUEST_METHOD'),
356            'url' => $this->get_current_url(),
357            'query_string' => $this->_server_variable('QUERY_STRING'),
358        );
359
360        // dont set this as an empty array as PHP will treat it as a numeric array
361        // instead of a mapping which goes against the defined Sentry spec
362        if (!empty($_POST)) {
363            $result['data'] = $_POST;
364        }
365        if (!empty($_COOKIE)) {
366            $result['cookies'] = $_COOKIE;
367        }
368        if (!empty($headers)) {
369            $result['headers'] = $headers;
370        }
371        if (!empty($env)) {
372            $result['env'] = $env;
373        }
374
375        return array(
376            'request' => $result,
377        );
378    }
379
380    protected function get_user_data()
381    {
382        $user = $this->context->user;
383        if ($user === null) {
384            if (!session_id()) {
385                return array();
386            }
387            $user = array(
388                'id' => session_id(),
389            );
390            if (!empty($_SESSION)) {
391                $user['data'] = $_SESSION;
392            }
393        }
394        return array(
395            'user' => $user,
396        );
397    }
398
399    protected function get_extra_data()
400    {
401        return $this->extra_data;
402    }
403
404    public function get_default_data()
405    {
406        return array(
407            'server_name' => $this->name,
408            'project' => $this->project,
409            'site' => $this->site,
410            'logger' => $this->logger,
411            'tags' => $this->tags,
412            'platform' => 'php',
413            'sdk' => array(
414                'name' => 'sentry-php',
415                'version' => self::VERSION,
416            ),
417        );
418    }
419
420    public function capture($data, $stack, $vars = null)
421    {
422        if (!isset($data['timestamp'])) {
423            $data['timestamp'] = gmdate('Y-m-d\TH:i:s\Z');
424        }
425        if (!isset($data['level'])) {
426            $data['level'] = self::ERROR;
427        }
428        if (!isset($data['tags'])) {
429            $data['tags'] = array();
430        }
431        if (!isset($data['extra'])) {
432            $data['extra'] = array();
433        }
434        if (!isset($data['event_id'])) {
435            $data['event_id'] = $this->uuid4();
436        }
437
438        if (isset($data['message'])) {
439            $data['message'] = substr($data['message'], 0, $this->message_limit);
440        }
441
442        $data = array_merge($this->get_default_data(), $data);
443
444        if ($this->is_http_request()) {
445            $data = array_merge($this->get_http_data(), $data);
446        }
447
448        $data = array_merge($this->get_user_data(), $data);
449
450        if ($this->release) {
451            $data['release'] = $this->release;
452        }
453        if ($this->environment) {
454            $data['environment'] = $this->environment;
455        }
456
457        $data['tags'] = array_merge(
458            $this->tags,
459            $this->context->tags,
460            $data['tags']);
461
462        $data['extra'] = array_merge(
463            $this->get_extra_data(),
464            $this->context->extra,
465            $data['extra']);
466
467        // manually trigger autoloading, as it's not done in some edge cases due to PHP bugs (see #60149)
468        if (!class_exists('Raven_Serializer')) {
469            spl_autoload_call('Raven_Serializer');
470        }
471
472        $serializer = new Raven_Serializer();
473        // avoid empty arrays (which dont convert to dicts)
474        if (empty($data['extra'])) {
475            unset($data['extra']);
476        } else {
477            $data['extra'] = $serializer->serialize($data['extra']);
478        }
479        if (empty($data['tags'])) {
480            unset($data['tags']);
481        } else {
482            $data['tags'] = $serializer->serialize($data['tags']);
483        }
484        if (!empty($data['user'])) {
485            $data['user'] = $serializer->serialize($data['user']);
486        }
487        if (!empty($data['request'])) {
488            $data['request'] = $serializer->serialize($data['request']);
489        }
490
491        if ((!$stack && $this->auto_log_stacks) || $stack === true) {
492            $stack = debug_backtrace();
493
494            // Drop last stack
495            array_shift($stack);
496        }
497
498        if (!empty($stack)) {
499            // manually trigger autoloading, as it's not done in some edge cases due to PHP bugs (see #60149)
500            if (!class_exists('Raven_Stacktrace')) {
501                spl_autoload_call('Raven_Stacktrace');
502            }
503
504            if (!isset($data['stacktrace']) && !isset($data['exception'])) {
505                $data['stacktrace'] = array(
506                    'frames' => Raven_Stacktrace::get_stack_info(
507                        $stack, $this->trace, $this->shift_vars, $vars, $this->message_limit,
508                        $this->prefixes, $this->app_path
509                    ),
510                );
511            }
512        }
513
514        $this->sanitize($data);
515        $this->process($data);
516
517        if (!$this->store_errors_for_bulk_send) {
518            $this->send($data);
519        } else {
520            if (empty($this->error_data)) {
521                $this->error_data = array();
522            }
523            $this->error_data[] = $data;
524        }
525
526        return $data['event_id'];
527    }
528
529    public function sanitize(&$data)
530    {
531    }
532
533    /**
534     * Process data through all defined Raven_Processor sub-classes
535     *
536     * @param array     $data       Associative array of data to log
537     */
538    public function process(&$data)
539    {
540        foreach ($this->processors as $processor) {
541            $processor->process($data);
542        }
543    }
544
545    public function sendUnsentErrors()
546    {
547        if (!empty($this->error_data)) {
548            foreach ($this->error_data as $data) {
549                $this->send($data);
550            }
551            unset($this->error_data);
552        }
553        if ($this->store_errors_for_bulk_send) {
554            //in case an error occurs after this is called, on shutdown, send any new errors.
555            $this->store_errors_for_bulk_send = !defined('RAVEN_CLIENT_END_REACHED');
556        }
557    }
558
559    /**
560     * Wrapper to handle encoding and sending data to the Sentry API server.
561     *
562     * @param array     $data       Associative array of data to log
563     */
564    public function send($data)
565    {
566        if (is_callable($this->send_callback) && !call_user_func($this->send_callback, $data)) {
567            // if send_callback returns falsely, end native send
568            return;
569        }
570
571        if (!$this->server) {
572            return;
573        }
574
575        $message = Raven_Compat::json_encode($data);
576
577        if (function_exists("gzcompress")) {
578            $message = gzcompress($message);
579        }
580        $message = base64_encode($message); // PHP's builtin curl_* function are happy without this, but the exec method requires it
581
582        $client_string = 'raven-php/' . self::VERSION;
583        $timestamp = microtime(true);
584        $headers = array(
585            'User-Agent' => $client_string,
586            'X-Sentry-Auth' => $this->get_auth_header(
587                $timestamp, $client_string, $this->public_key,
588                $this->secret_key),
589            'Content-Type' => 'application/octet-stream'
590        );
591
592        $this->send_remote($this->server, $message, $headers);
593    }
594
595    /**
596     * Send data to Sentry
597     *
598     * @param string    $url        Full URL to Sentry
599     * @param array     $data       Associative array of data to log
600     * @param array     $headers    Associative array of headers
601     */
602    private function send_remote($url, $data, $headers=array())
603    {
604        $parts = parse_url($url);
605        $parts['netloc'] = $parts['host'].(isset($parts['port']) ? ':'.$parts['port'] : null);
606        $this->send_http($url, $data, $headers);
607    }
608
609    protected function get_default_ca_cert()
610    {
611        return dirname(__FILE__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cacert.pem';
612    }
613
614    protected function get_curl_options()
615    {
616        $options = array(
617            CURLOPT_VERBOSE => false,
618            CURLOPT_SSL_VERIFYHOST => 2,
619            CURLOPT_SSL_VERIFYPEER => $this->verify_ssl,
620            CURLOPT_CAINFO => $this->ca_cert,
621            CURLOPT_USERAGENT => 'raven-php/' . self::VERSION,
622        );
623        if ($this->http_proxy) {
624            $options[CURLOPT_PROXY] = $this->http_proxy;
625        }
626        if ($this->curl_ssl_version) {
627            $options[CURLOPT_SSLVERSION] = $this->curl_ssl_version;
628        }
629        if ($this->curl_ipv4) {
630            $options[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
631        }
632        if (defined('CURLOPT_TIMEOUT_MS')) {
633            // MS is available in curl >= 7.16.2
634            $timeout = max(1, ceil(1000 * $this->timeout));
635
636            // some versions of PHP 5.3 don't have this defined correctly
637            if (!defined('CURLOPT_CONNECTTIMEOUT_MS')) {
638                //see http://stackoverflow.com/questions/9062798/php-curl-timeout-is-not-working/9063006#9063006
639                define('CURLOPT_CONNECTTIMEOUT_MS', 156);
640            }
641
642            $options[CURLOPT_CONNECTTIMEOUT_MS] = $timeout;
643            $options[CURLOPT_TIMEOUT_MS] = $timeout;
644        } else {
645            // fall back to the lower-precision timeout.
646            $timeout = max(1, ceil($this->timeout));
647            $options[CURLOPT_CONNECTTIMEOUT] = $timeout;
648            $options[CURLOPT_TIMEOUT] = $timeout;
649        }
650        return $options;
651    }
652
653    /**
654     * Send the message over http to the sentry url given
655     *
656     * @param string $url       URL of the Sentry instance to log to
657     * @param array $data       Associative array of data to log
658     * @param array $headers    Associative array of headers
659     */
660    private function send_http($url, $data, $headers=array())
661    {
662        if ($this->curl_method == 'async') {
663            $this->_curl_handler->enqueue($url, $data, $headers);
664        } elseif ($this->curl_method == 'exec') {
665            $this->send_http_asynchronous_curl_exec($url, $data, $headers);
666        } else {
667            $this->send_http_synchronous($url, $data, $headers);
668        }
669    }
670
671    /**
672     * Send the cURL to Sentry asynchronously. No errors will be returned from cURL
673     *
674     * @param string    $url        URL of the Sentry instance to log to
675     * @param array     $data       Associative array of data to log
676     * @param array     $headers    Associative array of headers
677     * @return bool
678     */
679    private function send_http_asynchronous_curl_exec($url, $data, $headers)
680    {
681        // TODO(dcramer): support ca_cert
682        $cmd = $this->curl_path.' -X POST ';
683        foreach ($headers as $key => $value) {
684            $cmd .= '-H \''. $key. ': '. $value. '\' ';
685        }
686        $cmd .= '-d \''. $data .'\' ';
687        $cmd .= '\''. $url .'\' ';
688        $cmd .= '-m 5 ';  // 5 second timeout for the whole process (connect + send)
689        if (!$this->verify_ssl) {
690            $cmd .= '-k ';
691        }
692        $cmd .= '> /dev/null 2>&1 &'; // ensure exec returns immediately while curl runs in the background
693
694        exec($cmd);
695
696        return true; // The exec method is just fire and forget, so just assume it always works
697    }
698
699    /**
700     * Send a blocking cURL to Sentry and check for errors from cURL
701     *
702     * @param string    $url        URL of the Sentry instance to log to
703     * @param array     $data       Associative array of data to log
704     * @param array     $headers    Associative array of headers
705     * @return bool
706     */
707    private function send_http_synchronous($url, $data, $headers)
708    {
709        $new_headers = array();
710        foreach ($headers as $key => $value) {
711            array_push($new_headers, $key .': '. $value);
712        }
713        // XXX(dcramer): Prevent 100-continue response form server (Fixes GH-216)
714        $new_headers[] = 'Expect:';
715
716        $curl = curl_init($url);
717        curl_setopt($curl, CURLOPT_POST, 1);
718        curl_setopt($curl, CURLOPT_HTTPHEADER, $new_headers);
719        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
720        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
721
722        $options = $this->get_curl_options();
723        $ca_cert = $options[CURLOPT_CAINFO];
724        unset($options[CURLOPT_CAINFO]);
725        curl_setopt_array($curl, $options);
726
727        curl_exec($curl);
728
729        $errno = curl_errno($curl);
730        // CURLE_SSL_CACERT || CURLE_SSL_CACERT_BADFILE
731        if ($errno == 60 || $errno == 77) {
732            curl_setopt($curl, CURLOPT_CAINFO, $ca_cert);
733            curl_exec($curl);
734        }
735
736        $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
737        $success = ($code == 200);
738        if (!$success) {
739            // It'd be nice just to raise an exception here, but it's not very PHP-like
740            $this->_lasterror = curl_error($curl);
741        } else {
742            $this->_lasterror = null;
743        }
744        curl_close($curl);
745
746        return $success;
747    }
748
749    /**
750     * Generate a Sentry authorization header string
751     *
752     * @param string    $timestamp      Timestamp when the event occurred
753     * @param string    $client         HTTP client name (not Raven_Client object)
754     * @param string    $api_key        Sentry API key
755     * @param string    $secret_key     Sentry API key
756     * @return string
757     */
758    protected function get_auth_header($timestamp, $client, $api_key, $secret_key)
759    {
760        $header = array(
761            sprintf('sentry_timestamp=%F', $timestamp),
762            "sentry_client={$client}",
763            sprintf('sentry_version=%s', self::PROTOCOL),
764        );
765
766        if ($api_key) {
767            $header[] = "sentry_key={$api_key}";
768        }
769
770        if ($secret_key) {
771            $header[] = "sentry_secret={$secret_key}";
772        }
773
774
775        return sprintf('Sentry %s', implode(', ', $header));
776    }
777
778    /**
779     * Generate an uuid4 value
780     *
781     * @return string
782     */
783    private function uuid4()
784    {
785        $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
786            // 32 bits for "time_low"
787            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
788
789            // 16 bits for "time_mid"
790            mt_rand(0, 0xffff),
791
792            // 16 bits for "time_hi_and_version",
793            // four most significant bits holds version number 4
794            mt_rand(0, 0x0fff) | 0x4000,
795
796            // 16 bits, 8 bits for "clk_seq_hi_res",
797            // 8 bits for "clk_seq_low",
798            // two most significant bits holds zero and one for variant DCE1.1
799            mt_rand(0, 0x3fff) | 0x8000,
800
801            // 48 bits for "node"
802            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
803        );
804
805        return str_replace('-', '', $uuid);
806    }
807
808    /**
809     * Return the URL for the current request
810     *
811     * @return string|null
812     */
813    protected function get_current_url()
814    {
815        // When running from commandline the REQUEST_URI is missing.
816        if (!isset($_SERVER['REQUEST_URI'])) {
817            return null;
818        }
819
820        // HTTP_HOST is a client-supplied header that is optional in HTTP 1.0
821        $host = (!empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST']
822            : (!empty($_SERVER['LOCAL_ADDR'])  ? $_SERVER['LOCAL_ADDR']
823            : (!empty($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '')));
824
825        $httpS = $this->isHttps() ? 's' : '';
826        return "http{$httpS}://{$host}{$_SERVER['REQUEST_URI']}";
827    }
828
829    /**
830     * Was the current request made over https?
831     *
832     * @return bool
833     */
834    protected function isHttps()
835    {
836        if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
837            return true;
838        }
839
840        if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) {
841            return true;
842        }
843
844        if (!empty($this->trust_x_forwarded_proto) &&
845            !empty($_SERVER['X-FORWARDED-PROTO']) &&
846            $_SERVER['X-FORWARDED-PROTO'] === 'https') {
847            return true;
848        }
849
850        return false;
851    }
852
853    /**
854     * Get the value of a key from $_SERVER
855     *
856     * @param string $key       Key whose value you wish to obtain
857     * @return string           Key's value
858     */
859    private function _server_variable($key)
860    {
861        if (isset($_SERVER[$key])) {
862            return $_SERVER[$key];
863        }
864
865        return '';
866    }
867
868    /**
869     * Translate a PHP Error constant into a Sentry log level group
870     *
871     * @param string $severity  PHP E_$x error constant
872     * @return string           Sentry log level group
873     */
874    public function translateSeverity($severity)
875    {
876        if (is_array($this->severity_map) && isset($this->severity_map[$severity])) {
877            return $this->severity_map[$severity];
878        }
879        switch ($severity) {
880            case E_ERROR:              return Raven_Client::ERROR;
881            case E_WARNING:            return Raven_Client::WARN;
882            case E_PARSE:              return Raven_Client::ERROR;
883            case E_NOTICE:             return Raven_Client::INFO;
884            case E_CORE_ERROR:         return Raven_Client::ERROR;
885            case E_CORE_WARNING:       return Raven_Client::WARN;
886            case E_COMPILE_ERROR:      return Raven_Client::ERROR;
887            case E_COMPILE_WARNING:    return Raven_Client::WARN;
888            case E_USER_ERROR:         return Raven_Client::ERROR;
889            case E_USER_WARNING:       return Raven_Client::WARN;
890            case E_USER_NOTICE:        return Raven_Client::INFO;
891            case E_STRICT:             return Raven_Client::INFO;
892            case E_RECOVERABLE_ERROR:  return Raven_Client::ERROR;
893        }
894        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
895            switch ($severity) {
896            case E_DEPRECATED:         return Raven_Client::WARN;
897            case E_USER_DEPRECATED:    return Raven_Client::WARN;
898          }
899        }
900        return Raven_Client::ERROR;
901    }
902
903    /**
904     * Provide a map of PHP Error constants to Sentry logging groups to use instead
905     * of the defaults in translateSeverity()
906     *
907     * @param array $map
908     */
909    public function registerSeverityMap($map)
910    {
911        $this->severity_map = $map;
912    }
913
914    /**
915     * Convenience function for setting a user's ID and Email
916     *
917     * @param string $id            User's ID
918     * @param string|null $email    User's email
919     * @param array $data           Additional user data
920     */
921    public function set_user_data($id, $email=null, $data=array())
922    {
923        $user = array('id' => $id);
924        if (isset($email)) {
925            $user['email'] = $email;
926        }
927        $this->user_context(array_merge($user, $data));
928    }
929
930    /**
931     * Sets user context.
932     *
933     * @param array $data   Associative array of user data
934     */
935    public function user_context($data)
936    {
937        $this->context->user = $data;
938    }
939
940    /**
941     * Appends tags context.
942     *
943     * @param array $data   Associative array of tags
944     */
945    public function tags_context($data)
946    {
947        $this->context->tags = array_merge($this->context->tags, $data);
948    }
949
950    /**
951     * Appends additional context.
952     *
953     * @param array $data   Associative array of extra data
954     */
955    public function extra_context($data)
956    {
957        $this->context->extra = array_merge($this->context->extra, $data);
958    }
959
960    /**
961     * @param array $processors
962     */
963    public function setProcessors(array $processors)
964    {
965        $this->processors = $processors;
966    }
967}