1<?php
2/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
3
4namespace Icinga\Util;
5
6use Icinga\Exception\Json\JsonDecodeException;
7use Icinga\Exception\Json\JsonEncodeException;
8
9/**
10 * Wrap {@link json_encode()} and {@link json_decode()} with error handling
11 */
12class Json
13{
14    /**
15     * {@link json_encode()} wrapper
16     *
17     * @param   mixed   $value
18     * @param   int     $options
19     * @param   int     $depth
20     *
21     * @return  string
22     * @throws  JsonEncodeException
23     */
24    public static function encode($value, $options = 0, $depth = 512)
25    {
26        return static::encodeAndSanitize($value, $options, $depth, false);
27    }
28
29    /**
30     * {@link json_encode()} wrapper, automatically sanitizes bad UTF-8
31     *
32     * @param   mixed   $value
33     * @param   int     $options
34     * @param   int     $depth
35     *
36     * @return  string
37     * @throws  JsonEncodeException
38     */
39    public static function sanitize($value, $options = 0, $depth = 512)
40    {
41        return static::encodeAndSanitize($value, $options, $depth, true);
42    }
43
44    /**
45     * {@link json_encode()} wrapper, sanitizes bad UTF-8
46     *
47     * @param   mixed   $value
48     * @param   int     $options
49     * @param   int     $depth
50     * @param   bool    $autoSanitize   Automatically sanitize invalid UTF-8 (if any)
51     *
52     * @return  string
53     * @throws  JsonEncodeException
54     */
55    protected static function encodeAndSanitize($value, $options, $depth, $autoSanitize)
56    {
57        $encoded = json_encode($value, $options, $depth);
58
59        switch (json_last_error()) {
60            case JSON_ERROR_NONE:
61                return $encoded;
62
63            /** @noinspection PhpMissingBreakStatementInspection */
64            case JSON_ERROR_UTF8:
65                if ($autoSanitize) {
66                    return static::encode(static::sanitizeUtf8Recursive($value), $options, $depth);
67                }
68                // Fallthrough
69
70            default:
71                throw new JsonEncodeException('%s: %s', json_last_error_msg(), var_export($value, true));
72        }
73    }
74
75    /**
76     * {@link json_decode()} wrapper
77     *
78     * @param   string  $json
79     * @param   bool    $assoc
80     * @param   int     $depth
81     * @param   int     $options
82     *
83     * @return  mixed
84     * @throws  JsonDecodeException
85     */
86    public static function decode($json, $assoc = false, $depth = 512, $options = 0)
87    {
88        $decoded = json_decode($json, $assoc, $depth, $options);
89
90        if (json_last_error() !== JSON_ERROR_NONE) {
91            throw new JsonDecodeException('%s: %s', json_last_error_msg(), var_export($json, true));
92        }
93        return $decoded;
94    }
95
96    /**
97     * Replace bad byte sequences in UTF-8 strings inside the given JSON-encodable structure with question marks
98     *
99     * @param   mixed   $value
100     *
101     * @return  mixed
102     */
103    protected static function sanitizeUtf8Recursive($value)
104    {
105        switch (gettype($value)) {
106            case 'string':
107                return static::sanitizeUtf8String($value);
108
109            case 'array':
110                $sanitized = array();
111
112                foreach ($value as $key => $val) {
113                    if (is_string($key)) {
114                        $key = static::sanitizeUtf8String($key);
115                    }
116
117                    $sanitized[$key] = static::sanitizeUtf8Recursive($val);
118                }
119
120                return $sanitized;
121
122            case 'object':
123                $sanitized = array();
124
125                foreach ($value as $key => $val) {
126                    if (is_string($key)) {
127                        $key = static::sanitizeUtf8String($key);
128                    }
129
130                    $sanitized[$key] = static::sanitizeUtf8Recursive($val);
131                }
132
133                return (object) $sanitized;
134
135            default:
136                return $value;
137        }
138    }
139
140    /**
141     * Replace bad byte sequences in the given UTF-8 string with question marks
142     *
143     * @param   string  $string
144     *
145     * @return  string
146     */
147    protected static function sanitizeUtf8String($string)
148    {
149        return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
150    }
151}
152