Passed
Push — master ( ad93c0...af2985 )
by Stefano
03:02
created

ModulesController::setupViewRelations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 17
rs 9.9
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
        $this->setupViewRelations($computedRelations);
179
180
        // set objectNav
181
        $objectNav = $this->getObjectNav((string)$id);
182
        $this->set('objectNav', $objectNav);
183
184
        $this->ObjectsEditors->update((string)$id);
185
186
        return null;
187
    }
188
189
    /**
190
     * View single resource by id, doing a proper redirect (302) to resource module view by type.
191
     * If no resource found by ID, redirect to referer.
192
     *
193
     * @param string|int $id Resource ID.
194
     * @return \Cake\Http\Response|null
195
     */
196
    public function uname($id): ?Response
197
    {
198
        try {
199
            $response = $this->apiClient->get(sprintf('/objects/%s', $id));
200
        } catch (BEditaClientException $e) {
201
            $msg = $e->getMessage();
202
            $error = $e->getCode() === 404 ?
203
                sprintf(__('Resource "%s" not found', true), $id) :
204
                sprintf(__('Resource "%s" not available. Error: %s', true), $id, $msg);
205
            $this->Flash->error($error);
206
207
            return $this->redirect($this->referer());
208
        }
209
        $_name = 'modules:view';
210
        $object_type = $response['data']['type'];
211
        $id = $response['data']['id'];
212
213
        return $this->redirect(compact('_name', 'object_type', 'id'));
214
    }
215
216
    /**
217
     * Display new resource form.
218
     *
219
     * @return \Cake\Http\Response|null
220
     */
221
    public function create(): ?Response
222
    {
223
        $this->viewBuilder()->setTemplate('view');
224
225
        // Create stub object with empty `attributes`.
226
        $schema = $this->Schema->getSchema();
227
        if (!is_array($schema)) {
228
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
229
230
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
231
        }
232
        $attributes = array_fill_keys(
233
            array_keys(
234
                array_filter(
235
                    $schema['properties'],
236
                    function ($schema) {
237
                        return empty($schema['readOnly']);
238
                    }
239
                )
240
            ),
241
            ''
242
        );
243
        $object = [
244
            'type' => $this->objectType,
245
            'attributes' => $attributes,
246
        ];
247
248
        $this->set(compact('object', 'schema'));
249
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
250
        $this->ProjectConfiguration->read();
251
252
        $this->setupViewRelations((array)Hash::get($schema, 'relations'));
253
254
        return null;
255
    }
256
257
    /**
258
     * Create new object from ajax request.
259
     *
260
     * @return void
261
     */
262
    public function save(): void
263
    {
264
        $this->viewBuilder()->setClassName('Json'); // force json response
265
        $this->getRequest()->allowMethod(['post']);
266
        $requestData = $this->prepareRequest($this->objectType);
267
        unset($requestData['_csrfToken']);
268
        // extract related objects data
269
        $relatedData = (array)Hash::get($requestData, '_api');
270
        unset($requestData['_api']);
271
272
        try {
273
            // upload file (if available)
274
            $this->Modules->upload($requestData);
275
276
            // save data
277
            $response = $this->apiClient->save($this->objectType, $requestData);
278
            $objectId = (string)Hash::get($response, 'data.id');
279
            $this->Modules->saveRelated($objectId, $this->objectType, $relatedData);
280
        } catch (BEditaClientException $error) {
281
            $this->log($error->getMessage(), LogLevel::ERROR);
282
            $this->Flash->error($error->getMessage(), ['params' => $error]);
283
284
            $this->set(['error' => $error->getAttributes()]);
285
            $this->setSerialize(['error']);
286
287
            // set session data to recover form
288
            $this->Modules->setDataFromFailedSave($this->objectType, $requestData);
289
290
            return;
291
        }
292
        if ($response['data']) {
293
            $response['data'] = [ $response['data'] ];
294
        }
295
296
        $this->Thumbs->urls($response);
297
298
        $this->set((array)$response);
299
        $this->setSerialize(array_keys($response));
300
    }
301
302
    /**
303
     * Clone single object.
304
     *
305
     * @param string|int $id Object ID.
306
     * @return \Cake\Http\Response|null
307
     */
