Passed
Branch master (d01bfd)
by Delete
02:01
created

Decoder::prepareJson()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 21
rs 9.0534
1
<?php
2
namespace Crossjoin\Json;
3
4
use Crossjoin\Json\Exception\EncodingNotSupportedException;
5
use Crossjoin\Json\Exception\InvalidArgumentException;
6
use Crossjoin\Json\Exception\JsonException;
7
8
/**
9
 * Class Decoder
10
 *
11
 * @package Crossjoin\Json
12
 * @author Christoph Ziegenberg <[email protected]>
13
 */
14
class Decoder extends Converter
15
{
16
    /**
17
     * @var bool
18
     */
19
    private $ignoreByteOrderMark = true;
20
21
    /**
22
     * Decoder constructor.
23
     *
24
     * @param bool $ignoreByteOrderMark
25
     *
26
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
27
     */
28
    public function __construct($ignoreByteOrderMark = true)
29
    {
30
        $this->setIgnoreByteOrderMark($ignoreByteOrderMark);
31
    }
32
33
    /**
34
     * @return boolean
35
     */
36
    public function getIgnoreByteOrderMark()
37
    {
38
        return $this->ignoreByteOrderMark;
39
    }
40
41
    /**
42
     * @param boolean $ignoreByteOrderMark
43
     *
44
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
45
     */
46
    public function setIgnoreByteOrderMark($ignoreByteOrderMark)
47
    {
48
        // Check arguments
49
        if (is_bool($ignoreByteOrderMark)) {
50
            $this->ignoreByteOrderMark = $ignoreByteOrderMark;
51
        } else {
52
            throw InvalidArgumentException::getInstance(
53
                'boolean',
54
                'ignoreByteOrderMark',
55
                $ignoreByteOrderMark,
56
                1478195542
57
            );
58
        }
59
    }
60
61
    /**
62
     * Gets the encoding of the JSON text.
63
     *
64
     * @param string $json
65
     *
66
     * @return string
67
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
68
     * @throws \Crossjoin\Json\Exception\EncodingNotSupportedException
69
     */
70
    public function getEncoding($json)
71
    {
72
        // Check arguments
73
        if (!is_string($json)) {
74
            throw InvalidArgumentException::getInstance('string', 'json', $json, 1478195652);
75
        }
76
77
        // Get the first bytes
78
        $bytes = $this->getEncodingBytes($json);
79
80
        // Check encoding
81
        if (preg_match('/^(?:[^\x00]{1,3}$|[^\x00]{4})/', $bytes)) {
82
            // It's UTF-8 encoded JSON if you have...
83
            // - 1 byte and it's not NUL ("xx")
84
            // - 2 bytes and none of them are NUL ("xx xx")
85
            // - 3 bytes and they are not NUL ("xx xx xx")
86
            // - 4 or more bytes and the first 4 bytes are not NUL ("xx xx xx xx")
87
            //
88
            // BUT the check also matches UTF-8 ByteOrderMarks, which isn't allowed in JSON.
89
            // So we need to do an additional check (if ByteOrderMarks have not already been removed before)
90
            if ($this->ignoreByteOrderMark || !preg_match('/^\xEF\xBB\xBF/', $bytes)) {
91
                return self::UTF8;
92
            }
93
        } else if (preg_match('/^(?:\x00[^\x00]{1}$|\x00[^\x00]{1}.{2})/s', $bytes)) {
94
            // It's UTF-16BE encoded JSON if you have...
95
            // - 2 bytes and only the first is NUL ("00 xx")
96
            // - 4 or more bytes and only the first byte of the first 2 bytes is NUL ("00 xx")
97
            return self::UTF16BE;
98
        } else if (preg_match('/^(?:[^\x00]{1}\x00$|[^\x00]{1}\x00[^\x00]{1}.{1})/s', $bytes)) {
99
            // It's UTF-16LE encoded JSON if you have...
100
            // - 2 bytes and only the second is NUL ("xx 00")
101
            // - 4 or more bytes and only the second of the first 3 bytes is NUL ("xx 00 xx")
102
            return self::UTF16LE;
103
        } else if (preg_match('/^[\x00]{3}[^\x00]{1}/', $bytes)) {
104
            // It's UTF-32BE encoded JSON if you have...
105
            // - 4 or more bytes and only the first to third byte of the first 4 bytes are NUL ("00 00 00 xx")
106
            return self::UTF32BE;
107
        } else if (preg_match('/^[^\x00]{1}[\x00]{3}/', $bytes)) {
108
            // It's UTF-32LE encoded JSON if you have...
109
            // - 4 or more bytes and only the second to fourth byte of the first 4 bytes are NUL ("xx 00 00 00")
110
            return self::UTF32LE;
111
        }
112
113
        // No encoding found
114
        throw new EncodingNotSupportedException(
115
            'The JSON text is encoded with an unsupported encoding.',
116
            1478092834
117
        );
118
    }
119
120
    /** @noinspection MoreThanThreeArgumentsInspection */
121
    /**
122
     * Parses a valid JSON text that is encoded as UTF-8, UTF-16BE, UTF-16LE, UTF-32BE or UTF-32LE
123
     * and returns the data as UTF-8.
124
     *
125
     * @param string $json
126
     * @param bool $assoc
127
     * @param int $depth
128
     * @param int $options
129
     *
130
     * @return mixed
131
     * @throws \Crossjoin\Json\Exception\NativeJsonErrorException
132
     * @throws \Crossjoin\Json\Exception\ConversionFailedException
133
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
134
     * @throws \Crossjoin\Json\Exception\EncodingNotSupportedException
135
     * @throws \Crossjoin\Json\Exception\ExtensionRequiredException
136
     */
137
    public function decode($json, $assoc = false, $depth = 512, $options = 0)
138
    {
139
        // Check arguments
140
        if (!is_string($json)) {
141
            throw InvalidArgumentException::getInstance('string', 'json', $json, 1478418105);
142
        } elseif (!is_bool($assoc)) {
143
            throw InvalidArgumentException::getInstance('boolean', 'assoc', $assoc, 1478418106);
144
        } elseif (!is_int($depth)) {
145
            throw InvalidArgumentException::getInstance('integer', 'depth', $assoc, 1478418107);
146
        } elseif (!is_int($options)) {
147
            throw InvalidArgumentException::getInstance('integer', 'options', $options, 1478418108);
148
        }
149
150
        // Prepare JSON data (remove BOMs and convert encoding)
151
        $json = $this->prepareJson($json);
152
153
        // Try to decode the json text
154
        // @codeCoverageIgnoreStart
155
        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
156
            return $this->decodePhpGte54($json, $assoc, $depth, $options);
157
        } else {
158
            return $this->decodePhpLt54($json, $assoc, $depth);
159
        }
160
        // @codeCoverageIgnoreEnd
161
    }
