Issues (836)

framework/helpers/BaseJson.php (1 issue)

Severity
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\helpers;
9
10
use yii\base\Arrayable;
11
use yii\base\InvalidArgumentException;
12
use yii\base\Model;
13
use yii\web\JsExpression;
14
use yii\web\JsonResponseFormatter;
15
16
/**
17
 * BaseJson provides concrete implementation for [[Json]].
18
 *
19
 * Do not use BaseJson. Use [[Json]] instead.
20
 *
21
 * @author Qiang Xue <[email protected]>
22
 * @since 2.0
23
 */
24
class BaseJson
25
{
26
    /**
27
     * @var bool|null Enables human readable output a.k.a. Pretty Print.
28
     * This can useful for debugging during development but is not recommended in a production environment!
29
     * In case `prettyPrint` is `null` (default) the `options` passed to `encode` functions will not be changed.
30
     * @since 2.0.43
31
     */
32
    public static $prettyPrint;
33
    /**
34
     * @var bool Avoids objects with zero-indexed keys to be encoded as array
35
     * `Json::encode((object)['test'])` will be encoded as an object not as an array. This matches the behaviour of `json_encode()`.
36
     * Defaults to false to avoid any backwards compatibility issues.
37
     * Enable for single purpose: `Json::$keepObjectType = true;`
38
     * @see JsonResponseFormatter documentation to enable for all JSON responses
39
     * @since 2.0.44
40
     */
41
    public static $keepObjectType = false;
42
    /**
43
     * @var array List of JSON Error messages assigned to constant names for better handling of PHP <= 5.5.
44
     * @since 2.0.7
45
     */
46
    public static $jsonErrorMessages = [
47
        'JSON_ERROR_SYNTAX' => 'Syntax error',
48
        'JSON_ERROR_UNSUPPORTED_TYPE' => 'Type is not supported',
49
        'JSON_ERROR_DEPTH' => 'The maximum stack depth has been exceeded',
50
        'JSON_ERROR_STATE_MISMATCH' => 'Invalid or malformed JSON',
51
        'JSON_ERROR_CTRL_CHAR' => 'Control character error, possibly incorrectly encoded',
52
        'JSON_ERROR_UTF8' => 'Malformed UTF-8 characters, possibly incorrectly encoded',
53
    ];
54
55
56
    /**
57
     * Encodes the given value into a JSON string.
58
     *
59
     * The method enhances `json_encode()` by supporting JavaScript expressions.
60
     * In particular, the method will not encode a JavaScript expression that is
61
     * represented in terms of a [[JsExpression]] object.
62
     *
63
     * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
64
     * You must ensure strings passed to this method have proper encoding before passing them.
65
     *
66
     * @param mixed $value the data to be encoded.
67
     * @param int $options the encoding options. For more details please refer to
68
     * <https://www.php.net/manual/en/function.json-encode.php>. Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`.
69
     * @return string the encoding result.
70
     * @throws InvalidArgumentException if there is any encoding error.
71
     */
72 42
    public static function encode($value, $options = 320)
73
    {
74 42
        $expressions = [];
75 42
        $value = static::processData($value, $expressions, uniqid('', true));
76 42
        set_error_handler(function () {
77
            static::handleJsonError(JSON_ERROR_SYNTAX);
78 42
        }, E_WARNING);
79
80 42
        if (static::$prettyPrint === true) {
81 1
            $options |= JSON_PRETTY_PRINT;
82 42
        } elseif (static::$prettyPrint === false) {
83 1
            $options &= ~JSON_PRETTY_PRINT;
84
        }
85
86 42
        $json = json_encode($value, $options);
87 42
        restore_error_handler();
88 42
        static::handleJsonError(json_last_error());
89
90 42
        return $expressions === [] ? $json : strtr($json, $expressions);
91
    }
92
93
    /**
94
     * Encodes the given value into a JSON string HTML-escaping entities so it is safe to be embedded in HTML code.
95
     *
96
     * The method enhances `json_encode()` by supporting JavaScript expressions.
97
     * In particular, the method will not encode a JavaScript expression that is
98
     * represented in terms of a [[JsExpression]] object.
99
     *
100
     * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
101
     * You must ensure strings passed to this method have proper encoding before passing them.
102
     *
103
     * @param mixed $value the data to be encoded
104
     * @return string the encoding result
105
     * @since 2.0.4
106
     * @throws InvalidArgumentException if there is any encoding error
107
     */
108 13
    public static function htmlEncode($value)
109
    {
110 13
        return static::encode($value, JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);
111
    }
112
113
    /**
114
     * Decodes the given JSON string into a PHP data structure.
115
     * @param string $json the JSON string to be decoded
116
     * @param bool $asArray whether to return objects in terms of associative arrays.
117
     * @return mixed the PHP data
118
     * @throws InvalidArgumentException if there is any decoding error
119
     */
120 14
    public static function decode($json, $asArray = true)
121
    {
122 14
        if (is_array($json)) {
0 ignored issues
show
The condition is_array($json) is always false.
Loading history...
123 1
            throw new InvalidArgumentException('Invalid JSON data.');
124 13
        } elseif ($json === null || $json === '') {
125 1
            return null;
126
        }
127 13
        $decode = json_decode((string) $json, $asArray);
128 13
        static::handleJsonError(json_last_error());
129
130 12
        return $decode;
131
    }
132
133
    /**
134
     * Handles [[encode()]] and [[decode()]] errors by throwing exceptions with the respective error message.
135
     *
136
     * @param int $lastError error code from [json_last_error()](https://www.php.net/manual/en/function.json-last-error.php).
137
     * @throws InvalidArgumentException if there is any encoding/decoding error.
138
     * @since 2.0.6
139
     */
140 52
    protected static function handleJsonError($lastError)
141
    {
142 52
        if ($lastError === JSON_ERROR_NONE) {
143 52
            return;
144
        }
145
146 1
        if (PHP_VERSION_ID >= 50500) {
147 1
            throw new InvalidArgumentException(json_last_error_msg(), $lastError);
148
        }
149
150
        foreach (static::$jsonErrorMessages as $const => $message) {
151
            if (defined($const) && constant($const) === $lastError) {
152
                throw new InvalidArgumentException($message, $lastError);
153
            }
154
        }
155
156
        throw new InvalidArgumentException('Unknown JSON encoding/decoding error.');
157
    }
158
159
    /**
160
     * Pre-processes the data before sending it to `json_encode()`.
161
     * @param mixed $data the data to be processed
162
     * @param array $expressions collection of JavaScript expressions
163
     * @param string $expPrefix a prefix internally used to handle JS expressions
164
     * @return mixed the processed data
165
     */
166 40
    protected static function processData($data, &$expressions, $expPrefix)
167
    {
168 40
        $revertToObject = false;
169
170 40
        if (is_object($data)) {
171 11
            if ($data instanceof JsExpression) {
172 3
                $token = "!{[$expPrefix=" . count($expressions) . ']}!';
173 3
                $expressions['"' . $token . '"'] = $data->expression;
174
175 3
                return $token;
176
            }
177
178 10
            if ($data instanceof \JsonSerializable) {
179 2
                return static::processData($data->jsonSerialize(), $expressions, $expPrefix);
180
            }
181
182 10
            if ($data instanceof \DateTimeInterface) {
183
                return static::processData((array)$data, $expressions, $expPrefix);
184
            }
185
186 10
            if ($data instanceof Arrayable) {
187 2
                $data = $data->toArray();
188 9
            } elseif ($data instanceof \Generator) {
189 1
                $_data = [];
190 1
                foreach ($data as $name => $value) {
191 1
                    $_data[$name] = static::processData($value, $expressions, $expPrefix);
192
                }
193 1
                $data = $_data;
194 9
            } elseif ($data instanceof \SimpleXMLElement) {
195 1
                $data = (array) $data;
196
197
                // Avoid empty elements to be returned as array.
198
                // Not breaking BC because empty array was always cast to stdClass before.
199 1
                $revertToObject = true;
200
            } else {
201
                /*
202
                 * $data type is changed to array here and its elements will be processed further
203
                 * We must cast $data back to object later to keep intended dictionary type in JSON.
204
                 * Revert is only done when keepObjectType flag is provided to avoid breaking BC
205
                 */
206 9
                $revertToObject = static::$keepObjectType;
207
208 9
                $result = [];
209 9
                foreach ($data as $name => $value) {
210 9
                    $result[$name] = $value;
211
                }
212 9
                $data = $result;
213
214
                // Avoid empty objects to be returned as array (would break BC without keepObjectType flag)
215 9
                if ($data === []) {
216 2
                    $revertToObject = true;
217
                }
218
            }
219
        }
220
221 40
        if (is_array($data)) {
222 36
            foreach ($data as $key => $value) {
223 33
                if (is_array($value) || is_object($value)) {
224 10
                    $data[$key] = static::processData($value, $expressions, $expPrefix);
225
                }
226
            }
227
        }
228
229 40
        return $revertToObject ? (object) $data : $data;
230
    }
231
232
    /**
233
     * Generates a summary of the validation errors.
234
     *
235
     * @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
236
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
237
     *
238
     * - showAllErrors: boolean, if set to true every error message for each attribute will be shown otherwise
239
     *   only the first error message for each attribute will be shown. Defaults to `false`.
240
     *
241
     * @return string the generated error summary
242
     * @since 2.0.14
243
     */
244 1
    public static function errorSummary($models, $options = [])
245
    {
246 1
        $showAllErrors = ArrayHelper::remove($options, 'showAllErrors', false);
247 1
        $lines = self::collectErrors($models, $showAllErrors);
248
249 1
        return static::encode($lines);
250
    }
251
252
    /**
253
     * Return array of the validation errors.
254
     *
255
     * @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
256
     * @param bool $showAllErrors if set to true every error message for each attribute will be shown otherwise
257
     * only the first error message for each attribute will be shown.
258
     * @return array of the validation errors
259
     * @since 2.0.14
260
     */
261 1
    private static function collectErrors($models, $showAllErrors)
262
    {
263 1
        $lines = [];
264
265 1
        if (!is_array($models)) {
266 1
            $models = [$models];
267
        }
268 1
        foreach ($models as $model) {
269 1
            $lines[] = $model->getErrorSummary($showAllErrors);
270
        }
271
272 1
        return array_unique(call_user_func_array('array_merge', $lines));
273
    }
274
}
275