scratch – Blame information for rev 117

Subversion Repositories:
Rev:
Rev Author Line No. Line
115 office 1 <?php
2  
3 /*
4 * This file is part of the Monolog package.
5 *
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
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 Monolog\Formatter;
13  
14 use Exception;
15  
16 /**
17 * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets
18 *
19 * @author Jordi Boggiano <j.boggiano@seld.be>
20 */
21 class NormalizerFormatter implements FormatterInterface
22 {
23 const SIMPLE_DATE = "Y-m-d H:i:s";
24  
25 protected $dateFormat;
26  
27 /**
28 * @param string $dateFormat The format of the timestamp: one supported by DateTime::format
29 */
30 public function __construct($dateFormat = null)
31 {
32 $this->dateFormat = $dateFormat ?: static::SIMPLE_DATE;
33 if (!function_exists('json_encode')) {
34 throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter');
35 }
36 }
37  
38 /**
39 * {@inheritdoc}
40 */
41 public function format(array $record)
42 {
43 return $this->normalize($record);
44 }
45  
46 /**
47 * {@inheritdoc}
48 */
49 public function formatBatch(array $records)
50 {
51 foreach ($records as $key => $record) {
52 $records[$key] = $this->format($record);
53 }
54  
55 return $records;
56 }
57  
58 protected function normalize($data)
59 {
60 if (null === $data || is_scalar($data)) {
61 if (is_float($data)) {
62 if (is_infinite($data)) {
63 return ($data > 0 ? '' : '-') . 'INF';
64 }
65 if (is_nan($data)) {
66 return 'NaN';
67 }
68 }
69  
70 return $data;
71 }
72  
73 if (is_array($data)) {
74 $normalized = array();
75  
76 $count = 1;
77 foreach ($data as $key => $value) {
78 if ($count++ >= 1000) {
79 $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization';
80 break;
81 }
82 $normalized[$key] = $this->normalize($value);
83 }
84  
85 return $normalized;
86 }
87  
88 if ($data instanceof \DateTime) {
89 return $data->format($this->dateFormat);
90 }
91  
92 if (is_object($data)) {
93 // TODO 2.0 only check for Throwable
94 if ($data instanceof Exception || (PHP_VERSION_ID > 70000 && $data instanceof \Throwable)) {
95 return $this->normalizeException($data);
96 }
97  
98 // non-serializable objects that implement __toString stringified
99 if (method_exists($data, '__toString') && !$data instanceof \JsonSerializable) {
100 $value = $data->__toString();
101 } else {
102 // the rest is json-serialized in some way
103 $value = $this->toJson($data, true);
104 }
105  
106 return sprintf("[object] (%s: %s)", get_class($data), $value);
107 }
108  
109 if (is_resource($data)) {
110 return sprintf('[resource] (%s)', get_resource_type($data));
111 }
112  
113 return '[unknown('.gettype($data).')]';
114 }
115  
116 protected function normalizeException($e)
117 {
118 // TODO 2.0 only check for Throwable
119 if (!$e instanceof Exception && !$e instanceof \Throwable) {
120 throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e));
121 }
122  
123 $data = array(
124 'class' => get_class($e),
125 'message' => $e->getMessage(),
126 'code' => $e->getCode(),
127 'file' => $e->getFile().':'.$e->getLine(),
128 );
129  
130 if ($e instanceof \SoapFault) {
131 if (isset($e->faultcode)) {
132 $data['faultcode'] = $e->faultcode;
133 }
134  
135 if (isset($e->faultactor)) {
136 $data['faultactor'] = $e->faultactor;
137 }
138  
139 if (isset($e->detail)) {
140 $data['detail'] = $e->detail;
141 }
142 }
143  
144 $trace = $e->getTrace();
145 foreach ($trace as $frame) {
146 if (isset($frame['file'])) {
147 $data['trace'][] = $frame['file'].':'.$frame['line'];
148 } elseif (isset($frame['function']) && $frame['function'] === '{closure}') {
149 // We should again normalize the frames, because it might contain invalid items
150 $data['trace'][] = $frame['function'];
151 } else {
152 // We should again normalize the frames, because it might contain invalid items
153 $data['trace'][] = $this->toJson($this->normalize($frame), true);
154 }
155 }
156  
157 if ($previous = $e->getPrevious()) {
158 $data['previous'] = $this->normalizeException($previous);
159 }
160  
161 return $data;
162 }
163  
164 /**
165 * Return the JSON representation of a value
166 *
167 * @param mixed $data
168 * @param bool $ignoreErrors
169 * @throws \RuntimeException if encoding fails and errors are not ignored
170 * @return string
171 */
172 protected function toJson($data, $ignoreErrors = false)
173 {
174 // suppress json_encode errors since it's twitchy with some inputs
175 if ($ignoreErrors) {
176 return @$this->jsonEncode($data);
177 }
178  
179 $json = $this->jsonEncode($data);
180  
181 if ($json === false) {
182 $json = $this->handleJsonError(json_last_error(), $data);
183 }
184  
185 return $json;
186 }
187  
188 /**
189 * @param mixed $data
190 * @return string JSON encoded data or null on failure
191 */
192 private function jsonEncode($data)
193 {
194 if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
195 return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
196 }
197  
198 return json_encode($data);
199 }
200  
201 /**
202 * Handle a json_encode failure.
203 *
204 * If the failure is due to invalid string encoding, try to clean the
205 * input and encode again. If the second encoding attempt fails, the
206 * inital error is not encoding related or the input can't be cleaned then
207 * raise a descriptive exception.
208 *
209 * @param int $code return code of json_last_error function
210 * @param mixed $data data that was meant to be encoded
211 * @throws \RuntimeException if failure can't be corrected
212 * @return string JSON encoded data after error correction
213 */
214 private function handleJsonError($code, $data)
215 {
216 if ($code !== JSON_ERROR_UTF8) {
217 $this->throwEncodeError($code, $data);
218 }
219  
220 if (is_string($data)) {
221 $this->detectAndCleanUtf8($data);
222 } elseif (is_array($data)) {
223 array_walk_recursive($data, array($this, 'detectAndCleanUtf8'));
224 } else {
225 $this->throwEncodeError($code, $data);
226 }
227  
228 $json = $this->jsonEncode($data);
229  
230 if ($json === false) {
231 $this->throwEncodeError(json_last_error(), $data);
232 }
233  
234 return $json;
235 }
236  
237 /**
238 * Throws an exception according to a given code with a customized message
239 *
240 * @param int $code return code of json_last_error function
241 * @param mixed $data data that was meant to be encoded
242 * @throws \RuntimeException
243 */
244 private function throwEncodeError($code, $data)
245 {
246 switch ($code) {
247 case JSON_ERROR_DEPTH:
248 $msg = 'Maximum stack depth exceeded';
249 break;
250 case JSON_ERROR_STATE_MISMATCH:
251 $msg = 'Underflow or the modes mismatch';
252 break;
253 case JSON_ERROR_CTRL_CHAR:
254 $msg = 'Unexpected control character found';
255 break;
256 case JSON_ERROR_UTF8:
257 $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
258 break;
259 default:
260 $msg = 'Unknown error';
261 }
262  
263 throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true));
264 }
265  
266 /**
267 * Detect invalid UTF-8 string characters and convert to valid UTF-8.
268 *
269 * Valid UTF-8 input will be left unmodified, but strings containing
270 * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed
271 * original encoding of ISO-8859-15. This conversion may result in
272 * incorrect output if the actual encoding was not ISO-8859-15, but it
273 * will be clean UTF-8 output and will not rely on expensive and fragile
274 * detection algorithms.
275 *
276 * Function converts the input in place in the passed variable so that it
277 * can be used as a callback for array_walk_recursive.
278 *
279 * @param mixed &$data Input to check and convert if needed
280 * @private
281 */
282 public function detectAndCleanUtf8(&$data)
283 {
284 if (is_string($data) && !preg_match('//u', $data)) {
285 $data = preg_replace_callback(
286 '/[\x80-\xFF]+/',
287 function ($m) { return utf8_encode($m[0]); },
288 $data
289 );
290 $data = str_replace(
291 array('¤', '¦', '¨', '´', '¸', '¼', '½', '¾'),
292 array('€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'),
293 $data
294 );
295 }
296 }
297 }