PageRenderTime 43ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/app/protected/extensions/sentrylog/lib/Raven/Client.php

https://bitbucket.org/andreustimm/zurmo
PHP | 419 lines | 293 code | 55 blank | 71 comment | 41 complexity | 65febb86176f43502da4630ce4b469c4 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0, LGPL-3.0, LGPL-2.1, BSD-2-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.1.0';
  18. const DEBUG = 10;
  19. const INFO = 20;
  20. const WARN = 30;
  21. const WARNING = 30;
  22. const ERROR = 40;
  23. function __construct($options_or_dsn=null, $options=array())
  24. {
  25. if (is_null($options_or_dsn) && !empty($_SERVER['SENTRY_DSN'])) {
  26. // Read from environment
  27. $options_or_dsn = $_SERVER['SENTRY_DSN'];
  28. }
  29. if (!is_array($options_or_dsn)) {
  30. if (!empty($options_or_dsn)) {
  31. // Must be a valid DSN
  32. $options_or_dsn = self::parseDSN($options_or_dsn);
  33. } else {
  34. $options_or_dsn = array();
  35. }
  36. }
  37. $options = array_merge($options_or_dsn, $options);
  38. $this->servers = (!empty($options['servers']) ? $options['servers'] : null);
  39. $this->secret_key = (!empty($options['secret_key']) ? $options['secret_key'] : null);
  40. $this->public_key = (!empty($options['public_key']) ? $options['public_key'] : null);
  41. $this->project = (isset($options['project']) ? $options['project'] : 1);
  42. $this->auto_log_stacks = (isset($options['auto_log_stacks']) ? $options['auto_log_stacks'] : false);
  43. $this->name = (!empty($options['name']) ? $options['name'] : Raven_Compat::gethostname());
  44. $this->site = (!empty($options['site']) ? $options['site'] : $this->_server_variable('SERVER_NAME'));
  45. $this->_lasterror = null;
  46. }
  47. /**
  48. * Parses a Raven-compatible DSN and returns an array of its values.
  49. */
  50. public static function parseDSN($dsn)
  51. {
  52. $url = parse_url($dsn);
  53. $scheme = (isset($url['scheme']) ? $url['scheme'] : '');
  54. if (!in_array($scheme, array('http', 'https', 'udp'))) {
  55. throw new InvalidArgumentException('Unsupported Sentry DSN scheme: ' . $scheme);
  56. }
  57. $netloc = (isset($url['host']) ? $url['host'] : null);
  58. $netloc.= (isset($url['port']) ? ':'.$url['port'] : null);
  59. $rawpath = (isset($url['path']) ? $url['path'] : null);
  60. if ($rawpath) {
  61. $pos = strrpos($rawpath, '/', 1);
  62. if ($pos !== false) {
  63. $path = substr($rawpath, 0, $pos);
  64. $project = substr($rawpath, $pos + 1);
  65. }
  66. else {
  67. $path = '';
  68. $project = substr($rawpath, 1);
  69. }
  70. }
  71. else {
  72. $project = null;
  73. $path = '';
  74. }
  75. $username = (isset($url['user']) ? $url['user'] : null);
  76. $password = (isset($url['pass']) ? $url['pass'] : null);
  77. if (empty($netloc) || empty($project) || empty($username) || empty($password)) {
  78. throw new InvalidArgumentException('Invalid Sentry DSN: ' . $dsn);
  79. }
  80. return array(
  81. 'servers' => array(sprintf('%s://%s%s/api/store/', $scheme, $netloc, $path)),
  82. 'project' => $project,
  83. 'public_key' => $username,
  84. 'secret_key' => $password,
  85. );
  86. }
  87. /**
  88. * Given an identifier, returns a Sentry searchable string.
  89. */
  90. public function getIdent($ident)
  91. {
  92. // XXX: We dont calculate checksums yet, so we only have the ident.
  93. return $ident;
  94. }
  95. /**
  96. * Deprecated
  97. */
  98. public function message($message, $params=array(), $level=self::INFO,
  99. $stack=false)
  100. {
  101. return $this->captureMessage($message, $params, $level, $stack);
  102. }
  103. /**
  104. * Deprecated
  105. */
  106. public function exception($exception)
  107. {
  108. return $this->captureException($exception);
  109. }
  110. /**
  111. * Log a message to sentry
  112. */
  113. public function captureMessage($message, $params=array(), $level=self::INFO,
  114. $stack=false)
  115. {
  116. $level = self::ERROR;
  117. // Gracefully handle messages which contain formatting characters, but were not
  118. // intended to be used with formatting.
  119. if (!empty($params)) {
  120. $formatted_message = vsprintf($message, $params);
  121. } else {
  122. $formatted_message = $message;
  123. }
  124. $data = array(
  125. 'message' => $formatted_message,
  126. 'level' => $level,
  127. 'sentry.interfaces.Message' => array(
  128. 'message' => $message,
  129. 'params' => $params,
  130. )
  131. );
  132. return $this->capture($data, $stack);
  133. }
  134. /**
  135. * Log an exception to sentry
  136. */
  137. public function captureException($exception, $culprit=null, $logger=null)
  138. {
  139. $exc_message = $exception->getMessage();
  140. if (empty($exc_message)) {
  141. $exc_message = '<unknown exception>';
  142. }
  143. $data = array(
  144. 'message' => $exc_message
  145. );
  146. $data['sentry.interfaces.Exception'] = array(
  147. 'value' => $exc_message,
  148. 'type' => $exception->getCode(),
  149. 'module' => $exception->getFile() .':'. $exception->getLine(),
  150. );
  151. if ($culprit){
  152. $data["culprit"] = $culprit;
  153. }
  154. if ($logger){
  155. $data["logger"] = $logger;
  156. }
  157. /**'sentry.interfaces.Exception'
  158. * Exception::getTrace doesn't store the point at where the exception
  159. * was thrown, so we have to stuff it in ourselves. Ugh.
  160. */
  161. $trace = $exception->getTrace();
  162. $frame_where_exception_thrown = array(
  163. 'file' => $exception->getFile(),
  164. 'line' => $exception->getLine(),
  165. );
  166. array_unshift($trace, $frame_where_exception_thrown);
  167. return $this->capture($data, $trace);
  168. }
  169. public function capture($data, $stack)
  170. {
  171. $event_id = $this->uuid4();
  172. if (!isset($data['timestamp'])) $data['timestamp'] = gmdate('Y-m-d\TH:i:s\Z');
  173. if (!isset($data['level'])) $data['level'] = self::ERROR;
  174. // The function getallheaders() is only available when running in a
  175. // web-request. The function is missing when run from the commandline..
  176. $headers = array();
  177. if (function_exists('getallheaders')) {
  178. $headers = getallheaders();
  179. // Don't log cookie information, for security reasons
  180. $headers['Cookie'] = 'Unset';
  181. }
  182. $data = array_merge($data, array(
  183. 'server_name' => $this->name . ' [Zurmo Version: ' . $data['sentry.interfaces.Message']['params']['zurmoVersion'] . ']',
  184. 'event_id' => $event_id,
  185. 'project' => $this->project,
  186. 'site' => $this->site,
  187. 'sentry.interfaces.Http' => array(
  188. 'method' => $this->_server_variable('REQUEST_METHOD'),
  189. 'url' => $this->get_current_url(),
  190. 'query_string' => $this->_server_variable('QUERY_STRNG'),
  191. 'data' => $_POST,
  192. 'cookies' => array(), // Don't log cookie information, for security reasons
  193. 'headers' => $headers,
  194. 'env' => $_SERVER,
  195. )
  196. ));
  197. if ((!$stack && $this->auto_log_stacks) || $stack === True) {
  198. $stack = debug_backtrace();
  199. // Drop last stack
  200. array_shift($stack);
  201. }
  202. if (!empty($stack)) {
  203. /**
  204. * PHP's way of storing backstacks seems bass-ackwards to me
  205. * 'function' is not the function you're in; it's any function being
  206. * called, so we have to shift 'function' down by 1. Ugh.
  207. */
  208. for ($i = 0; $i < count($stack) - 1; $i++) {
  209. $stack[$i]['function'] = $stack[$i + 1]['function'];
  210. }
  211. $stack[count($stack) - 1]['function'] = null;
  212. if (!isset($data['sentry.interfaces.Stacktrace'])) {
  213. $data['sentry.interfaces.Stacktrace'] = array(
  214. 'frames' => Raven_Stacktrace::get_stack_info($stack)
  215. );
  216. }
  217. }
  218. if (function_exists('mb_convert_encoding')) {
  219. $data = $this->remove_invalid_utf8($data);
  220. }
  221. $this->send($data);
  222. return $event_id;
  223. }
  224. public function send($data)
  225. {
  226. $message = base64_encode(gzcompress(Raven_Compat::json_encode($data)));
  227. foreach($this->servers as $url) {
  228. $client_string = 'raven-php/' . self::VERSION;
  229. $timestamp = microtime(true);
  230. $signature = $this->get_signature(
  231. $message, $timestamp, $this->secret_key);
  232. $headers = array(
  233. 'User-Agent' => $client_string,
  234. 'X-Sentry-Auth' => $this->get_auth_header(
  235. $signature, $timestamp, $client_string, $this->public_key),
  236. 'Content-Type' => 'application/octet-stream'
  237. );
  238. $this->send_remote($url, $message, $headers);
  239. }
  240. }
  241. private function send_remote($url, $data, $headers=array())
  242. {
  243. $parts = parse_url($url);
  244. $parts['netloc'] = $parts['host'].(isset($parts['port']) ? ':'.$parts['port'] : null);
  245. if ($parts['scheme'] === 'udp')
  246. return $this->send_udp($parts['netloc'], $data, $headers['X-Sentry-Auth']);
  247. return $this->send_http($url, $data, $headers);
  248. }
  249. private function send_udp($netloc, $data, $headers)
  250. {
  251. list($host, $port) = explode(':', $netloc);
  252. $raw_data = $headers."\n\n".$data;
  253. $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
  254. socket_sendto($sock, $raw_data, strlen($raw_data), 0, $host, $port);
  255. socket_close($sock);
  256. return true;
  257. }
  258. /**
  259. * Send the message over http to the sentry url given
  260. */
  261. private function send_http($url, $data, $headers=array())
  262. {
  263. $new_headers = array();
  264. foreach($headers as $key => $value) {
  265. array_push($new_headers, $key .': '. $value);
  266. }
  267. $parts = parse_url($url);
  268. $curl = curl_init($url);
  269. curl_setopt($curl, CURLOPT_POST, 1);
  270. curl_setopt($curl, CURLOPT_HTTPHEADER, $new_headers);
  271. curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
  272. curl_setopt($curl, CURLOPT_VERBOSE, false);
  273. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  274. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  275. $ret = curl_exec($curl);
  276. $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  277. $success = ($code == 200);
  278. curl_close($curl);
  279. if (!$success) {
  280. // It'd be nice just to raise an exception here, but it's not very PHP-like
  281. $this->_lasterror = $ret;
  282. }
  283. return $success;
  284. }
  285. /**
  286. * Create a signature
  287. */
  288. private function get_signature($message, $timestamp, $key)
  289. {
  290. return Raven_Compat::hash_hmac('sha1', sprintf('%F', $timestamp) .' '. $message, $key);
  291. }
  292. private function get_auth_header($signature, $timestamp, $client,
  293. $api_key=null)
  294. {
  295. $header = array(
  296. sprintf("sentry_timestamp=%F", $timestamp),
  297. "sentry_signature={$signature}",
  298. "sentry_client={$client}",
  299. "sentry_version=2.0",
  300. );
  301. if ($api_key) {
  302. array_push($header, "sentry_key={$api_key}");
  303. }
  304. return sprintf('Sentry %s', implode(', ', $header));
  305. }
  306. /**
  307. * Generate an uuid4 value
  308. */
  309. private function uuid4()
  310. {
  311. $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
  312. // 32 bits for "time_low"
  313. mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
  314. // 16 bits for "time_mid"
  315. mt_rand( 0, 0xffff ),
  316. // 16 bits for "time_hi_and_version",
  317. // four most significant bits holds version number 4
  318. mt_rand( 0, 0x0fff ) | 0x4000,
  319. // 16 bits, 8 bits for "clk_seq_hi_res",
  320. // 8 bits for "clk_seq_low",
  321. // two most significant bits holds zero and one for variant DCE1.1
  322. mt_rand( 0, 0x3fff ) | 0x8000,
  323. // 48 bits for "node"
  324. mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
  325. );
  326. return str_replace('-', '', $uuid);
  327. }
  328. /**
  329. * Return the URL for the current request
  330. */
  331. private function get_current_url()
  332. {
  333. // When running from commandline the REQUEST_URI is missing.
  334. if ($this->_server_variable('REQUEST_URI') === '') {
  335. return '';
  336. }
  337. $schema = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'
  338. || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
  339. return $schema . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
  340. }
  341. private function _server_variable($key)
  342. {
  343. if (isset($_SERVER[$key])) {
  344. return $_SERVER[$key];
  345. }
  346. return '';
  347. }
  348. private function remove_invalid_utf8($data)
  349. {
  350. foreach ($data as $key => $value) {
  351. if (is_string($key)) {
  352. $key = mb_convert_encoding($key, 'UTF-8', 'UTF-8');
  353. }
  354. if (is_string($value)) {
  355. $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
  356. }
  357. if (is_array($value)) {
  358. $value = $this->remove_invalid_utf8($value);
  359. }
  360. $data[$key] = $value;
  361. }
  362. return $data;
  363. }
  364. }