162
163
    /** @noinspection MoreThanThreeArgumentsInspection */
164
    /**
165
     * @param string $json
166
     * @param bool $assoc
167
     * @param int $depth
168
     * @param int $options
169
     *
170
     * @return mixed
171
     * @throws \Crossjoin\Json\Exception\NativeJsonErrorException
172
     */
173 View Code Duplication
    private function decodePhpGte54($json, $assoc, $depth, $options)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
    {
175
        $data = \json_decode($json, $assoc, $depth, $options);
176
177
        if (\json_last_error() !== \JSON_ERROR_NONE) {
178
            throw $this->getNativeJsonErrorException();
179
        }
180
181
        return $data;
182
    }
183
184
    /**
185
     * @param string $json
186
     * @param bool $assoc
187
     * @param int $depth
188
     *
189
     * @return mixed
190
     * @throws \Crossjoin\Json\Exception\NativeJsonErrorException
191
     */
192 View Code Duplication
    private function decodePhpLt54($json, $assoc, $depth)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
193
    {
194
        $data = \json_decode($json, $assoc, $depth);
195
196
        if (\json_last_error() !== \JSON_ERROR_NONE) {
197
            throw $this->getNativeJsonErrorException();
198
        }
199
200
        return $data;
201
    }
202
203
    /**
204
     * @param string $json
205
     *
206
     * @return string
207
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
208
     */
209
    private function getEncodingBytes($json)
210
    {
211
        // Do not use str_* function here because of possible mb_str_* overloading
212
        preg_match('/^(.{0,8})/s', $json, $matches);
213
        $bytes = array_key_exists(1, $matches) ? $matches[1] : '';
214
215
        // Remove byte order marks
216
        if ($this->ignoreByteOrderMark && $bytes !== '') {
217
            $bytes = $this->removeByteOrderMark($bytes);
218
        }
219
220
        return $bytes;
221
    }
222
223
    /**
224
     * @param string $json
225
     *
226
     * @return string
227
     */
228
    private function prepareJson($json)
229
    {
230
        try {
231
            // Ignore empty string
232
            // (will cause a parsing error in the native json_decode function)
233
            if ($json !== '') {
234
                // Remove byte order marks
235
                if ($this->ignoreByteOrderMark) {
236
                    $json = $this->removeByteOrderMark($json);
237
                }
238
239
                // Convert encoding to UTF-8
240
                $json = $this->convertEncoding($json, $this->getEncoding($json), self::UTF8);
241
            }
242
        } catch (JsonException $e) {
243
            // Ignore exception here, so that the native json_decode function
244
            // is called by the decode() method and we get the native error message.
245
        }
246
247
        return $json;
248
    }
249
}
250