1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace kdn\yii2; |
4
|
|
|
|
5
|
|
|
use kdn\yii2\assets\JsonEditorFullAsset; |
6
|
|
|
use kdn\yii2\assets\JsonEditorMinimalistAsset; |
7
|
|
|
use yii\helpers\ArrayHelper; |
8
|
|
|
use yii\helpers\Html; |
9
|
|
|
use yii\helpers\Inflector; |
10
|
|
|
use yii\helpers\Json; |
11
|
|
|
use yii\web\JsExpression; |
12
|
|
|
use yii\widgets\InputWidget; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Class JsonEditor. |
16
|
|
|
* @package kdn\yii2 |
17
|
|
|
*/ |
18
|
|
|
class JsonEditor extends InputWidget |
19
|
|
|
{ |
20
|
|
|
/** |
21
|
|
|
* @var array options which will be passed to JSON editor |
22
|
|
|
* @see https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options |
23
|
|
|
*/ |
24
|
|
|
public $clientOptions = []; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var string[] list of JSON editor modes for which all fields should be collapsed automatically; |
28
|
|
|
* allowed modes 'tree', 'view', and 'form' |
29
|
|
|
* @see https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#jsoneditorcollapseall |
30
|
|
|
*/ |
31
|
|
|
public $collapseAll = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var array HTML attributes to be applied to the JSON editor container tag |
35
|
|
|
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered |
36
|
|
|
*/ |
37
|
|
|
public $containerOptions = []; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var mixed this property can be used instead of `value`; |
41
|
|
|
* while `value` must be JSON string, `decodedValue` accepts decoded JSON, i.e. arrays, floats, booleans etc.; |
42
|
|
|
* `decodedValue` has precedence over `value`: if `decodedValue` is set then `value` will be ignored |
43
|
|
|
* @see value |
44
|
|
|
*/ |
45
|
|
|
public $decodedValue; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var string default value |
49
|
|
|
*/ |
50
|
|
|
public $defaultValue = '{}'; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var string[] list of JSON editor modes for which all fields should be expanded automatically; |
54
|
|
|
* allowed modes 'tree', 'view', and 'form' |
55
|
|
|
* @see https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#jsoneditorexpandall |
56
|
|
|
*/ |
57
|
|
|
public $expandAll = []; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var null|bool whether to use minimalist version of JSON editor; |
61
|
|
|
* note that "minimalist" is not the same as "minimized"; |
62
|
|
|
* if property is not set then extension will try to determine automatically whether full version is needed, |
63
|
|
|
* if full version is not required then minimalist version will be used; |
64
|
|
|
* you can explicitly set this property to true or false if automatic detection does not fit for you application |
65
|
|
|
* @see https://github.com/josdejong/jsoneditor/blob/master/src/docs/which%20files%20do%20I%20need.md |
66
|
|
|
*/ |
67
|
|
|
public $minimalist; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var string[] list of client options which should be automatically converted to `JsExpression` |
71
|
|
|
* @see clientOptions |
72
|
|
|
*/ |
73
|
|
|
protected $jsExpressionClientOptions = [ |
74
|
|
|
'ace', |
75
|
|
|
'ajv', |
76
|
|
|
'autocomplete', |
77
|
|
|
'createQuery', |
78
|
|
|
'executeQuery', |
79
|
|
|
'languages', |
80
|
|
|
'modalAnchor', |
81
|
|
|
'onBlur', |
82
|
|
|
'onChange', |
83
|
|
|
'onChangeJSON', |
84
|
|
|
'onChangeText', |
85
|
|
|
'onClassName', |
86
|
|
|
'onColorPicker', |
87
|
|
|
'onCreateMenu', |
88
|
|
|
'onEditable', |
89
|
|
|
'onError', |
90
|
|
|
'onEvent', |
91
|
|
|
'onFocus', |
92
|
|
|
'onModeChange', |
93
|
|
|
'onNodeName', |
94
|
|
|
'onSelectionChange', |
95
|
|
|
'onTextSelectionChange', |
96
|
|
|
'onValidate', |
97
|
|
|
'onValidationError', |
98
|
|
|
'popupAnchor', |
99
|
|
|
'schema', |
100
|
|
|
'schemaRefs', |
101
|
|
|
'templates', |
102
|
|
|
'timestampFormat', |
103
|
|
|
'timestampTag', |
104
|
|
|
]; |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @var string default JSON editor mode |
108
|
|
|
*/ |
109
|
|
|
private $mode = 'tree'; |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @var string[] available JSON editor modes |
113
|
|
|
*/ |
114
|
|
|
private $modes = []; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* {@inheritdoc} |
118
|
|
|
*/ |
119
|
|
|
public function init() |
120
|
|
|
{ |
121
|
|
|
parent::init(); |
122
|
|
|
if (!isset($this->containerOptions['id'])) { |
123
|
|
|
$this->containerOptions['id'] = $this->options['id'] . '-json-editor'; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
$this->determineValue(); |
127
|
|
|
|
128
|
|
|
foreach (['mode', 'modes'] as $parameterName) { |
129
|
|
|
$this->$parameterName = ArrayHelper::getValue($this->clientOptions, $parameterName, $this->$parameterName); |
130
|
|
|
} |
131
|
|
|
// make sure that "mode" is specified, otherwise JavaScript error can occur in some situations |
132
|
|
|
$this->clientOptions['mode'] = $this->mode; |
133
|
|
|
|
134
|
|
|
// if property is not set then try to determine automatically whether full version is needed |
135
|
|
|
if ($this->minimalist === null) { |
136
|
|
|
$this->minimalist = $this->mode != 'code' && !in_array('code', $this->modes); |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Analyses input data and determines what should be used as value. |
142
|
|
|
* This method must set `value` and `decodedValue` properties. |
143
|
|
|
*/ |
144
|
|
|
protected function determineValue() |
145
|
|
|
{ |
146
|
|
|
// decodedValue property has first precedence |
147
|
|
|
if ($this->decodedValue !== null) { |
148
|
|
|
$this->value = Json::encode($this->decodedValue); |
149
|
|
|
return; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
// value property has second precedence |
153
|
|
|
// options['value'] property has third precedence |
154
|
|
|
if (!$this->issetValue() && isset($this->options['value'])) { |
155
|
|
|
$this->value = $this->options['value']; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
// model attribute has fourth precedence |
159
|
|
|
if (!$this->issetValue() && $this->hasModel()) { |
160
|
|
|
$this->value = Html::getAttributeValue($this->model, $this->attribute); |
|
|
|
|
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
// value is not set anywhere, use default |
164
|
|
|
if (!$this->issetValue()) { |
165
|
|
|
$this->value = $this->defaultValue; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
$this->decodedValue = Json::decode($this->value, false); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Check whether `value` property is set. For JSON string the empty string is considered as equivalent of null. |
173
|
|
|
* @return bool whether `value` property is set. |
174
|
|
|
*/ |
175
|
|
|
protected function issetValue() |
176
|
|
|
{ |
177
|
|
|
return $this->value !== null && $this->value !== ''; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* {@inheritdoc} |
182
|
|
|
*/ |
183
|
|
|
public function run() |
184
|
|
|
{ |
185
|
|
|
$this->registerClientScript(); |
186
|
|
|
if ($this->hasModel()) { |
187
|
|
|
$this->options['value'] = $this->value; // model may contain decoded JSON, override value for rendering |
188
|
|
|
echo Html::activeHiddenInput($this->model, $this->attribute, $this->options); |
189
|
|
|
} else { |
190
|
|
|
echo Html::hiddenInput($this->name, $this->value, $this->options); |
191
|
|
|
} |
192
|
|
|
echo Html::tag('div', '', $this->containerOptions); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Initializes client options. |
197
|
|
|
*/ |
198
|
|
|
protected function initClientOptions() |
199
|
|
|
{ |
200
|
|
|
$options = $this->clientOptions; |
201
|
|
|
foreach ($options as $key => $value) { |
202
|
|
|
if (!$value instanceof JsExpression && in_array($key, $this->jsExpressionClientOptions)) { |
203
|
|
|
$options[$key] = new JsExpression($value); |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
$this->clientOptions = $options; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Registers the needed client script. |
211
|
|
|
*/ |
212
|
|
|
public function registerClientScript() |
213
|
|
|
{ |
214
|
|
|
$this->initClientOptions(); |
215
|
|
|
$view = $this->getView(); |
216
|
|
|
|
217
|
|
|
if ($this->minimalist) { |
218
|
|
|
JsonEditorMinimalistAsset::register($view); |
219
|
|
|
} else { |
220
|
|
|
JsonEditorFullAsset::register($view); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
$hiddenInputId = $this->options['id']; |
224
|
|
|
$editorName = Inflector::variablize($hiddenInputId) . 'JsonEditor_' . hash('crc32', $hiddenInputId); |
225
|
|
|
$this->options['data-json-editor-name'] = $editorName; |
226
|
|
|
|
227
|
|
|
$jsUpdateHiddenField = "jQuery('#$hiddenInputId').val($editorName.getText());"; |
228
|
|
|
|
229
|
|
|
if (isset($this->clientOptions['onChange'])) { |
230
|
|
|
$userFunction = " var userFunction = {$this->clientOptions['onChange']}; userFunction.call(this);"; |
231
|
|
|
} else { |
232
|
|
|
$userFunction = ''; |
233
|
|
|
} |
234
|
|
|
$this->clientOptions['onChange'] = new JsExpression("function() {{$jsUpdateHiddenField}$userFunction}"); |
235
|
|
|
|
236
|
|
|
if (!empty($this->collapseAll) || !empty($this->expandAll)) { |
237
|
|
|
if (isset($this->clientOptions['onModeChange'])) { |
238
|
|
|
$userFunction = " var userFunction = {$this->clientOptions['onModeChange']}; " . |
239
|
|
|
"userFunction.call(this, newMode, oldMode);"; |
240
|
|
|
} else { |
241
|
|
|
$userFunction = ''; |
242
|
|
|
} |
243
|
|
|
$jsOnModeChange = "function(newMode, oldMode) {"; |
244
|
|
|
foreach (['collapseAll', 'expandAll'] as $property) { |
245
|
|
|
if (!empty($this->$property)) { |
246
|
|
|
$jsOnModeChange .= "if (" . Json::htmlEncode($this->$property) . ".indexOf(newMode) !== -1) " . |
247
|
|
|
"{{$editorName}.$property();}"; |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
$jsOnModeChange .= "$userFunction}"; |
251
|
|
|
$this->clientOptions['onModeChange'] = new JsExpression($jsOnModeChange); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$htmlEncodedValue = Json::htmlEncode($this->decodedValue); // Json::htmlEncode is needed to prevent XSS |
255
|
|
|
$jsCode = "$editorName = new JSONEditor(document.getElementById('{$this->containerOptions['id']}'), " . |
256
|
|
|
Json::htmlEncode($this->clientOptions) . ");\n" . |
257
|
|
|
"$editorName.set($htmlEncodedValue);\n" . // have to use set method, |
258
|
|
|
// because constructor works wrong for '0', 'null', '""'; constructor turns them to {}, which may be wrong |
259
|
|
|
"jQuery('#$hiddenInputId').parents('form').submit(function() {{$jsUpdateHiddenField}});"; |
260
|
|
|
if (in_array($this->mode, $this->collapseAll)) { |
261
|
|
|
$jsCode .= "\n$editorName.collapseAll();"; |
262
|
|
|
} |
263
|
|
|
if (in_array($this->mode, $this->expandAll)) { |
264
|
|
|
$jsCode .= "\n$editorName.expandAll();"; |
265
|
|
|
} |
266
|
|
|
$view->registerJs($jsCode); |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.