diff --git a/src/Monolog/Formatter/FluentdFormatter.php b/src/Monolog/Formatter/FluentdFormatter.php index f8df1850..29b14d30 100644 --- a/src/Monolog/Formatter/FluentdFormatter.php +++ b/src/Monolog/Formatter/FluentdFormatter.php @@ -11,6 +11,8 @@ namespace Monolog\Formatter; +use Monolog\Utils; + /** * Class FluentdFormatter * @@ -71,7 +73,7 @@ class FluentdFormatter implements FormatterInterface $message['level_name'] = $record['level_name']; } - return json_encode([$tag, $record['datetime']->getTimestamp(), $message]); + return Utils::jsonEncode([$tag, $record['datetime']->getTimestamp(), $message]); } public function formatBatch(array $records): string diff --git a/src/Monolog/Formatter/HtmlFormatter.php b/src/Monolog/Formatter/HtmlFormatter.php index 16a30329..d82dfb23 100644 --- a/src/Monolog/Formatter/HtmlFormatter.php +++ b/src/Monolog/Formatter/HtmlFormatter.php @@ -12,6 +12,7 @@ namespace Monolog\Formatter; use Monolog\Logger; +use Monolog\Utils; /** * Formats incoming records into an HTML table @@ -133,6 +134,6 @@ class HtmlFormatter extends NormalizerFormatter $data = $this->normalize($data); - return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return Utils::jsonEncode($data, JSON_PRETTY_PRINT | Utils::DEFAULT_JSON_FLAGS, true); } } diff --git a/src/Monolog/Formatter/LineFormatter.php b/src/Monolog/Formatter/LineFormatter.php index 8a7b390b..fa0c44bc 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -152,7 +152,7 @@ class LineFormatter extends NormalizerFormatter return (string) $data; } - return (string) $this->toJson($data, true); + return $this->toJson($data, true); } protected function replaceNewlines(string $str): string diff --git a/src/Monolog/Formatter/MongoDBFormatter.php b/src/Monolog/Formatter/MongoDBFormatter.php index d38c3ef1..9d41a505 100644 --- a/src/Monolog/Formatter/MongoDBFormatter.php +++ b/src/Monolog/Formatter/MongoDBFormatter.php @@ -94,7 +94,7 @@ class MongoDBFormatter implements FormatterInterface $formattedException = [ 'class' => Utils::getClass($exception), 'message' => $exception->getMessage(), - 'code' => $exception->getCode(), + 'code' => (int) $exception->getCode(), 'file' => $exception->getFile() . ':' . $exception->getLine(), ]; diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 9c42a735..fe75881b 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -28,7 +28,7 @@ class NormalizerFormatter implements FormatterInterface protected $maxNormalizeDepth = 9; protected $maxNormalizeItemCount = 1000; - private $jsonEncodeOptions = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION; + private $jsonEncodeOptions = Utils::DEFAULT_JSON_FLAGS; /** * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format @@ -189,7 +189,7 @@ class NormalizerFormatter implements FormatterInterface $data = [ 'class' => Utils::getClass($e), 'message' => $e->getMessage(), - 'code' => $e->getCode(), + 'code' => (int) $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), ]; @@ -202,8 +202,8 @@ class NormalizerFormatter implements FormatterInterface $data['faultactor'] = $e->faultactor; } - if (isset($e->detail)) { - $data['detail'] = $e->detail; + if (isset($e->detail) && (is_string($e->detail) || is_object($e->detail) || is_array($e->detail))) { + $data['detail'] = is_string($e->detail) ? $e->detail : reset($e->detail); } } @@ -226,130 +226,11 @@ class NormalizerFormatter implements FormatterInterface * * @param mixed $data * @throws \RuntimeException if encoding fails and errors are not ignored - * @return string|bool + * @return string if encoding fails and ignoreErrors is true 'null' is returned */ - protected function toJson($data, bool $ignoreErrors = false) + protected function toJson($data, bool $ignoreErrors = false): string { - // suppress json_encode errors since it's twitchy with some inputs - if ($ignoreErrors) { - return @$this->jsonEncode($data); - } - - $json = $this->jsonEncode($data); - - if ($json === false) { - $json = $this->handleJsonError(json_last_error(), $data); - } - - return $json; - } - - /** - * @param mixed $data - * @return string|bool JSON encoded data or false on failure - */ - private function jsonEncode($data) - { - return json_encode($data, $this->jsonEncodeOptions); - } - - /** - * Handle a json_encode failure. - * - * If the failure is due to invalid string encoding, try to clean the - * input and encode again. If the second encoding attempt fails, the - * initial error is not encoding related or the input can't be cleaned then - * raise a descriptive exception. - * - * @param int $code return code of json_last_error function - * @param mixed $data data that was meant to be encoded - * @throws \RuntimeException if failure can't be corrected - * @return string JSON encoded data after error correction - */ - private function handleJsonError(int $code, $data): string - { - if ($code !== JSON_ERROR_UTF8) { - $this->throwEncodeError($code, $data); - } - - if (is_string($data)) { - $this->detectAndCleanUtf8($data); - } elseif (is_array($data)) { - array_walk_recursive($data, [$this, 'detectAndCleanUtf8']); - } else { - $this->throwEncodeError($code, $data); - } - - $json = $this->jsonEncode($data); - - if ($json === false) { - $this->throwEncodeError(json_last_error(), $data); - } - - return $json; - } - - /** - * Throws an exception according to a given code with a customized message - * - * @param int $code return code of json_last_error function - * @param mixed $data data that was meant to be encoded - * @throws \RuntimeException - */ - private function throwEncodeError(int $code, $data) - { - switch ($code) { - case JSON_ERROR_DEPTH: - $msg = 'Maximum stack depth exceeded'; - break; - case JSON_ERROR_STATE_MISMATCH: - $msg = 'Underflow or the modes mismatch'; - break; - case JSON_ERROR_CTRL_CHAR: - $msg = 'Unexpected control character found'; - break; - case JSON_ERROR_UTF8: - $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; - break; - default: - $msg = 'Unknown error'; - } - - throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); - } - - /** - * Detect invalid UTF-8 string characters and convert to valid UTF-8. - * - * Valid UTF-8 input will be left unmodified, but strings containing - * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed - * original encoding of ISO-8859-15. This conversion may result in - * incorrect output if the actual encoding was not ISO-8859-15, but it - * will be clean UTF-8 output and will not rely on expensive and fragile - * detection algorithms. - * - * Function converts the input in place in the passed variable so that it - * can be used as a callback for array_walk_recursive. - * - * @param mixed &$data Input to check and convert if needed - * @private - */ - public function detectAndCleanUtf8(&$data) - { - if (is_string($data) && !preg_match('//u', $data)) { - $data = preg_replace_callback( - '/[\x80-\xFF]+/', - function ($m) { - return utf8_encode($m[0]); - }, - $data - ); - $data = str_replace( - ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'], - ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'], - $data - ); - } + return Utils::jsonEncode($data, $this->jsonEncodeOptions, $ignoreErrors); } protected function formatDate(\DateTimeInterface $date) diff --git a/src/Monolog/Handler/BrowserConsoleHandler.php b/src/Monolog/Handler/BrowserConsoleHandler.php index 04fb1268..310591c7 100644 --- a/src/Monolog/Handler/BrowserConsoleHandler.php +++ b/src/Monolog/Handler/BrowserConsoleHandler.php @@ -167,21 +167,22 @@ class BrowserConsoleHandler extends AbstractProcessingHandler private static function handleStyles(string $formatted): array { - $args = [static::quote('font-weight: normal')]; + $args = []; $format = '%c' . $formatted; preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); foreach (array_reverse($matches) as $match) { - $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0])); $args[] = '"font-weight: normal"'; + $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0])); $pos = $match[0][1]; $format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0])); } - array_unshift($args, static::quote($format)); + $args[] = static::quote('font-weight: normal'); + $args[] = static::quote($format); - return $args; + return array_reverse($args); } private static function handleCustomStyles(string $style, string $string): string diff --git a/src/Monolog/Handler/ChromePHPHandler.php b/src/Monolog/Handler/ChromePHPHandler.php index f391a90d..ef93f660 100644 --- a/src/Monolog/Handler/ChromePHPHandler.php +++ b/src/Monolog/Handler/ChromePHPHandler.php @@ -14,6 +14,7 @@ namespace Monolog\Handler; use Monolog\Formatter\ChromePHPFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; +use Monolog\Utils; /** * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/) @@ -144,7 +145,7 @@ class ChromePHPHandler extends AbstractProcessingHandler self::$json['request_uri'] = $_SERVER['REQUEST_URI'] ?? ''; } - $json = @json_encode(self::$json); + $json = Utils::jsonEncode(self::$json, null, true); $data = base64_encode(utf8_encode($json)); if (strlen($data) > 3 * 1024) { self::$overflowed = true; @@ -159,7 +160,7 @@ class ChromePHPHandler extends AbstractProcessingHandler 'extra' => [], ]; self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record); - $json = @json_encode(self::$json); + $json = Utils::jsonEncode(self::$json, null, true); $data = base64_encode(utf8_encode($json)); } diff --git a/src/Monolog/Handler/CubeHandler.php b/src/Monolog/Handler/CubeHandler.php index 298e572a..00d38e99 100644 --- a/src/Monolog/Handler/CubeHandler.php +++ b/src/Monolog/Handler/CubeHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\Logger; +use Monolog\Utils; /** * Logs to Cube. @@ -122,9 +123,9 @@ class CubeHandler extends AbstractProcessingHandler $data['data']['level'] = $record['level']; if ($this->scheme === 'http') { - $this->writeHttp(json_encode($data)); + $this->writeHttp(Utils::jsonEncode($data)); } else { - $this->writeUdp(json_encode($data)); + $this->writeUdp(Utils::jsonEncode($data)); } } diff --git a/src/Monolog/Handler/FlowdockHandler.php b/src/Monolog/Handler/FlowdockHandler.php index be46d032..062af418 100644 --- a/src/Monolog/Handler/FlowdockHandler.php +++ b/src/Monolog/Handler/FlowdockHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\Logger; +use Monolog\Utils; use Monolog\Formatter\FlowdockFormatter; use Monolog\Formatter\FormatterInterface; @@ -96,7 +97,7 @@ class FlowdockHandler extends SocketHandler */ private function buildContent(array $record): string { - return json_encode($record['formatted']['flowdock']); + return Utils::jsonEncode($record['formatted']['flowdock']); } /** diff --git a/src/Monolog/Handler/IFTTTHandler.php b/src/Monolog/Handler/IFTTTHandler.php index 21043c50..921c9f24 100644 --- a/src/Monolog/Handler/IFTTTHandler.php +++ b/src/Monolog/Handler/IFTTTHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\Logger; +use Monolog\Utils; /** * IFTTTHandler uses cURL to trigger IFTTT Maker actions @@ -53,7 +54,7 @@ class IFTTTHandler extends AbstractProcessingHandler "value2" => $record["level_name"], "value3" => $record["message"], ]; - $postString = json_encode($postData); + $postString = Utils::jsonEncode($postData); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://maker.ifttt.com/trigger/" . $this->eventName . "/with/key/" . $this->secretKey); diff --git a/src/Monolog/Handler/NewRelicHandler.php b/src/Monolog/Handler/NewRelicHandler.php index 5efd4e6a..177ad21a 100644 --- a/src/Monolog/Handler/NewRelicHandler.php +++ b/src/Monolog/Handler/NewRelicHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\Logger; +use Monolog\Utils; use Monolog\Formatter\NormalizerFormatter; use Monolog\Formatter\FormatterInterface; @@ -182,7 +183,7 @@ class NewRelicHandler extends AbstractProcessingHandler if (null === $value || is_scalar($value)) { newrelic_add_custom_parameter($key, $value); } else { - newrelic_add_custom_parameter($key, @json_encode($value)); + newrelic_add_custom_parameter($key, Utils::jsonEncode($value, null, true)); } } diff --git a/src/Monolog/Handler/PHPConsoleHandler.php b/src/Monolog/Handler/PHPConsoleHandler.php index 356e0748..aa94e613 100644 --- a/src/Monolog/Handler/PHPConsoleHandler.php +++ b/src/Monolog/Handler/PHPConsoleHandler.php @@ -14,6 +14,7 @@ namespace Monolog\Handler; use Monolog\Formatter\LineFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; +use Monolog\Utils; use PhpConsole\Connector; use PhpConsole\Handler as VendorPhpConsoleHandler; use PhpConsole\Helper; @@ -188,7 +189,7 @@ class PHPConsoleHandler extends AbstractProcessingHandler $tags = $this->getRecordTags($record); $message = $record['message']; if ($record['context']) { - $message .= ' ' . json_encode($this->connector->getDumper()->dump(array_filter($record['context']))); + $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($record['context'])), null, true); } $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']); } diff --git a/src/Monolog/Handler/Slack/SlackRecord.php b/src/Monolog/Handler/Slack/SlackRecord.php index c0821366..dbfcd982 100644 --- a/src/Monolog/Handler/Slack/SlackRecord.php +++ b/src/Monolog/Handler/Slack/SlackRecord.php @@ -12,6 +12,7 @@ namespace Monolog\Handler\Slack; use Monolog\Logger; +use Monolog\Utils; use Monolog\Formatter\NormalizerFormatter; use Monolog\Formatter\FormatterInterface; @@ -211,14 +212,13 @@ class SlackRecord public function stringify(array $fields): string { $normalized = $this->normalizerFormatter->format($fields); - $prettyPrintFlag = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 128; $hasSecondDimension = count(array_filter($normalized, 'is_array')); $hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric')); return $hasSecondDimension || $hasNonNumericKeys - ? json_encode($normalized, $prettyPrintFlag|JSON_UNESCAPED_UNICODE) - : json_encode($normalized, JSON_UNESCAPED_UNICODE); + ? Utils::jsonEncode($normalized, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) + : Utils::jsonEncode($normalized, JSON_UNESCAPED_UNICODE); } /** diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php index 8a6bfdc1..c2b93e61 100644 --- a/src/Monolog/Handler/SlackHandler.php +++ b/src/Monolog/Handler/SlackHandler.php @@ -13,6 +13,7 @@ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; +use Monolog\Utils; use Monolog\Handler\Slack\SlackRecord; /** @@ -115,7 +116,7 @@ class SlackHandler extends SocketHandler $dataArray['token'] = $this->token; if (!empty($dataArray['attachments'])) { - $dataArray['attachments'] = json_encode($dataArray['attachments']); + $dataArray['attachments'] = Utils::jsonEncode($dataArray['attachments']); } return $dataArray; diff --git a/src/Monolog/Handler/SlackWebhookHandler.php b/src/Monolog/Handler/SlackWebhookHandler.php index 9b19a2e1..17d12b06 100644 --- a/src/Monolog/Handler/SlackWebhookHandler.php +++ b/src/Monolog/Handler/SlackWebhookHandler.php @@ -13,6 +13,7 @@ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; +use Monolog\Utils; use Monolog\Handler\Slack\SlackRecord; /** @@ -92,7 +93,7 @@ class SlackWebhookHandler extends AbstractProcessingHandler protected function write(array $record): void { $postData = $this->slackRecord->getSlackData($record); - $postString = json_encode($postData); + $postString = Utils::jsonEncode($postData); $ch = curl_init(); $options = array( diff --git a/src/Monolog/Utils.php b/src/Monolog/Utils.php index c3890422..6af709c5 100644 --- a/src/Monolog/Utils.php +++ b/src/Monolog/Utils.php @@ -13,6 +13,8 @@ namespace Monolog; final class Utils { + const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION; + /** * @internal */ @@ -31,4 +33,135 @@ final class Utils return substr($string, $start, $length); } + + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string when errors are ignored and the encoding fails, "null" is returned which is valid json for null + */ + public static function jsonEncode($data, ?int $encodeFlags = null, bool $ignoreErrors = false): string + { + if (null === $encodeFlags) { + $encodeFlags = self::DEFAULT_JSON_FLAGS; + } + + $json = json_encode($data, $encodeFlags); + + if (false === $json) { + if ($ignoreErrors) { + return 'null'; + } + + $json = self::handleJsonError(json_last_error(), $data); + } + + return $json; + } + + /** + * Handle a json_encode failure. + * + * If the failure is due to invalid string encoding, try to clean the + * input and encode again. If the second encoding attempt fails, the + * inital error is not encoding related or the input can't be cleaned then + * raise a descriptive exception. + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION + * @throws \RuntimeException if failure can't be corrected + * @return string JSON encoded data after error correction + */ + public static function handleJsonError(int $code, $data, ?int $encodeFlags = null): string + { + if ($code !== JSON_ERROR_UTF8) { + self::throwEncodeError($code, $data); + } + + if (is_string($data)) { + self::detectAndCleanUtf8($data); + } elseif (is_array($data)) { + array_walk_recursive($data, array('Monolog\Utils', 'detectAndCleanUtf8')); + } else { + self::throwEncodeError($code, $data); + } + + if (null === $encodeFlags) { + $encodeFlags = self::DEFAULT_JSON_FLAGS; + } + + $json = json_encode($data, $encodeFlags); + + if ($json === false) { + self::throwEncodeError(json_last_error(), $data); + } + + return $json; + } + + /** + * Throws an exception according to a given code with a customized message + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @throws \RuntimeException + */ + private static function throwEncodeError(int $code, $data) + { + switch ($code) { + case JSON_ERROR_DEPTH: + $msg = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $msg = 'Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $msg = 'Unexpected control character found'; + break; + case JSON_ERROR_UTF8: + $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $msg = 'Unknown error'; + } + + throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); + } + + /** + * Detect invalid UTF-8 string characters and convert to valid UTF-8. + * + * Valid UTF-8 input will be left unmodified, but strings containing + * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed + * original encoding of ISO-8859-15. This conversion may result in + * incorrect output if the actual encoding was not ISO-8859-15, but it + * will be clean UTF-8 output and will not rely on expensive and fragile + * detection algorithms. + * + * Function converts the input in place in the passed variable so that it + * can be used as a callback for array_walk_recursive. + * + * @param mixed &$data Input to check and convert if needed + */ + private static function detectAndCleanUtf8(&$data) + { + if (is_string($data) && !preg_match('//u', $data)) { + $data = preg_replace_callback( + '/[\x80-\xFF]+/', + function ($m) { + return utf8_encode($m[0]); + }, + $data + ); + $data = str_replace( + ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'], + ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'], + $data + ); + } + } } diff --git a/tests/Monolog/Formatter/NormalizerFormatterTest.php b/tests/Monolog/Formatter/NormalizerFormatterTest.php index 34c5d4bd..d077d687 100644 --- a/tests/Monolog/Formatter/NormalizerFormatterTest.php +++ b/tests/Monolog/Formatter/NormalizerFormatterTest.php @@ -87,7 +87,7 @@ class NormalizerFormatterTest extends TestCase } $formatter = new NormalizerFormatter('Y-m-d'); - $e = new \SoapFault('foo', 'bar', 'hello', 'world'); + $e = new \SoapFault('foo', 'bar', 'hello', (object) ['foo' => 'world']); $formatted = $formatter->format([ 'exception' => $e, ]); @@ -188,7 +188,7 @@ class NormalizerFormatterTest extends TestCase restore_error_handler(); - $this->assertEquals(@json_encode([$foo, $bar]), $res); + $this->assertEquals('null', $res); } public function testCanNormalizeReferences() @@ -223,7 +223,7 @@ class NormalizerFormatterTest extends TestCase restore_error_handler(); - $this->assertEquals(@json_encode([$resource]), $res); + $this->assertEquals('null', $res); } public function testNormalizeHandleLargeArraysWithExactly1000Items() @@ -330,66 +330,6 @@ class NormalizerFormatterTest extends TestCase ); } - /** - * @param mixed $in Input - * @param mixed $expect Expected output - * @covers Monolog\Formatter\NormalizerFormatter::detectAndCleanUtf8 - * @dataProvider providesDetectAndCleanUtf8 - */ - public function testDetectAndCleanUtf8($in, $expect) - { - $formatter = new NormalizerFormatter(); - $formatter->detectAndCleanUtf8($in); - $this->assertSame($expect, $in); - } - - public function providesDetectAndCleanUtf8() - { - $obj = new \stdClass; - - return [ - 'null' => [null, null], - 'int' => [123, 123], - 'float' => [123.45, 123.45], - 'bool false' => [false, false], - 'bool true' => [true, true], - 'ascii string' => ['abcdef', 'abcdef'], - 'latin9 string' => ["\xB1\x31\xA4\xA6\xA8\xB4\xB8\xBC\xBD\xBE\xFF", '±1€ŠšŽžŒœŸÿ'], - 'unicode string' => ['¤¦¨´¸¼½¾€ŠšŽžŒœŸ', '¤¦¨´¸¼½¾€ŠšŽžŒœŸ'], - 'empty array' => [[], []], - 'array' => [['abcdef'], ['abcdef']], - 'object' => [$obj, $obj], - ]; - } - - /** - * @param int $code - * @param string $msg - * @dataProvider providesHandleJsonErrorFailure - */ - public function testHandleJsonErrorFailure($code, $msg) - { - $formatter = new NormalizerFormatter(); - $reflMethod = new \ReflectionMethod($formatter, 'handleJsonError'); - $reflMethod->setAccessible(true); - - $this->expectException('RuntimeException'); - $this->expectExceptionMessage($msg); - $reflMethod->invoke($formatter, $code, 'faked'); - } - - public function providesHandleJsonErrorFailure() - { - return [ - 'depth' => [JSON_ERROR_DEPTH, 'Maximum stack depth exceeded'], - 'state' => [JSON_ERROR_STATE_MISMATCH, 'Underflow or the modes mismatch'], - 'ctrl' => [JSON_ERROR_CTRL_CHAR, 'Unexpected control character found'], - 'default' => [-1, 'Unknown error'], - ]; - } - - // This happens i.e. in React promises or Guzzle streams where stream wrappers are registered - // and no file or line are included in the trace because it's treated as internal function public function testExceptionTraceWithArgs() { try { diff --git a/tests/Monolog/Handler/BrowserConsoleHandlerTest.php b/tests/Monolog/Handler/BrowserConsoleHandlerTest.php index e0cd6e3d..5bda6b00 100644 --- a/tests/Monolog/Handler/BrowserConsoleHandlerTest.php +++ b/tests/Monolog/Handler/BrowserConsoleHandlerTest.php @@ -48,6 +48,22 @@ EOF; $this->assertEquals($expected, $this->generateScript()); } + public function testStylingMultiple() + { + $handler = new BrowserConsoleHandler(); + $handler->setFormatter($this->getIdentityFormatter()); + + $handler->handle($this->getRecord(Logger::DEBUG, 'foo[[bar]]{color: red}[[baz]]{color: blue}')); + + $expected = <<assertEquals($expected, $this->generateScript()); + } + public function testEscaping() { $handler = new BrowserConsoleHandler(); diff --git a/tests/Monolog/UtilsTest.php b/tests/Monolog/UtilsTest.php new file mode 100644 index 00000000..529e88ea --- /dev/null +++ b/tests/Monolog/UtilsTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +class UtilsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @param int $code + * @param string $msg + * @dataProvider providesHandleJsonErrorFailure + */ + public function testHandleJsonErrorFailure($code, $msg) + { + $this->expectException('RuntimeException', $msg); + Utils::handleJsonError($code, 'faked'); + } + + public function providesHandleJsonErrorFailure() + { + return [ + 'depth' => [JSON_ERROR_DEPTH, 'Maximum stack depth exceeded'], + 'state' => [JSON_ERROR_STATE_MISMATCH, 'Underflow or the modes mismatch'], + 'ctrl' => [JSON_ERROR_CTRL_CHAR, 'Unexpected control character found'], + 'default' => [-1, 'Unknown error'], + ]; + } + + /** + * @param mixed $in Input + * @param mixed $expect Expected output + * @covers Monolog\Formatter\NormalizerFormatter::detectAndCleanUtf8 + * @dataProvider providesDetectAndCleanUtf8 + */ + public function testDetectAndCleanUtf8($in, $expect) + { + $reflMethod = new \ReflectionMethod(Utils::class, 'detectAndCleanUtf8'); + $reflMethod->setAccessible(true); + $args = [&$in]; + $reflMethod->invokeArgs(null, $args); + $this->assertSame($expect, $in); + } + + public function providesDetectAndCleanUtf8() + { + $obj = new \stdClass; + + return [ + 'null' => [null, null], + 'int' => [123, 123], + 'float' => [123.45, 123.45], + 'bool false' => [false, false], + 'bool true' => [true, true], + 'ascii string' => ['abcdef', 'abcdef'], + 'latin9 string' => ["\xB1\x31\xA4\xA6\xA8\xB4\xB8\xBC\xBD\xBE\xFF", '±1€ŠšŽžŒœŸÿ'], + 'unicode string' => ['¤¦¨´¸¼½¾€ŠšŽžŒœŸ', '¤¦¨´¸¼½¾€ŠšŽžŒœŸ'], + 'empty array' => [[], []], + 'array' => [['abcdef'], ['abcdef']], + 'object' => [$obj, $obj], + ]; + } +}