308
    public function clone($id): ?Response
309
    {
310
        $this->viewBuilder()->setTemplate('view');
311
312
        $schema = $this->Schema->getSchema();
313
        if (!is_array($schema)) {
314
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
315
316
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
317
        }
318
        try {
319
            $source = $this->apiClient->getObject($id, $this->objectType);
320
            $attributes = $source['data']['attributes'];
321
            $attributes['uname'] = '';
322
            unset($attributes['relationships']);
323
            $attributes['title'] = $this->getRequest()->getQuery('title');
324
            $attributes['status'] = 'draft';
325
            $save = $this->apiClient->save($this->objectType, $attributes);
326
            $destination = (string)Hash::get($save, 'data.id');
327
            $this->Clone->relations($source, $destination);
328
            $id = $destination;
329
        } catch (BEditaClientException $e) {
330
            $this->log($e->getMessage(), LogLevel::ERROR);
331
            $this->Flash->error($e->getMessage(), ['params' => $e]);
332
        }
333
334
        return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
335
    }
336
337
    /**
338
     * Delete single resource.
339
     *
340
     * @return \Cake\Http\Response|null
341
     */
342
    public function delete(): ?Response
343
    {
344
        $this->getRequest()->allowMethod(['post']);
345
        $ids = [];
346
        if (!empty($this->getRequest()->getData('ids'))) {
347
            if (is_string($this->getRequest()->getData('ids'))) {
348
                $ids = explode(',', (string)$this->getRequest()->getData('ids'));
349
            }
350
        } elseif (!empty($this->getRequest()->getData('id'))) {
351
            $ids = [$this->getRequest()->getData('id')];
352
        }
353
        foreach ($ids as $id) {
354
            try {
355
                $this->apiClient->deleteObject($id, $this->objectType);
356
            } catch (BEditaClientException $e) {
357
                $this->log($e->getMessage(), LogLevel::ERROR);
358
                $this->Flash->error($e->getMessage(), ['params' => $e]);
359
                if (!empty($this->getRequest()->getData('id'))) {
360
                    return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->getRequest()->getData('id')]);
361
                }
362
363
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
364
            }
365
        }
366
        $this->Flash->success(__('Object(s) deleted'));
367
368
        return $this->redirect([
369
            '_name' => 'modules:list',
370
            'object_type' => $this->objectType,
371
        ]);
372
    }
373
374
    /**
375
     * Relation data load via API => `GET /:object_type/:id/related/:relation`
376
     *
377
     * @param string|int $id The object ID.
378
     * @param string $relation The relation name.
379
     * @return void
380
     */
381
    public function related($id, string $relation): void
382
    {
383
        if ($id === 'new') {
384
            $this->set('data', []);
385
            $this->setSerialize(['data']);
386
387
            return;
388
        }
389
390
        $this->getRequest()->allowMethod(['get']);
391
        $query = $this->Query->prepare($this->getRequest()->getQueryParams());
392
        try {
393
            $response = $this->apiClient->getRelated($id, $this->objectType, $relation, $query);
394
            $response = $this->ApiFormatter->embedIncluded((array)$response);
395
        } catch (BEditaClientException $error) {
396
            $this->log($error->getMessage(), LogLevel::ERROR);
397
398
            $this->set(compact('error'));
399
            $this->setSerialize(['error']);
400
401
            return;
402
        }
403
404
        $this->Thumbs->urls($response);
405
406
        $this->set((array)$response);
407
        $this->setSerialize(array_keys($response));
408
    }
409
410
    /**
411
     * Load resources of $type callig api `GET /:type/`
412
     * Json response
413
     *
414
     * @param string|int $id the object identifier.
415
     * @param string $type the resource type name.
416
     * @return void
417
     */
418
    public function resources($id, string $type): void
419
    {
420
        $this->getRequest()->allowMethod(['get']);
421
        $query = $this->Query->prepare($this->getRequest()->getQueryParams());
422
        try {
423
            $response = $this->apiClient->get($type, $query);
424
        } catch (BEditaClientException $error) {
425
            $this->log($error, LogLevel::ERROR);
426
427
            $this->set(compact('error'));
428
            $this->setSerialize(['error']);
429
430
            return;
431
        }
432
433
        $this->set((array)$response);
434
        $this->setSerialize(array_keys($response));
435
    }
