PageRenderTime 83ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php

https://gitlab.com/madwanz64/laravel
PHP | 293 lines | 184 code | 40 blank | 69 comment | 18 complexity | 79b43be74d22449e3469eb1c03f46bc2 MD5 | raw file
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of the Monolog package.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Monolog\Handler;
  11. use Monolog\Formatter\FormatterInterface;
  12. use Monolog\Formatter\LineFormatter;
  13. use Monolog\Utils;
  14. use function count;
  15. use function headers_list;
  16. use function stripos;
  17. use function trigger_error;
  18. use const E_USER_DEPRECATED;
  19. /**
  20. * Handler sending logs to browser's javascript console with no browser extension required
  21. *
  22. * @author Olivier Poitrey <rs@dailymotion.com>
  23. *
  24. * @phpstan-import-type FormattedRecord from AbstractProcessingHandler
  25. */
  26. class BrowserConsoleHandler extends AbstractProcessingHandler
  27. {
  28. /** @var bool */
  29. protected static $initialized = false;
  30. /** @var FormattedRecord[] */
  31. protected static $records = [];
  32. protected const FORMAT_HTML = 'html';
  33. protected const FORMAT_JS = 'js';
  34. protected const FORMAT_UNKNOWN = 'unknown';
  35. /**
  36. * {@inheritDoc}
  37. *
  38. * Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format.
  39. *
  40. * Example of formatted string:
  41. *
  42. * You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
  43. */
  44. protected function getDefaultFormatter(): FormatterInterface
  45. {
  46. return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
  47. }
  48. /**
  49. * {@inheritDoc}
  50. */
  51. protected function write(array $record): void
  52. {
  53. // Accumulate records
  54. static::$records[] = $record;
  55. // Register shutdown handler if not already done
  56. if (!static::$initialized) {
  57. static::$initialized = true;
  58. $this->registerShutdownFunction();
  59. }
  60. }
  61. /**
  62. * Convert records to javascript console commands and send it to the browser.
  63. * This method is automatically called on PHP shutdown if output is HTML or Javascript.
  64. */
  65. public static function send(): void
  66. {
  67. $format = static::getResponseFormat();
  68. if ($format === self::FORMAT_UNKNOWN) {
  69. return;
  70. }
  71. if (count(static::$records)) {
  72. if ($format === self::FORMAT_HTML) {
  73. static::writeOutput('<script>' . static::generateScript() . '</script>');
  74. } elseif ($format === self::FORMAT_JS) {
  75. static::writeOutput(static::generateScript());
  76. }
  77. static::resetStatic();
  78. }
  79. }
  80. public function close(): void
  81. {
  82. self::resetStatic();
  83. }
  84. public function reset()
  85. {
  86. parent::reset();
  87. self::resetStatic();
  88. }
  89. /**
  90. * Forget all logged records
  91. */
  92. public static function resetStatic(): void
  93. {
  94. static::$records = [];
  95. }
  96. /**
  97. * Wrapper for register_shutdown_function to allow overriding
  98. */
  99. protected function registerShutdownFunction(): void
  100. {
  101. if (PHP_SAPI !== 'cli') {
  102. register_shutdown_function(['Monolog\Handler\BrowserConsoleHandler', 'send']);
  103. }
  104. }
  105. /**
  106. * Wrapper for echo to allow overriding
  107. */
  108. protected static function writeOutput(string $str): void
  109. {
  110. echo $str;
  111. }
  112. /**
  113. * Checks the format of the response
  114. *
  115. * If Content-Type is set to application/javascript or text/javascript -> js
  116. * If Content-Type is set to text/html, or is unset -> html
  117. * If Content-Type is anything else -> unknown
  118. *
  119. * @return string One of 'js', 'html' or 'unknown'
  120. * @phpstan-return self::FORMAT_*
  121. */
  122. protected static function getResponseFormat(): string
  123. {
  124. // Check content type
  125. foreach (headers_list() as $header) {
  126. if (stripos($header, 'content-type:') === 0) {
  127. return static::getResponseFormatFromContentType($header);
  128. }
  129. }
  130. return self::FORMAT_HTML;
  131. }
  132. /**
  133. * @return string One of 'js', 'html' or 'unknown'
  134. * @phpstan-return self::FORMAT_*
  135. */
  136. protected static function getResponseFormatFromContentType(string $contentType): string
  137. {
  138. // This handler only works with HTML and javascript outputs
  139. // text/javascript is obsolete in favour of application/javascript, but still used
  140. if (stripos($contentType, 'application/javascript') !== false || stripos($contentType, 'text/javascript') !== false) {
  141. return self::FORMAT_JS;
  142. }
  143. if (stripos($contentType, 'text/html') !== false) {
  144. return self::FORMAT_HTML;
  145. }
  146. return self::FORMAT_UNKNOWN;
  147. }
  148. private static function generateScript(): string
  149. {
  150. $script = [];
  151. foreach (static::$records as $record) {
  152. $context = static::dump('Context', $record['context']);
  153. $extra = static::dump('Extra', $record['extra']);
  154. if (empty($context) && empty($extra)) {
  155. $script[] = static::call_array('log', static::handleStyles($record['formatted']));
  156. } else {
  157. $script = array_merge(
  158. $script,
  159. [static::call_array('groupCollapsed', static::handleStyles($record['formatted']))],
  160. $context,
  161. $extra,
  162. [static::call('groupEnd')]
  163. );
  164. }
  165. }
  166. return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
  167. }
  168. /**
  169. * @return string[]
  170. */
  171. private static function handleStyles(string $formatted): array
  172. {
  173. $args = [];
  174. $format = '%c' . $formatted;
  175. preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
  176. foreach (array_reverse($matches) as $match) {
  177. $args[] = '"font-weight: normal"';
  178. $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0]));
  179. $pos = $match[0][1];
  180. $format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0]));
  181. }
  182. $args[] = static::quote('font-weight: normal');
  183. $args[] = static::quote($format);
  184. return array_reverse($args);
  185. }
  186. private static function handleCustomStyles(string $style, string $string): string
  187. {
  188. static $colors = ['blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey'];
  189. static $labels = [];
  190. $style = preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function (array $m) use ($string, &$colors, &$labels) {
  191. if (trim($m[1]) === 'autolabel') {
  192. // Format the string as a label with consistent auto assigned background color
  193. if (!isset($labels[$string])) {
  194. $labels[$string] = $colors[count($labels) % count($colors)];
  195. }
  196. $color = $labels[$string];
  197. return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
  198. }
  199. return $m[1];
  200. }, $style);
  201. if (null === $style) {
  202. $pcreErrorCode = preg_last_error();
  203. throw new \RuntimeException('Failed to run preg_replace_callback: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
  204. }
  205. return $style;
  206. }
  207. /**
  208. * @param mixed[] $dict
  209. * @return mixed[]
  210. */
  211. private static function dump(string $title, array $dict): array
  212. {
  213. $script = [];
  214. $dict = array_filter($dict);
  215. if (empty($dict)) {
  216. return $script;
  217. }
  218. $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title));
  219. foreach ($dict as $key => $value) {
  220. $value = json_encode($value);
  221. if (empty($value)) {
  222. $value = static::quote('');
  223. }
  224. $script[] = static::call('log', static::quote('%s: %o'), static::quote((string) $key), $value);
  225. }
  226. return $script;
  227. }
  228. private static function quote(string $arg): string
  229. {
  230. return '"' . addcslashes($arg, "\"\n\\") . '"';
  231. }
  232. /**
  233. * @param mixed $args
  234. */
  235. private static function call(...$args): string
  236. {
  237. $method = array_shift($args);
  238. if (!is_string($method)) {
  239. throw new \UnexpectedValueException('Expected the first arg to be a string, got: '.var_export($method, true));
  240. }
  241. return static::call_array($method, $args);
  242. }
  243. /**
  244. * @param mixed[] $args
  245. */
  246. private static function call_array(string $method, array $args): string
  247. {
  248. return 'c.' . $method . '(' . implode(', ', $args) . ');';
  249. }
  250. }