Completed
Push — master ( a5a9b4...e8fd61 )
by Stefano
15s queued 13s
created

ModulesController::saveCategory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 14
rs 9.9666
cc 2
nc 2
nop 0
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 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
namespace App\Controller;
14
15
use BEdita\SDK\BEditaClientException;
16
use Cake\Event\EventInterface;
17
use Cake\Http\Response;
18
use Cake\Utility\Hash;
19
use Psr\Log\LogLevel;
20
21
/**
22
 * Modules controller: list, add, edit, remove objects
23
 *
24
 * @property \App\Controller\Component\CategoriesComponent $Categories
25
 * @property \App\Controller\Component\CloneComponent $Clone
26
 * @property \App\Controller\Component\HistoryComponent $History
27
 * @property \App\Controller\Component\ObjectsEditorsComponent $ObjectsEditors
28
 * @property \App\Controller\Component\ProjectConfigurationComponent $ProjectConfiguration
29
 * @property \App\Controller\Component\PropertiesComponent $Properties
30
 * @property \App\Controller\Component\QueryComponent $Query
31
 * @property \App\Controller\Component\ThumbsComponent $Thumbs
32
 * @property \BEdita\WebTools\Controller\Component\ApiFormatterComponent $ApiFormatter
33
 */
34
class ModulesController extends AppController
35
{
36
    /**
37
     * Object type currently used
38
     *
39
     * @var string
40
     */
41
    protected $objectType = null;
42
43
    /**
44
     * @inheritDoc
45
     */
46
    public function initialize(): void
47
    {
48
        parent::initialize();
49
50
        $this->loadComponent('Categories');
51
        $this->loadComponent('Clone');
52
        $this->loadComponent('History');
53
        $this->loadComponent('ObjectsEditors');
54
        $this->loadComponent('Properties');
55
        $this->loadComponent('ProjectConfiguration');
56
        $this->loadComponent('Query');
57
        $this->loadComponent('Thumbs');
58
        $this->loadComponent('BEdita/WebTools.ApiFormatter');
59
        if ($this->getRequest()->getParam('object_type')) {
60
            $this->objectType = $this->getRequest()->getParam('object_type');
61
            $this->Modules->setConfig('currentModuleName', $this->objectType);
62
            $this->Schema->setConfig('type', $this->objectType);
63
        }
64
        $this->Security->setConfig('unlockedActions', ['save']);
65
    }
66
67
    /**
68
     * {@inheritDoc}
69
     *
70
     * @codeCoverageIgnore
71
     */
72
    public function beforeRender(EventInterface $event): ?Response
73
    {
74
        $this->set('objectType', $this->objectType);
75
76
        return parent::beforeRender($event);
77
    }
78
79
    /**
80
     * Display resources list.
81
     *
82
     * @return \Cake\Http\Response|null
83
     */
84
    public function index(): ?Response
85
    {
86
        $this->getRequest()->allowMethod(['get']);
87
88
        // handle filter and query parameters using session
89
        $result = $this->applySessionFilter();
90
        if ($result != null) {
91
            return $result;
92
        }
93
94
        try {
95
            $response = $this->apiClient->getObjects($this->objectType, $this->Query->index());
96
        } catch (BEditaClientException $e) {
97
            $this->log($e->getMessage(), LogLevel::ERROR);
98
            $this->Flash->error($e->getMessage(), ['params' => $e]);
99
            // remove session filter to avoid error repetition
100
            $session = $this->getRequest()->getSession();
101
            $session->delete(sprintf('%s.filter', $this->Modules->getConfig('currentModuleName')));
102
103
            return $this->redirect(['_name' => 'dashboard']);
104
        }
105
106
        $this->ProjectConfiguration->read();
107
108
        $response = $this->ApiFormatter->embedIncluded((array)$response);
109
        $objects = (array)Hash::get($response, 'data');
110
        $this->set('objects', $objects);
111
        $this->set('meta', (array)Hash::get($response, 'meta'));
112
        $this->set('links', (array)Hash::get($response, 'links'));
113
        $this->set('types', ['right' => $this->Schema->descendants($this->objectType)]);
114
115
        $this->set('properties', $this->Properties->indexList($this->objectType));
116
117
        // base/custom filters for filter view
118
        $this->set('filter', $this->Properties->filterList($this->objectType));
119
120
        // base/custom bulk actions for index view
121
        $this->set('bulkActions', $this->Properties->bulkList($this->objectType));
122
123
        // objectTypes schema
124
        $this->set('schema', $this->getSchemaForIndex($this->objectType));
125
126
        // set prevNext for views navigations
127
        $this->setObjectNav($objects);
128
129
        return null;
130
    }
131
132
    /**
133
     * View single resource.
134
     *
135
     * @param string|int $id Resource ID.
136
     * @return \Cake\Http\Response|null
137
     */
138
    public function view($id): ?Response
139
    {
140
        $this->getRequest()->allowMethod(['get']);
141
142
        try {
143
            $query = ['count' => 'all'];
144
            $response = $this->apiClient->getObject($id, $this->objectType, $query);
145
        } catch (BEditaClientException $e) {
146
            // Error! Back to index.
147
            $this->log($e->getMessage(), LogLevel::ERROR);
148
            $this->Flash->error(__('Error retrieving the requested content'), ['params' => $e]);
149
150
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
151
        }
152
        $this->ProjectConfiguration->read();
153
154
        $revision = Hash::get($response, 'meta.schema.' . $this->objectType . '.revision', null);
155
        $schema = $this->Schema->getSchema($this->objectType, $revision);
156
157
        $object = $response['data'];
158
159
        // setup `currentAttributes` and recover failure data from session.
160
        $this->Modules->setupAttributes($object);
161
162
        $included = !empty($response['included']) ? $response['included'] : [];
163
        $typeIncluded = (array)Hash::combine($included, '{n}.id', '{n}', '{n}.type');
164
        $streams = Hash::get($typeIncluded, 'streams');
165
        $this->History->load($id, $object);
166
        $this->set(compact('object', 'included', 'schema', 'streams'));
167
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
168
169
        $computedRelations = array_reduce(
170
            array_keys($object['relationships']),
171
            function ($acc, $relName) use ($schema) {
172
                $acc[$relName] = (array)Hash::get($schema, sprintf('relations.%s', $relName), []);
173
174
                return $acc;
175
            },
176
            []
177
        );
178
179
        // setup relations metadata
180
        $this->Modules->setupRelationsMeta(
181
            $this->Schema->getRelationsSchema(),
182
            $computedRelations,
183
            $this->Properties->relationsList($this->objectType),
184
            $this->Properties->hiddenRelationsList($this->objectType),
185
            $this->Properties->readonlyRelationsList($this->objectType)
186
        );
187
188
        $rel = (array)$this->viewBuilder()->getVar('relationsSchema');
189
        $rightTypes = \App\Utility\Schema::rightTypes($rel);
190
191
        // set schemas for relations right types
192
        $schemasByType = $this->Schema->getSchemasByType($rightTypes);
193
        $this->set('schemasByType', $schemasByType);
194
195
        $this->set('filtersByType', $this->Properties->filtersByType($rightTypes));
196
197
        // set objectNav
198
        $objectNav = $this->getObjectNav((string)$id);
199
        $this->set('objectNav', $objectNav);
200
201
        $this->ObjectsEditors->update((string)$id);
202
203
        return null;
204
    }
205
206
    /**
207
     * View single resource by id, doing a proper redirect (302) to resource module view by type.
208
     * If no resource found by ID, redirect to referer.
209
     *
210
     * @param string|int $id Resource ID.
211
     * @return \Cake\Http\Response|null
212
     */
213
    public function uname($id): ?Response
214
    {
215
        try {
216
            $response = $this->apiClient->get(sprintf('/objects/%s', $id));
217
        } catch (BEditaClientException $e) {
218
            $msg = $e->getMessage();
219
            $error = $e->getCode() === 404 ?
220
                sprintf(__('Resource "%s" not found', true), $id) :
221
                sprintf(__('Resource "%s" not available. Error: %s', true), $id, $msg);
222
            $this->Flash->error($error);
223
224
            return $this->redirect($this->referer());
225
        }
226
        $_name = 'modules:view';
227
        $object_type = $response['data']['type'];
228
        $id = $response['data']['id'];
229
230
        return $this->redirect(compact('_name', 'object_type', 'id'));
231
    }
232
233
    /**
234
     * Display new resource form.
235
     *
236
     * @return \Cake\Http\Response|null
237
     */
238
    public function create(): ?Response
239
    {
240
        $this->viewBuilder()->setTemplate('view');
241
242
        // Create stub object with empty `attributes`.
243
        $schema = $this->Schema->getSchema();
244
        if (!is_array($schema)) {
245
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
246
247
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
248
        }
249
        $attributes = array_fill_keys(
250
            array_keys(
251
                array_filter(
252
                    $schema['properties'],
253
                    function ($schema) {
254
                        return empty($schema['readOnly']);
255
                    }
256
                )
257
            ),
258
            ''
259
        );
260
        $object = [
261
            'type' => $this->objectType,
262
            'attributes' => $attributes,
263
        ];
264
265
        $this->set(compact('object', 'schema'));
266
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
267
        $this->ProjectConfiguration->read();
268
269
        // setup relations metadata
270
        $relationships = (array)Hash::get($schema, 'relations');
271
        $this->Modules->setupRelationsMeta($this->Schema->getRelationsSchema(), $relationships);
272
273
        return null;
274
    }
275
276
    /**
277
     * Create new object from ajax request.
278
     *
279
     * @return void
280
     */
281
    public function save(): void
282
    {
283
        $this->viewBuilder()->setClassName('Json'); // force json response
284
        $this->getRequest()->allowMethod(['post']);
285
        $requestData = $this->prepareRequest($this->objectType);
286
        unset($requestData['_csrfToken']);
287
        // extract related objects data
288
        $relatedData = (array)Hash::get($requestData, '_api');
289
        unset($requestData['_api']);
290
291
        try {
292
            // upload file (if available)
293
            $this->Modules->upload($requestData);
294
295
            // save data
296
            $response = $this->apiClient->save($this->objectType, $requestData);
297
            $objectId = (string)Hash::get($response, 'data.id');
298
            $this->Modules->saveRelated($objectId, $this->objectType, $relatedData);
299
        } catch (BEditaClientException $error) {
300
            $this->log($error->getMessage(), LogLevel::ERROR);
301
            $this->Flash->error($error->getMessage(), ['params' => $error]);
302
303
            $this->set(['error' => $error->getAttributes()]);
304
            $this->setSerialize(['error']);
305
306
            // set session data to recover form
307
            $this->Modules->setDataFromFailedSave($this->objectType, $requestData);
308
309
            return;
310
        }
311
        if ($response['data']) {
312
            $response['data'] = [ $response['data'] ];
313
        }
314
315
        $this->Thumbs->urls($response);
316
317
        $this->set((array)$response);
318
        $this->setSerialize(array_keys($response));
319
    }
320
321
    /**
322
     * Clone single object.
323
     *
324
     * @param string|int $id Object ID.
325
     * @return \Cake\Http\Response|null
326
     */
327
    public function clone($id): ?Response
328
    {
329
        $this->viewBuilder()->setTemplate('view');
330
331
        $schema = $this->Schema->getSchema();
332
        if (!is_array($schema)) {
333
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
334
335
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
336
        }
337
        try {
338
            $source = $this->apiClient->getObject($id, $this->objectType);
339
            $attributes = $source['data']['attributes'];
340
            $attributes['uname'] = '';
341
            unset($attributes['relationships']);
342
            $attributes['title'] = $this->getRequest()->getQuery('title');
343
            $attributes['status'] = 'draft';
344
            $save = $this->apiClient->save($this->objectType, $attributes);
345
            $destination = (string)Hash::get($save, 'data.id');
346
            $this->Clone->relations($source, $destination);
347
            $id = $destination;
348
        } catch (BEditaClientException $e) {
349
            $this->log($e->getMessage(), LogLevel::ERROR);
350
            $this->Flash->error($e->getMessage(), ['params' => $e]);
351
        }
352
353
        return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
354
    }
355
356
    /**
357
     * Delete single resource.
358
     *
359
     * @return \Cake\Http\Response|null
360
     */
361
    public function delete(): ?Response
362
    {
363
        $this->getRequest()->allowMethod(['post']);
364
        $ids = [];
365
        if (!empty($this->getRequest()->getData('ids'))) {
366
            if (is_string($this->getRequest()->getData('ids'))) {
367
                $ids = explode(',', (string)$this->getRequest()->getData('ids'));
368
            }
369
        } elseif (!empty($this->getRequest()->getData('id'))) {
370
            $ids = [$this->getRequest()->getData('id')];
371
        }
372
        foreach ($ids as $id) {
373
            try {
374
                $this->apiClient->deleteObject($id, $this->objectType);
375
            } catch (BEditaClientException $e) {
376
                $this->log($e->getMessage(), LogLevel::ERROR);
377
                $this->Flash->error($e->getMessage(), ['params' => $e]);
378
                if (!empty($this->getRequest()->getData('id'))) {
379
                    return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->getRequest()->getData('id')]);
380
                }
381
382
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
383
            }
384
        }
