Passed
Pull Request — master (#10)
by
unknown
07:04
created

JsonEditor::initClientOptions()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 5
c 2
b 0
f 0
dl 0
loc 9
ccs 0
cts 8
cp 0
rs 10
cc 4
nc 3
nop 0
crap 20
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);
1 ignored issue
show
Documentation Bug introduced by
It seems like yii\helpers\Html::getAtt...odel, $this->attribute) can also be of type array. However, the property $value is declared as type string. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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