Passed
Push — master ( f25e4d...a2be21 )
by Benjamin
03:57 queued 01:51
created

JsonEncoder::handleJsonError()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
c 1
b 0
f 0
nc 12
nop 2
dl 0
loc 17
ccs 0
cts 12
cp 0
crap 30
rs 9.5555
1
<?php
2
3
/*
4
 * This file is part of the php-gelf package.
5
 *
6
 * (c) Benjamin Zikarsky <http://benjamin-zikarsky.de>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Gelf\Encoder;
13
14
use Gelf\MessageInterface;
15
16
/**
17
 * The JsonEncoder allows the encoding of GELF messages as described
18
 * in http://www.graylog2.org/resources/documentation/sending/gelfhttp
19
 *
20
 * @author Benjamin Zikarsky <[email protected]>
21
 */
22
class JsonEncoder implements NoNullByteEncoderInterface
23
{
24
25
    /**
26
     * Encodes a given message
27
     *
28
     * @param MessageInterface $message
29
     * @return string
30
     */
31 3
    public function encode(MessageInterface $message)
32
    {
33 3
        return $this->toJson($message->toArray());
34
    }
35
36
    /**
37
     * Return the JSON representation of a value
38
     *
39
     * @param mixed $data
40
     * @return string
41
     * @throws \RuntimeException if encoding fails and errors are not ignored
42
     */
43 3
    protected function toJson($data)
44
    {
45 3
        $json = $this->jsonEncode($data);
46 3
        if ($json === false) {
47
            $json = $this->handleJsonError(json_last_error(), $data);
48
        }
49 3
        return $json;
50
    }
51
52
    /**
53
     * @param mixed $data
54
     * @return string JSON encoded data or null on failure
55
     */
56 3
    private function jsonEncode($data)
57
    {
58 3
        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
59 3
            return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
60
        }
61
        return json_encode($data);
62
    }
63
64
    /**
65
     * Handle a json_encode failure.
66
     *
67
     * If the failure is due to invalid string encoding, try to clean the
68
     * input and encode again. If the second encoding attempt fails, the
69
     * inital error is not encoding related or the input can't be cleaned then
70
     * raise a descriptive exception.
71
     *
72
     * @param int $code return code of json_last_error function
73
     * @param mixed $data data that was meant to be encoded
74
     * @return string            JSON encoded data after error correction
75
     * @throws \RuntimeException if failure can't be corrected
76
     */
77
    private function handleJsonError($code, $data)
78
    {
79
        if ($code !== JSON_ERROR_UTF8) {
80
            $this->throwEncodeError($code, $data);
81
        }
82
        if (is_string($data)) {
83
            $this->detectAndCleanUtf8($data);
84
        } elseif (is_array($data)) {
85
            array_walk_recursive($data, array($this, 'detectAndCleanUtf8'));
86
        } else {
87
            $this->throwEncodeError($code, $data);
88
        }
89
        $json = $this->jsonEncode($data);
90
        if ($json === false) {
91
            $this->throwEncodeError(json_last_error(), $data);
92
        }
93
        return $json;
94
    }
95
96
    /**
97
     * Throws an exception according to a given code with a customized message
98
     *
99
     * @param int $code return code of json_last_error function
100
     * @param mixed $data data that was meant to be encoded
101
     * @throws \RuntimeException
102
     */
103
    private function throwEncodeError($code, $data)
104
    {
105
        switch ($code) {
106
            case JSON_ERROR_DEPTH:
107
                $msg = 'Maximum stack depth exceeded';
108
                break;
109
            case JSON_ERROR_STATE_MISMATCH:
110
                $msg = 'Underflow or the modes mismatch';
111
                break;
112
            case JSON_ERROR_CTRL_CHAR:
113
                $msg = 'Unexpected control character found';
114
                break;
115
            case JSON_ERROR_UTF8:
116
                $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
117
                break;
118
            default:
119
                $msg = 'Unknown error';
120
        }
121
        throw new \RuntimeException('JSON encoding failed: ' . $msg . '. Encoding: ' . var_export($data, true));
122
    }
123
124
    /**
125
     * Detect invalid UTF-8 string characters and convert to valid UTF-8.
126
     *
127
     * Valid UTF-8 input will be left unmodified, but strings containing
128
     * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed
129
     * original encoding of ISO-8859-15. This conversion may result in
130
     * incorrect output if the actual encoding was not ISO-8859-15, but it
131
     * will be clean UTF-8 output and will not rely on expensive and fragile
132
     * detection algorithms.
133
     *
134
     * Function converts the input in place in the passed variable so that it
135
     * can be used as a callback for array_walk_recursive.
136
     *
137
     * @param mixed &$data Input to check and convert if needed
138
     * @private
139
     */
140
    public function detectAndCleanUtf8(&$data)
141
    {
142
        if (is_string($data) && !preg_match('//u', $data)) {
143
            $data = preg_replace_callback(
144
                '/[\x80-\xFF]+/',
145
                function ($m) {
146
                    return utf8_encode($m[0]);
147
                },
148
                $data
149
            );
150
            $data = str_replace(
151
                array('¤', '¦', '¨', '´', '¸', '¼', '½', '¾'),
152
                array('€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'),
153
                $data
154
            );
155
        }
156
    }
157
}
158