PageRenderTime 28ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

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