436
437
    /**
438
     * Relation data load callig api `GET /:object_type/:id/relationships/:relation`
439
     * Json response
440
     *
441
     * @param string|int $id The object ID.
442
     * @param string $relation The relation name.
443
     * @return void
444
     */
445
    public function relationships($id, string $relation): void
446
    {
447
        $this->getRequest()->allowMethod(['get']);
448
        $available = $this->availableRelationshipsUrl($relation);
449
450
        try {
451
            $query = $this->Query->prepare($this->getRequest()->getQueryParams());
452
            $response = $this->apiClient->get($available, $query);
453
454
            $this->Thumbs->urls($response);
455
        } catch (BEditaClientException $ex) {
456
            $this->log($ex->getMessage(), LogLevel::ERROR);
457
458
            $this->set('error', $ex->getMessage());
459
            $this->setSerialize(['error']);
460
461
            return;
462
        }
463
464
        $this->set((array)$response);
465
        $this->setSerialize(array_keys($response));
466
    }
467
468
    /**
469
     * Retrieve URL to get objects available for a relation
470
     *
471
     * @param string $relation The relation name.
472
     * @return string
473
     */
474
    protected function availableRelationshipsUrl(string $relation): string
475
    {
476
        $defaults = [
477
            'children' => '/objects',
478
            'parent' => '/folders',
479
            'parents' => '/folders',
480
        ];
481
        $defaultUrl = (string)Hash::get($defaults, $relation);
482
        if (!empty($defaultUrl)) {
483
            return $defaultUrl;
484
        }
485
486
        $relationsSchema = $this->Schema->getRelationsSchema();
487
        $types = $this->Modules->relatedTypes($relationsSchema, $relation);
488
        if (count($types) === 1) {
489
            return sprintf('/%s', $types[0]);
490
        }
491
492
        return '/objects?filter[type][]=' . implode('&filter[type][]=', $types);
493
    }
494
495
    /**
496
     * get object properties and format them for index
497
     *
498
     * @param string $objectType objecte type name
499
     * @return array $schema
500
     */
501
    public function getSchemaForIndex($objectType): array
502
    {
503
        $schema = (array)$this->Schema->getSchema($objectType);
504
505
        // if prop is an enum then prepend an empty string for select element
506
        if (!empty($schema['properties'])) {
507
            foreach ($schema['properties'] as &$property) {
508
                if (isset($property['enum'])) {
509
                    array_unshift($property['enum'], '');
510
                }
511
            }
512
        }
513
514
        return $schema;
515
    }
516
517
    /**
518
     * Get objectType
519
     *
520
     * @return string|null
521
     */
522
    public function getObjectType(): ?string
523
    {
524
        return $this->objectType;
525
    }
526
527
    /**
528
     * Set objectType
529
     *
530
     * @param string|null $objectType The object type
531
     * @return void
532
     */
533
    public function setObjectType(?string $objectType): void
534
    {
535
        $this->objectType = $objectType;
536
    }
537
538
    /**
539
     * Set schemasByType and filtersByType, considering relations and schemas.
540
     *
541
     * @param array $relations The relations
542
     * @return void
543
     */
544
    private function setupViewRelations(array $relations): void
545
    {
546
        // setup relations metadata
547
        $this->Modules->setupRelationsMeta(
548
            $this->Schema->getRelationsSchema(),
549
            $relations,
550
            $this->Properties->relationsList($this->objectType),
551
            $this->Properties->hiddenRelationsList($this->objectType),
552
            $this->Properties->readonlyRelationsList($this->objectType)
553
        );
554
        $rel = (array)$this->viewBuilder()->getVar('relationsSchema');
555
        $rightTypes = \App\Utility\Schema::rightTypes($rel);
556
557
        // set schemas for relations right types
558
        $schemasByType = $this->Schema->getSchemasByType($rightTypes);
559
        $this->set('schemasByType', $schemasByType);
560
        $this->set('filtersByType', $this->Properties->filtersByType($rightTypes));
561
    }
562
}
563