385
        $this->Flash->success(__('Object(s) deleted'));
386
387
        return $this->redirect([
388
            '_name' => 'modules:list',
389
            'object_type' => $this->objectType,
390
        ]);
391
    }
392
393
    /**
394
     * Relation data load via API => `GET /:object_type/:id/related/:relation`
395
     *
396
     * @param string|int $id The object ID.
397
     * @param string $relation The relation name.
398
     * @return void
399
     */
400
    public function related($id, string $relation): void
401
    {
402
        if ($id === 'new') {
403
            $this->set('data', []);
404
            $this->setSerialize(['data']);
405
406
            return;
407
        }
408
409
        $this->getRequest()->allowMethod(['get']);
410
        $query = $this->Query->prepare($this->getRequest()->getQueryParams());
411
        try {
412
            $response = $this->apiClient->getRelated($id, $this->objectType, $relation, $query);
413
            $response = $this->ApiFormatter->embedIncluded((array)$response);
414
        } catch (BEditaClientException $error) {
415
            $this->log($error->getMessage(), LogLevel::ERROR);
416
417
            $this->set(compact('error'));
418
            $this->setSerialize(['error']);
419
420
            return;
421
        }
422
423
        $this->Thumbs->urls($response);
424
425
        $this->set((array)$response);
426
        $this->setSerialize(array_keys($response));
427
    }
