SchemaHelper::formatByte()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2019 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
14
namespace App\View\Helper;
15
16
use App\Form\Control;
17
use App\Form\ControlType;
18
use App\Form\Options;
19
use Cake\Core\Configure;
20
use Cake\I18n\Number;
21
use Cake\Utility\Hash;
22
use Cake\Utility\Inflector;
23
use Cake\View\Helper;
24
25
/**
26
 * Schema helper
27
 *
28
 * @property \App\View\Helper\PermsHelper $Perms
29
 * @property \Cake\View\Helper\TimeHelper $Time
30
 */
31
class SchemaHelper extends Helper
32
{
33
    /**
34
     * {@inheritDoc}
35
     *
36
     * @var array
37
     */
38
    public $helpers = ['Perms', 'Time'];
39
40
    /**
41
     * Default translatable fields to be prepended in translations
42
     *
43
     * @var array
44
     */
45
    public const DEFAULT_TRANSLATABLE = ['title', 'description', 'body'];
46
47
    /**
48
     * Translatable media types
49
     *
50
     * @var array
51
     */
52
    public const TRANSLATABLE_MEDIATYPES = ['text/html', 'text/plain'];
53
54
    /**
55
     * Get control options for a property schema.
56
     *
57
     * @param string $name Property name.
58
     * @param mixed $value Property value.
59
     * @param array|null $schema Property schema.
60
     * @return array
61
     */
62
    public function controlOptions(string $name, $value, ?array $schema = null): array
63
    {
64
        $options = Options::customControl($name, $value);
65
        $objectType = (string)$this->_View->get('objectType');
66
        $ctrlOptionsPath = sprintf('Properties.%s.options.%s', $objectType, $name);
67
        $ctrlOptions = (array)Configure::read($ctrlOptionsPath);
68
69
        if (!empty($options)) {
70
            $this->updateRicheditorOptions($name, !empty($schema['placeholders']), $options);
71
72
            return array_merge($options, [
73
                'label' => Hash::get($ctrlOptions, 'label'),
74
                'readonly' => Hash::get($ctrlOptions, 'readonly', false),
75
                'disabled' => Hash::get($ctrlOptions, 'readonly', false),
76
            ]);
77
        }
78
        if (empty($ctrlOptions['type'])) {
79
            $ctrlOptionsType = ControlType::fromSchema($schema);
80
            $ctrlOptions['type'] = $ctrlOptionsType;
81
            if (in_array($ctrlOptionsType, ['integer', 'number'])) {
82
                $ctrlOptions['step'] = $ctrlOptionsType === 'number' ? 'any' : '1';
83
                $ctrlOptions['type'] = 'number';
84
            }
85
        }
86
        // verify if there's a custom control handler for $type and $name
87
        $custom = $this->customControl($name, $value, $ctrlOptions);
88
        if (!empty($custom)) {
89
            return $custom;
90
        }
91
        $opts = [
92
            'objectType' => $objectType,
93
            'property' => $name,
94
            'value' => $value,
95
            'schema' => (array)$schema,
96
            'propertyType' => (string)$ctrlOptions['type'],
97
            'label' => Hash::get($ctrlOptions, 'label'),
98
            'readonly' => Hash::get($ctrlOptions, 'readonly', false),
99
            'disabled' => Hash::get($ctrlOptions, 'readonly', false),
100
        ];
101
        if (!empty($ctrlOptions['step'])) {
102
            $opts['step'] = $ctrlOptions['step'];
103
        }
104
        $this->updateRicheditorOptions($name, !empty($schema['placeholders']), $opts);
105
106
        return Control::control($opts);
107
    }
108
109
    /**
110
     * Update richeditor options if a toolbar config is defined in UI.richeditor for the property.
111
     *
112
     * @param string $name Property name
113
     * @param bool $placeholders True if property has placeholders in schema
114
     * @param array $options Control options
115
     * @return void
116
     */
117
    protected function updateRicheditorOptions(string $name, bool $placeholders, array &$options)
118
    {
119
        $uiRichtext = (array)Configure::read(sprintf('UI.richeditor.%s.toolbar', $name));
120
        if (empty($uiRichtext)) {
121
            return;
122
        }
123
        if ($this->Perms->userIsAdmin() && !in_array('code', $uiRichtext)) {
124
            $uiRichtext[] = 'code';
125
        }
126
        $options['type'] = 'textarea';
127
        $richeditorKey = $placeholders ? 'v-richeditor.placeholders' : 'v-richeditor';
128
        $options[$richeditorKey] = json_encode($uiRichtext);
129
    }
130
131
    /**
132
     * Return custom control array if a custom handler has been defined or null otherwise.
133
     *
134
     * @param string $name Property name
135
     * @param mixed $value Property value.
136
     * @param array $options Control options.
137
     * @return array|null
138
     */
139
    protected function customControl($name, $value, array $options): ?array
140
    {
141
        $handlerClass = Hash::get($options, 'handler');
142
        if (empty($handlerClass)) {
143
            return null;
144
        }
145
        /** @var \App\Form\CustomHandlerInterface $handler */
146
        $handler = new $handlerClass();
147
148
        return $handler->control($name, $value, $options);
149
    }
150
151
    /**
152
     * Display a formatted property value using schema.
153
     *
154
     * @param mixed $value Property value.
155
     * @param array $schema Property schema array.
156
     * @return string
157
     */
158
    public function format($value, $schema = []): string
159
    {
160
        $type = static::typeFromSchema((array)$schema);
161
        $type = Inflector::variable(str_replace('-', '_', $type));
162
        $methodName = sprintf('format%s', ucfirst($type));
163
        if (method_exists($this, $methodName) && $value !== null) {
164
            return call_user_func_array([$this, $methodName], [$value]);
165
        }
166
        if (is_array($value)) {
167
            return json_encode($value);
168
        }
169
170
        return (string)$value;
171
    }
172
173
    /**
174
     * Format byte value
175
     *
176
     * @param mixed $value Property value.
177
     * @return string
178
     */
179
    public function formatByte($value): string
180
    {
181
        return Number::toReadableSize((int)$value);
182
    }
183
184
    /**
185
     * Format boolean value
186
     *
187
     * @param mixed $value Property value.
188
     * @return string
189
     */
190
    public function formatBoolean($value): string
191
    {
192
        $res = filter_var($value, FILTER_VALIDATE_BOOLEAN);
193
194
        return $res ? __('Yes') : __('No');
195
    }
196
197
    /**
198
     * Format date value
199
     *
200
     * @param mixed $value Property value.
201
     * @return string
202
     */
203
    public function formatDate($value): string
204
    {
205
        if (empty($value)) {
206
            return '';
207
        }
208
209
        return (string)$this->Time->format($value);
210
    }
211
212
    /**
213
     * Format date-time value
214
     *
215
     * @param mixed $value Property value.
216
     * @return string
217
     */
218
    public function formatDateTime($value): string
219
    {
220
        return $this->formatDate($value);
221
    }
222
223
    /**
224
     * Infer type from property schema in JSON-SCHEMA format
225
     * Possible return values:
226
     *
227
     *   'string'
228
     *   'number'
229
     *   'integer'
230
     *   'boolean'
231
     *   'array'
232
     *   'object'
233
     *   'date-time'
234
     *   'date'
235
     *
236
     * @param array $schema The property schema
237
     * @return string
238
     */
239
    public static function typeFromSchema(array $schema): string
240
    {
241
        if (!empty($schema['oneOf'])) {
242
            foreach ($schema['oneOf'] as $subSchema) {
243
                if (!empty($subSchema['type']) && $subSchema['type'] === 'null') {
244
                    continue;
245
                }
246
247
                return static::typeFromSchema($subSchema);
248
            }
249
        }
250
        if (empty($schema['type']) || !in_array($schema['type'], ControlType::SCHEMA_PROPERTY_TYPES)) {
251
            return 'string';
252
        }
253
        $format = Hash::get($schema, 'format');
254
        $isStringDate = $schema['type'] === 'string' && in_array($format, ['date', 'date-time']);
255
256
        return $isStringDate ? $format : $schema['type'];
257
    }
258
259
    /**
260
     * Provides list of translatable fields.
261
     * If set Properties.<objectType>.translatable, it will be added to translatable fields.
262
     *
263
     * @param array $schema The object type schema
264
     * @return array
265
     */
266
    public function translatableFields(array $schema): array
267
    {
268
        if (isset($schema['translatable'])) {
269
            $priorityFields = array_intersect(static::DEFAULT_TRANSLATABLE, (array)$schema['translatable']);
270
            $otherFields = array_diff((array)$schema['translatable'], $priorityFields);
271
        } else {
272
            $properties = (array)Hash::get($schema, 'properties');
273
            $priorityFields = array_intersect(static::DEFAULT_TRANSLATABLE, array_keys($properties));
274
            $otherFields = array_keys(array_filter(
275
                array_diff_key($properties, array_flip($priorityFields)),
276
                [$this, 'translatableType']
277
            ));
278
        }
279
280
        return array_unique(array_values(array_merge($priorityFields, $otherFields)));
281
    }
282
283
    /**
284
     * Helper recursive method to check if a property is translatable checking its JSON SCHEMA
285
     *
286
     * @param array $schema Property schema
287
     * @return bool
288
     */
289
    protected function translatableType(array $schema): bool
290
    {
291
        if (!empty($schema['oneOf'])) {
292
            return array_reduce(
293
                (array)$schema['oneOf'],
294
                function ($carry, $item) {
295
                    if ($carry) {
296
                        return true;
297
                    }
298
299
                    return $this->translatableType((array)$item);
300
                }
301
            );
302
        }
303
        // accept as translatable 'string' type having text/html or tex/plain 'contentMediaType'
304
        $type = (string)Hash::get($schema, 'type');
305
        $contentMediaType = Hash::get($schema, 'contentMediaType');
306
307
        return $type === 'string' &&
308
            in_array($contentMediaType, static::TRANSLATABLE_MEDIATYPES);
309
    }
310
311
    /**
312
     * Verify field's schema, return true if field is sortable.
313
     *
314
     * @param string $field The field to check
315
     * @return bool
316
     */
317
    public function sortable(string $field): bool
318
    {
319
        // default sortable fields
320
        if (in_array($field, ['date_ranges', 'modified', 'id', 'title'])) {
321
            return true;
322
        }
323
        $schema = (array)$this->_View->get('schema');
324
        $customProps = (array)$this->_View->get('customProps');
325
        $schema = Hash::get($schema, sprintf('properties.%s', $field), []);
326
        // empty schema or field is a custom prop, then not sortable
327
        if (empty($schema) || in_array($field, $customProps)) {
328
            return false;
329
        }
330
        $type = self::typeFromSchema($schema);
331
332
        // not sortable: 'array', 'object'
333
        // other types are sortable: 'string', 'number', 'integer', 'boolean', 'date-time', 'date'
334
335
        return !in_array($type, ['array', 'object']);
336
    }
337
338
    /**
339
     * Get filter list from filters and schema properties
340
     *
341
     * @param array $filters Filters list
342
     * @param array|null $schemaProperties Schema properties
343
     * @return array
344
     */
345
    public function filterList(array $filters, ?array $schemaProperties): array
346
    {
347
        $list = [];
348
        foreach ($filters as $f) {
349
            $fname = is_array($f) ? (string)Hash::get($f, 'name', __('untitled')) : $f;
350
            $flabel = is_array($f) ? (string)Hash::get($f, 'label', $fname) : Inflector::humanize($f);
351
            $noProperties = empty($schemaProperties) || !array_key_exists($fname, $schemaProperties);
352
            $schema = $noProperties ? null : (array)Hash::get($schemaProperties, $fname);
353
            $item = self::controlOptions($fname, null, $schema);
354
            $item['name'] = $fname;
355
            $item['label'] = $flabel;
356
            $list[] = $item;
357
        }
358
359
        return $list;
360
    }
361
362
    /**
363
     * Get filter list by type
364
     *
365
     * @param array $filtersByType Filters list by type
366
     * @param array|null $schemasByType Schema properties by type
367
     * @return array
368
     */
369
    public function filterListByType(array $filtersByType, ?array $schemasByType): array
370
    {
371
        $list = [];
372
        foreach ($filtersByType as $type => $filters) {
373
            $noSchema = empty($schemasByType) || !array_key_exists($type, $schemasByType);
374
            $schemaProperties = $noSchema ? null : Hash::get($schemasByType, $type);
375
            $schemaProperties = array_key_exists('properties', (array)$schemaProperties) ? $schemaProperties['properties'] : $schemaProperties;
376
            $schemaProperties = $schemaProperties !== false ? $schemaProperties : null;
377
            $list[$type] = self::filterList($filters, $schemaProperties);
378
        }
379
380
        return $list;
381
    }
382
}
383