diff --git a/core/lib/Drupal/Core/Logger/LogMessageParser.php b/core/lib/Drupal/Core/Logger/LogMessageParser.php index 8a67df12dc..0beadf7fc0 100644 --- a/core/lib/Drupal/Core/Logger/LogMessageParser.php +++ b/core/lib/Drupal/Core/Logger/LogMessageParser.php @@ -2,6 +2,8 @@ namespace Drupal\Core\Logger; +use Psr\Log\InvalidArgumentException; + /** * Parses log messages and their placeholders. */ @@ -20,6 +22,11 @@ public function parseMessagePlaceholders(&$message, array &$context) { $message = preg_replace('/\{(.*)\}/U', '@$1', $message); } foreach ($context as $key => $variable) { + + if (!$this->placeholderIsStringable($variable)) { + throw new InvalidArgumentException('Log context contains non-string value for key "' . $key . '"'); + } + // PSR3 style placeholders. if ($has_psr3) { // Keys are not prefixed with anything according to PSR3 specs. @@ -29,13 +36,35 @@ public function parseMessagePlaceholders(&$message, array &$context) { $key = '@' . $key; } } - if (!empty($key) && ($key[0] === '@' || $key[0] === '%' || $key[0] === '!')) { + + if (!empty($key) && ($key[0] === '@' || $key[0] === '%' || $key[0] === ':')) { // The key is now in \Drupal\Component\Utility\SafeMarkup::format() style. - $variables[$key] = $variable; + $variables[$key] = (string) $variable; } } return $variables; } + /** + * Check if a placeholder value can be converted into a string. + * + * @param array &$context + * The context to be logged. + * + * @return bool + * Returns TRUE if the placeholder can be converted into a string. + */ + protected function placeholderIsStringable($placeholder) { + if (is_resource($placeholder)) { + return FALSE; + } + + if (is_object($placeholder) && method_exists($placeholder, '__toString')) { + return TRUE; + } + + return is_null($placeholder) || is_scalar($placeholder); + } + } diff --git a/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php b/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php index 395517f51f..71712a0bb7 100644 --- a/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php +++ b/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php @@ -4,6 +4,23 @@ use Drupal\Core\Logger\LogMessageParser; use Drupal\Tests\UnitTestCase; +use Psr\Log\InvalidArgumentException; + +/** + * A class that isn't string convertible. + */ +class NotStringConvertible { } + +/** + * A class that is string convertible. + */ +class StringConvertible { + + public function __toString() { + return 'convertible'; + } + +} /** * @coversDefaultClass \Drupal\Core\Logger\LogMessageParser @@ -22,15 +39,25 @@ class LogMessageParserTest extends UnitTestCase { * An array with the expected values after the test has run. * - message: The expected parsed message. * - context: The expected values of the placeholders. + * @param bool $stringable_placeholders + * A boolean flag indicating if the placeholders should be convertible + * into strings. * * @dataProvider providerTestParseMessagePlaceholders * @covers ::parseMessagePlaceholders */ - public function testParseMessagePlaceholders(array $value, array $expected) { + public function testParseMessagePlaceholders(array $value, array $expected, $stringable_placeholders) { $parser = new LogMessageParser(); - $message_placeholders = $parser->parseMessagePlaceholders($value['message'], $value['context']); - $this->assertEquals($expected['message'], $value['message']); - $this->assertEquals($expected['context'], $message_placeholders); + try { + $message_placeholders = $parser->parseMessagePlaceholders($value['message'], $value['context']); + $this->assertEquals($expected['message'], $value['message']); + $this->assertEquals($expected['context'], $message_placeholders); + $this->assertTrue($stringable_placeholders); + } + catch (InvalidArgumentException $e) { + $this->assertFalse($stringable_placeholders); + $this->assertStringStartsWith('Log context contains non-string value for key', $e->getMessage()); + } } /** @@ -42,26 +69,73 @@ public function providerTestParseMessagePlaceholders() { [ ['message' => 'User {username} created', 'context' => ['username' => 'Dries']], ['message' => 'User @username created', 'context' => ['@username' => 'Dries']], + TRUE, ], // PSR3 style mixed in a format_string style message. [ ['message' => 'User {username} created @time', 'context' => ['username' => 'Dries', '@time' => 'now']], ['message' => 'User @username created @time', 'context' => ['@username' => 'Dries', '@time' => 'now']], + TRUE, ], // format_string style message only. [ ['message' => 'User @username created', 'context' => ['@username' => 'Dries']], ['message' => 'User @username created', 'context' => ['@username' => 'Dries']], + TRUE, + ], + // format_string style message only. + [ + ['message' => 'Example :url', 'context' => [':url' => 'http://example.com']], + ['message' => 'Example :url', 'context' => [':url' => 'http://example.com']], + TRUE, ], // Message without placeholders but wildcard characters. [ ['message' => 'User W-\\};~{&! created @', 'context' => ['' => '']], ['message' => 'User W-\\};~{&! created @', 'context' => []], + TRUE, ], // Message with double PSR3 style messages. [ ['message' => 'Test {with} two {encapsuled} strings', 'context' => ['with' => 'together', 'encapsuled' => 'awesome']], ['message' => 'Test @with two @encapsuled strings', 'context' => ['@with' => 'together', '@encapsuled' => 'awesome']], + TRUE, + ], + // Placeholders not convertible into a string. + [ + ['message' => 'array @a', 'context' => ['@a' => []]], + ['message' => 'array @a', 'context' => ['@a' => []]], + FALSE, + ], + // Placeholders not convertible into a string. + [ + ['message' => 'object @b', 'context' => ['@b' => new StringConvertible()]], + ['message' => 'object @b', 'context' => ['@b' => 'convertible']], + TRUE, + ], + // Placeholders not convertible into a string. + [ + ['message' => 'object @b', 'context' => ['@b' => new NotStringConvertible()]], + ['message' => 'object @b', 'context' => ['@b' => new NotStringConvertible()]], + FALSE, + ], + // Closures are not convertible into a string. + [ + ['message' => 'closure @c', 'context' => ['@c' => function() {} ]], + ['message' => 'closure @c', 'context' => ['@c' => function() {} ]], + FALSE, + ], + // Resources cannot be converted into a string. + [ + ['message' => 'resource @r', 'context' => ['@r' => fopen('php://memory', 'r+') ]], + ['message' => 'resource @r', 'context' => ['@r' => fopen('php://memory', 'r+') ]], + FALSE, + ], + // Placeholders not converible into strings that are not the first placeholder. + [ + ['message' => 'mixed @a @b @c', 'context' => ['@a' => 123, '@b' => [1], '@c' => TRUE]], + ['message' => 'mixed @a @b @c', 'context' => ['@a' => 123, '@b' => [1], '@c' => TRUE]], + FALSE, ], ]; }