428
429
    /**
430
     * Load resources of $type callig api `GET /:type/`
431
     * Json response
432
     *
433
     * @param string|int $id the object identifier.
434
     * @param string $type the resource type name.
435
     * @return void
436
     */
437
    public function resources($id, string $type): void
438
    {
439
        $this->getRequest()->allowMethod(['get']);
440
        $query = $this->Query->prepare($this->getRequest()->getQueryParams());
441
        try {
442
            $response = $this->apiClient->get($type, $query);
443
        } catch (BEditaClientException $error) {
444
            $this->log($error, LogLevel::ERROR);
445
446
            $this->set(compact('error'));
447
            $this->setSerialize(['error']);
448
449
            return;
450
        }
451
452
        $this->set((array)$response);
453
        $this->setSerialize(array_keys($response));
454
    }
455
456
    /**
457
     * Relation data load callig api `GET /:object_type/:id/relationships/:relation`
458
     * Json response
459
     *
460
     * @param string|int $id The object ID.
461
     * @param string $relation The relation name.
462
     * @return void
463
     */
464
    public function relationships($id, string $relation): void
465
    {
466
        $this->getRequest()->allowMethod(['get']);
467
        $available = $this->availableRelationshipsUrl($relation);
468
469
        try {
470
            $query = $this->Query->prepare($this->getRequest()->getQueryParams());
471
            $response = $this->apiClient->get($available, $query);
472
473
            $this->Thumbs->urls($response);
474
        } catch (BEditaClientException $ex) {
475
            $this->log($ex->getMessage(), LogLevel::ERROR);
476
477
            $this->set('error', $ex->getMessage());
478
            $this->setSerialize(['error']);
479
480
            return;
481
        }
482
483
        $this->set((array)$response);
484
        $this->setSerialize(array_keys($response));
485
    }
