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) |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|
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.