Stacktrace.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. /**
  3. * Small helper class to inspect the stacktrace
  4. *
  5. * @package raven
  6. */
  7. class Raven_Stacktrace
  8. {
  9. public static $statements = array(
  10. 'include',
  11. 'include_once',
  12. 'require',
  13. 'require_once',
  14. );
  15. public static function get_stack_info($frames,
  16. $trace = false,
  17. $errcontext = null,
  18. $frame_var_limit = Raven_Client::MESSAGE_LIMIT,
  19. $strip_prefixes = null,
  20. $app_path = null,
  21. $excluded_app_paths = null,
  22. Raven_Serializer $serializer = null,
  23. Raven_ReprSerializer $reprSerializer = null)
  24. {
  25. $serializer = $serializer ?: new Raven_Serializer();
  26. $reprSerializer = $reprSerializer ?: new Raven_ReprSerializer();
  27. /**
  28. * PHP stores calls in the stacktrace, rather than executing context. Sentry
  29. * wants to know "when Im calling this code, where am I", and PHP says "I'm
  30. * calling this function" not "I'm in this function". Due to that, we shift
  31. * the context for a frame up one, meaning the variables (which are the calling
  32. * args) come from the previous frame.
  33. */
  34. $result = array();
  35. for ($i = 0; $i < count($frames); $i++) {
  36. $frame = isset($frames[$i]) ? $frames[$i] : array();
  37. $nextframe = isset($frames[$i + 1]) ? $frames[$i + 1] : array();
  38. if (!array_key_exists('file', $frame)) {
  39. $context = array();
  40. if (!empty($frame['class'])) {
  41. $context['line'] = sprintf('%s%s%s', $frame['class'], $frame['type'], $frame['function']);
  42. try {
  43. $reflect = new ReflectionClass($frame['class']);
  44. $context['filename'] = $filename = $reflect->getFileName();
  45. } catch (ReflectionException $e) {
  46. // Forget it if we run into errors, it's not worth it.
  47. }
  48. } elseif (!empty($frame['function'])) {
  49. $context['line'] = sprintf('%s(anonymous)', $frame['function']);
  50. } else {
  51. $context['line'] = sprintf('(anonymous)');
  52. }
  53. if (empty($context['filename'])) {
  54. $context['filename'] = $filename = '[Anonymous function]';
  55. }
  56. $abs_path = '';
  57. $context['prefix'] = '';
  58. $context['suffix'] = '';
  59. $context['lineno'] = 0;
  60. } else {
  61. $context = self::read_source_file($frame['file'], $frame['line']);
  62. $abs_path = $frame['file'];
  63. }
  64. // strip base path if present
  65. $context['filename'] = self::strip_prefixes($context['filename'], $strip_prefixes);
  66. if ($i === 0 && isset($errcontext)) {
  67. // If we've been given an error context that can be used as the vars for the first frame.
  68. $vars = $errcontext;
  69. } else {
  70. if ($trace) {
  71. $vars = self::get_frame_context($nextframe, $frame_var_limit);
  72. } else {
  73. $vars = array();
  74. }
  75. }
  76. $data = array(
  77. 'filename' => $context['filename'],
  78. 'lineno' => (int) $context['lineno'],
  79. 'function' => isset($nextframe['function']) ? $nextframe['function'] : null,
  80. 'pre_context' => $serializer->serialize($context['prefix']),
  81. 'context_line' => $serializer->serialize($context['line']),
  82. 'post_context' => $serializer->serialize($context['suffix']),
  83. );
  84. // detect in_app based on app path
  85. if ($app_path) {
  86. $norm_abs_path = @realpath($abs_path) ?: $abs_path;
  87. if (!$abs_path) {
  88. $in_app = false;
  89. } else {
  90. $in_app = (bool)(substr($norm_abs_path, 0, strlen($app_path)) === $app_path);
  91. }
  92. if ($in_app && $excluded_app_paths) {
  93. foreach ($excluded_app_paths as $path) {
  94. if (substr($norm_abs_path, 0, strlen($path)) === $path) {
  95. $in_app = false;
  96. break;
  97. }
  98. }
  99. }
  100. $data['in_app'] = $in_app;
  101. }
  102. // dont set this as an empty array as PHP will treat it as a numeric array
  103. // instead of a mapping which goes against the defined Sentry spec
  104. if (!empty($vars)) {
  105. $cleanVars = array();
  106. foreach ($vars as $key => $value) {
  107. $value = $reprSerializer->serialize($value);
  108. if (is_string($value) || is_numeric($value)) {
  109. $cleanVars[(string)$key] = substr($value, 0, $frame_var_limit);
  110. } else {
  111. $cleanVars[(string)$key] = $value;
  112. }
  113. }
  114. $data['vars'] = $cleanVars;
  115. }
  116. $result[] = $data;
  117. }
  118. return array_reverse($result);
  119. }
  120. public static function get_default_context($frame, $frame_arg_limit = Raven_Client::MESSAGE_LIMIT)
  121. {
  122. if (!isset($frame['args'])) {
  123. return array();
  124. }
  125. $i = 1;
  126. $args = array();
  127. foreach ($frame['args'] as $arg) {
  128. $args['param'.$i] = self::serialize_argument($arg, $frame_arg_limit);
  129. $i++;
  130. }
  131. return $args;
  132. }
  133. public static function get_frame_context($frame, $frame_arg_limit = Raven_Client::MESSAGE_LIMIT)
  134. {
  135. if (!isset($frame['args'])) {
  136. return array();
  137. }
  138. // The reflection API seems more appropriate if we associate it with the frame
  139. // where the function is actually called (since we're treating them as function context)
  140. if (!isset($frame['function'])) {
  141. return self::get_default_context($frame, $frame_arg_limit);
  142. }
  143. if (strpos($frame['function'], '__lambda_func') !== false) {
  144. return self::get_default_context($frame, $frame_arg_limit);
  145. }
  146. if (isset($frame['class']) && $frame['class'] == 'Closure') {
  147. return self::get_default_context($frame, $frame_arg_limit);
  148. }
  149. if (strpos($frame['function'], '{closure}') !== false) {
  150. return self::get_default_context($frame, $frame_arg_limit);
  151. }
  152. if (in_array($frame['function'], self::$statements)) {
  153. if (empty($frame['args'])) {
  154. // No arguments
  155. return array();
  156. } else {
  157. // Sanitize the file path
  158. return array(
  159. 'param1' => self::serialize_argument($frame['args'][0], $frame_arg_limit),
  160. );
  161. }
  162. }
  163. try {
  164. if (isset($frame['class'])) {
  165. if (method_exists($frame['class'], $frame['function'])) {
  166. $reflection = new ReflectionMethod($frame['class'], $frame['function']);
  167. } elseif ($frame['type'] === '::') {
  168. $reflection = new ReflectionMethod($frame['class'], '__callStatic');
  169. } else {
  170. $reflection = new ReflectionMethod($frame['class'], '__call');
  171. }
  172. } elseif (function_exists($frame['function'])) {
  173. $reflection = new ReflectionFunction($frame['function']);
  174. } else {
  175. return self::get_default_context($frame, $frame_arg_limit);
  176. }
  177. } catch (ReflectionException $e) {
  178. return self::get_default_context($frame, $frame_arg_limit);
  179. }
  180. $params = $reflection->getParameters();
  181. $args = array();
  182. foreach ($frame['args'] as $i => $arg) {
  183. $arg = self::serialize_argument($arg, $frame_arg_limit);
  184. if (isset($params[$i])) {
  185. // Assign the argument by the parameter name
  186. $args[$params[$i]->name] = $arg;
  187. } else {
  188. $args['param'.$i] = $arg;
  189. }
  190. }
  191. return $args;
  192. }
  193. private static function serialize_argument($arg, $frame_arg_limit)
  194. {
  195. if (is_array($arg)) {
  196. $_arg = array();
  197. foreach ($arg as $key => $value) {
  198. if (is_string($value) || is_numeric($value)) {
  199. $_arg[$key] = substr($value, 0, $frame_arg_limit);
  200. } else {
  201. $_arg[$key] = $value;
  202. }
  203. }
  204. return $_arg;
  205. } elseif (is_string($arg) || is_numeric($arg)) {
  206. return substr($arg, 0, $frame_arg_limit);
  207. } else {
  208. return $arg;
  209. }
  210. }
  211. private static function strip_prefixes($filename, $prefixes)
  212. {
  213. if ($prefixes === null) {
  214. return $filename;
  215. }
  216. foreach ($prefixes as $prefix) {
  217. if (substr($filename, 0, strlen($prefix)) === $prefix) {
  218. return substr($filename, strlen($prefix));
  219. }
  220. }
  221. return $filename;
  222. }
  223. private static function read_source_file($filename, $lineno, $context_lines = 5)
  224. {
  225. $frame = array(
  226. 'prefix' => array(),
  227. 'line' => '',
  228. 'suffix' => array(),
  229. 'filename' => $filename,
  230. 'lineno' => $lineno,
  231. );
  232. if ($filename === null || $lineno === null) {
  233. return $frame;
  234. }
  235. // Code which is eval'ed have a modified filename.. Extract the
  236. // correct filename + linenumber from the string.
  237. $matches = array();
  238. $matched = preg_match("/^(.*?)\\((\\d+)\\) : eval\\(\\)'d code$/",
  239. $filename, $matches);
  240. if ($matched) {
  241. $frame['filename'] = $filename = $matches[1];
  242. $frame['lineno'] = $lineno = $matches[2];
  243. }
  244. // In the case of an anonymous function, the filename is sent as:
  245. // "</path/to/filename>(<lineno>) : runtime-created function"
  246. // Extract the correct filename + linenumber from the string.
  247. $matches = array();
  248. $matched = preg_match("/^(.*?)\\((\\d+)\\) : runtime-created function$/",
  249. $filename, $matches);
  250. if ($matched) {
  251. $frame['filename'] = $filename = $matches[1];
  252. $frame['lineno'] = $lineno = $matches[2];
  253. }
  254. if (!file_exists($filename)) {
  255. return $frame;
  256. }
  257. try {
  258. $file = new SplFileObject($filename);
  259. $target = max(0, ($lineno - ($context_lines + 1)));
  260. $file->seek($target);
  261. $cur_lineno = $target+1;
  262. while (!$file->eof()) {
  263. $line = rtrim($file->current(), "\r\n");
  264. if ($cur_lineno == $lineno) {
  265. $frame['line'] = $line;
  266. } elseif ($cur_lineno < $lineno) {
  267. $frame['prefix'][] = $line;
  268. } elseif ($cur_lineno > $lineno) {
  269. $frame['suffix'][] = $line;
  270. }
  271. $cur_lineno++;
  272. if ($cur_lineno > $lineno + $context_lines) {
  273. break;
  274. }
  275. $file->next();
  276. }
  277. } catch (RuntimeException $exc) {
  278. return $frame;
  279. }
  280. return $frame;
  281. }
  282. }