486
487
    /**
488
     * Retrieve URL to get objects available for a relation
489
     *
490
     * @param string $relation The relation name.
491
     * @return string
492
     */
493
    protected function availableRelationshipsUrl(string $relation): string
494
    {
495
        $defaults = [
496
            'children' => '/objects',
497
            'parent' => '/folders',
498
            'parents' => '/folders',
499
        ];
500
        $defaultUrl = (string)Hash::get($defaults, $relation);
501
        if (!empty($defaultUrl)) {
502
            return $defaultUrl;
503
        }
504
505
        $relationsSchema = $this->Schema->getRelationsSchema();
506
        $types = $this->Modules->relatedTypes($relationsSchema, $relation);
507
        if (count($types) === 1) {
508
            return sprintf('/%s', $types[0]);
509
        }
510
511
        return '/objects?filter[type][]=' . implode('&filter[type][]=', $types);
512
    }
513
514
    /**
515
     * get object properties and format them for index
516
     *
517
     * @param string $objectType objecte type name
518
     * @return array $schema
519
     */
520
    public function getSchemaForIndex($objectType): array
521
    {
522
        $schema = (array)$this->Schema->getSchema($objectType);
523
524
        // if prop is an enum then prepend an empty string for select element
525
        if (!empty($schema['properties'])) {
526
            foreach ($schema['properties'] as &$property) {
527
                if (isset($property['enum'])) {
528
                    array_unshift($property['enum'], '');
529
                }
530
            }
531
        }
532
533
        return $schema;
534
    }
535
536
    /**
537
     * Get objectType
538
     *
539
     * @return string|null
540
     */
541
    public function getObjectType(): ?string
542
    {
543
        return $this->objectType;
544
    }
545
546
    /**
547
     * Set objectType
548
     *
549
     * @param string|null $objectType The object type
550
     * @return void
551
     */
552
    public function setObjectType(?string $objectType): void
553
    {
554
        $this->objectType = $objectType;
555
    }
556
}
557