Passed
Push — master ( 84cb95...42814d )
by Dante
02:12
created

ModulesController::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 1
dl 0
loc 15
rs 9.8666
c 0
b 0
f 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 App\Utility\CacheTools;
16
use App\Utility\Message;
17
use App\Utility\PermissionsTrait;
18
use BEdita\SDK\BEditaClientException;
19
use BEdita\WebTools\Utility\ApiTools;
20
use Cake\Core\Configure;
21
use Cake\Event\Event;
22
use Cake\Event\EventInterface;
23
use Cake\Http\Response;
24
use Cake\I18n\I18n;
25
use Cake\Utility\Hash;
26
use Psr\Log\LogLevel;
27
28
/**
29
 * Modules controller: list, add, edit, remove objects
30
 *
31
 * @property \App\Controller\Component\CategoriesComponent $Categories
32
 * @property \App\Controller\Component\ChildrenComponent $Children
33
 * @property \App\Controller\Component\HistoryComponent $History
34
 * @property \App\Controller\Component\ObjectsEditorsComponent $ObjectsEditors
35
 * @property \App\Controller\Component\ParentsComponent $Parents
36
 * @property \App\Controller\Component\ProjectConfigurationComponent $ProjectConfiguration
37
 * @property \App\Controller\Component\PropertiesComponent $Properties
38
 * @property \App\Controller\Component\QueryComponent $Query
39
 * @property \App\Controller\Component\ThumbsComponent $Thumbs
40
 * @property \BEdita\WebTools\Controller\Component\ApiFormatterComponent $ApiFormatter
41
 */
42
class ModulesController extends AppController
43
{
44
    use PermissionsTrait;
45
46
    /**
47
     * Object type currently used
48
     *
49
     * @var string
50
     */
51
    protected $objectType = null;
52
53
    /**
54
     * @inheritDoc
55
     */
56
    public function initialize(): void
57
    {
58
        parent::initialize();
59
60
        $this->loadComponent('Children');
61
        $this->loadComponent('History');
62
        $this->loadComponent('ObjectsEditors');
63
        $this->loadComponent('Parents');
64
        $this->loadComponent('Properties');
65
        $this->loadComponent('ProjectConfiguration');
66
        $this->loadComponent('Query');
67
        $this->loadComponent('Thumbs', Configure::read('Thumbs', []));
68
        $this->loadComponent('BEdita/WebTools.ApiFormatter');
69
        if ($this->getRequest()->getParam('object_type')) {
70
            $this->objectType = $this->getRequest()->getParam('object_type');
71
            $this->Modules->setConfig('currentModuleName', $this->objectType);
72
            $this->Schema->setConfig('type', $this->objectType);
73
        }
74
        $this->Security->setConfig('unlockedActions', ['save']);
75
    }
76
77
    /**
78
     * {@inheritDoc}
79
     *
80
     * @codeCoverageIgnore
81
     */
82
    public function beforeRender(EventInterface $event): ?Response
83
    {
84
        $this->set('objectType', $this->objectType);
85
86
        return parent::beforeRender($event);
87
    }
88
89
    /**
90
     * Display resources list.
91
     *
92
     * @return \Cake\Http\Response|null
93
     */
94
    public function index(): ?Response
95
    {
96
        $this->getRequest()->allowMethod(['get']);
97
98
        // handle filter and query parameters using session
99
        $result = $this->applySessionFilter();
100
        if ($result != null) {
101
            return $result;
102
        }
103
104
        try {
105
            $params = $this->Query->index();
106
            $response = $this->apiClient->getObjects($this->objectType, $params);
107
            if (empty($params['q']) && empty($params['filter'])) {
108
                CacheTools::setModuleCount((array)$response, $this->Modules->getConfig('currentModuleName'));
109
            }
110
        } catch (BEditaClientException $e) {
111
            $this->log($e->getMessage(), LogLevel::ERROR);
112
            $this->Flash->error($e->getMessage(), ['params' => $e]);
113
            // remove session filter to avoid error repetition
114
            $session = $this->getRequest()->getSession();
115
            $session->delete(sprintf('%s.filter', $this->Modules->getConfig('currentModuleName')));
116
117
            return $this->redirect(['_name' => 'dashboard']);
118
        }
119
120
        $this->ProjectConfiguration->read();
121
122
        $response = $this->ApiFormatter->embedIncluded((array)$response);
123
        $objects = (array)Hash::get($response, 'data');
124
        $this->set('objects', $objects);
125
        $this->set('meta', (array)Hash::get($response, 'meta'));
126
        $this->set('links', (array)Hash::get($response, 'links'));
127
        $this->set('types', ['right' => $this->Schema->descendants($this->objectType)]);
128
129
        $this->set('properties', $this->Properties->indexList($this->objectType));
130
131
        // base/custom filters for filter view
132
        $this->set('filter', $this->Properties->filterList($this->objectType));
133
134
        // base/custom bulk actions for index view
135
        $this->set('bulkActions', $this->Properties->bulkList($this->objectType));
136
137
        // objectTypes schema
138
        $this->set('schema', $this->getSchemaForIndex($this->objectType));
139
        // custom properties
140
        $this->set('customProps', $this->Schema->customProps($this->objectType));
141
142
        // set prevNext for views navigations
143
        $this->setObjectNav($objects);
144
145
        return null;
146
    }
147
148
    /**
149
     * View single resource.
150
     *
151
     * @param string|int $id Resource ID.
152
     * @return \Cake\Http\Response|null
153
     */
154
    public function view($id): ?Response
155
    {
156
        $this->getRequest()->allowMethod(['get']);
157
158
        try {
159
            $query = ['count' => 'all'];
160
            $response = $this->apiClient->getObject($id, $this->objectType, $query);
161
        } catch (BEditaClientException $e) {
162
            // Error! Back to index.
163
            $this->log($e->getMessage(), LogLevel::ERROR);
164
            $this->Flash->error(__('Error retrieving the requested content'), ['params' => $e]);
165
166
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
167
        }
168
        $this->ProjectConfiguration->read();
169
170
        $revision = Hash::get($response, 'meta.schema.' . $this->objectType . '.revision', null);
171
        $schema = $this->Schema->getSchema($this->objectType, $revision);
172
173
        $object = $response['data'];
174
175
        // setup `currentAttributes` and recover failure data from session.
176
        $this->Modules->setupAttributes($object);
177
178
        $included = !empty($response['included']) ? $response['included'] : [];
179
        $typeIncluded = Hash::combine($included, '{n}.id', '{n}', '{n}.type');
180
        $streams = Hash::get($typeIncluded, 'streams');
181
        $this->History->load($id, $object);
182
        $this->set(compact('object', 'included', 'schema', 'streams'));
183
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
184
        $this->set('foldersSchema', $this->Schema->getSchema('folders'));
185
186
        $computedRelations = array_reduce(
187
            array_keys($object['relationships']),
188
            function ($acc, $relName) use ($schema) {
189
                $acc[$relName] = (array)Hash::get($schema, sprintf('relations.%s', $relName), []);
190
191
                return $acc;
192
            },
193
            []
194
        );
195
        $this->setupViewRelations($computedRelations);
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
            $msgNotFound = sprintf(__('Resource "%s" not found', true), $id);
220
            $msgNotAvailable = sprintf(__('Resource "%s" not available. Error: %s', true), $id, $msg);
221
            $error = $e->getCode() === 404 ? $msgNotFound : $msgNotAvailable;
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
            null
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
        $this->setupViewRelations((array)Hash::get($schema, 'relations'));
270
271
        return null;
272
    }
273
274
    /**
275
     * Create new object from ajax request.
276
     *
277
     * @return void
278
     */
279
    public function save(): void
280
    {
281
        $this->viewBuilder()->setClassName('Json'); // force json response
282
        $this->getRequest()->allowMethod(['post']);
283
        $requestData = $this->prepareRequest($this->objectType);
284
        unset($requestData['_csrfToken']);
285
        // extract related objects data
286
        $relatedData = (array)Hash::get($requestData, '_api');
287
        unset($requestData['_api']);
288
289
        try {
290
            $uname = Hash::get($requestData, 'uname');
291
            if (!empty($uname) && is_numeric($uname)) {
292
                $this->set(['error' => __('Invalid numeric uname. Change it to a valid string')]);
293
                $this->setSerialize(['error']);
294
295
                return;
296
            }
297
            $id = Hash::get($requestData, 'id');
298
            // skip save if no data changed
299
            if (empty($relatedData) && count($requestData) === 1 && !empty($id)) {
300
                $response = $this->apiClient->getObject($id, $this->objectType, ['count' => 'all']);
301
                $this->Thumbs->urls($response);
302
                $this->set((array)$response);
303
                $this->setSerialize(array_keys($response));
304
305
                return;
306
            }
307
308
            // upload file (if available)
309
            $this->Modules->upload($requestData);
310
311
            // save data
312
            $lang = I18n::getLocale();
313
            $headers = ['Accept-Language' => $lang];
314
            $response = $this->apiClient->save($this->objectType, $requestData, $headers);
315
            $this->savePermissions(
316
                (array)$response,
317
                (array)$this->Schema->getSchema($this->objectType),
318
                (array)Hash::get($requestData, 'permissions')
319
            );
320
            $id = (string)Hash::get($response, 'data.id');
321
            $this->Modules->saveRelated($id, $this->objectType, $relatedData);
322
            $options = [
323
                'id' => Hash::get($response, 'data.id'),
324
                'type' => $this->objectType,
325
                'data' => $requestData,
326
            ];
327
            $event = new Event('Controller.afterSave', $this, $options);
328
            $this->getEventManager()->dispatch($event);
329
        } catch (BEditaClientException $error) {
330
            $message = new Message($error);
331
            $this->log($message->get(), LogLevel::ERROR);
332
            $this->Flash->error($message->get(), ['params' => $error]);
333
            $this->set(['error' => $message->get()]);
334
            $this->setSerialize(['error']);
335
336
            return;
337
        }
338
        if ($response['data']) {
339
            $response['data'] = [ $response['data'] ];
340
        }
341
342
        $this->Thumbs->urls($response);
343
344
        $this->set((array)$response);
345
        $this->setSerialize(array_keys($response));
346
    }
347
348
    /**
349
     * Clone single object.
350
     *
351
     * @param string|int $id Object ID.
352
     * @return \Cake\Http\Response|null
353
     */
354
    public function clone($id): ?Response
355
    {
356
        $this->viewBuilder()->setTemplate('view');
357
        $schema = $this->Schema->getSchema();
358
        if (!is_array($schema)) {
359
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
360
361
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
362
        }
363
        try {
364
            $modified = [
365
                'title' => $this->getRequest()->getQuery('title'),
366
                'status' => 'draft',
367
            ];
368
            $reset = (array)Configure::read(sprintf('Clone.%s.reset', $this->objectType));
369
            foreach ($reset as $field) {
370
                $modified[$field] = null;
371
            }
372
            $included = [];
373
            foreach (['relationships', 'translations'] as $attribute) {
374
                if ($this->getRequest()->getQuery($attribute) === 'true') {
375
                    $included[] = $attribute;
376
                }
377
            }
378
            $clone = $this->apiClient->clone($this->objectType, $id, $modified, $included);
379
            $id = (string)Hash::get($clone, 'data.id');
380
        } catch (BEditaClientException $e) {
381
            $this->log($e->getMessage(), LogLevel::ERROR);
382
            $this->Flash->error($e->getMessage(), ['params' => $e]);
383
        }
384
385
        return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
386
    }
387
388
    /**
389
     * Delete single resource.
390
     *
391
     * @return \Cake\Http\Response|null
392
     */
393
    public function delete(): ?Response
394
    {
395
        $this->getRequest()->allowMethod(['post']);
396
        $id = $this->getRequest()->getData('id');
397
        $ids = $this->getRequest()->getData('ids');
398
        $ids = is_string($ids) ? explode(',', $ids) : $ids;
399
        $ids = empty($ids) ? [$id] : $ids;
400
        try {
401
            $this->apiClient->deleteObjects($ids, $this->objectType);
402
            $eventManager = $this->getEventManager();
403
            foreach ($ids as $id) {
404
                $event = new Event('Controller.afterDelete', $this, ['id' => $id, 'type' => $this->objectType]);
405
                $eventManager->dispatch($event);
406
            }
407
        } catch (BEditaClientException $e) {
408
            $this->log($e->getMessage(), LogLevel::ERROR);
409
            $this->Flash->error($e->getMessage(), ['params' => $e]);
410
            $id = $this->getRequest()->getData('id');
411
            $options = empty($id) ? $this->referer() : ['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id];
412
413
            return $this->redirect($options);
414
        }
415
        $this->Flash->success(__('Object(s) deleted'));
416
417
        return $this->redirect([
418
            '_name' => 'modules:list',
419
            'object_type' => $this->objectType,
420
        ]);
421
    }
422
423
    /**
424
     * Relation data load via API => `GET /:object_type/:id/related/:relation`
425
     *
426
     * @param string|int $id The object ID.
427
     * @param string $relation The relation name.
428
     * @return void
429
     */
430
    public function related($id, string $relation): void
431
    {
432
        if ($id === 'new') {
433
            $this->set('data', []);
434
            $this->setSerialize(['data']);
435
436
            return;
437
        }
438
439
        $this->getRequest()->allowMethod(['get']);
440
        $query = $this->Query->prepare($this->getRequest()->getQueryParams());
441
        try {
442
            $response = $this->apiClient->getRelated($id, $this->objectType, $relation, $query);
443
            $response = $this->ApiFormatter->embedIncluded((array)$response);
444
        } catch (BEditaClientException $error) {
445
            $this->log($error->getMessage(), LogLevel::ERROR);
446
447
            $this->set(compact('error'));
448
            $this->setSerialize(['error']);
449
450
            return;
451
        }
452
453
        $this->Thumbs->urls($response);
454
455
        $this->set((array)$response);
456
        $this->setSerialize(array_keys($response));
457
    }
458
459
    /**
460
     * Load resources of $type callig api `GET /:type/`
461
     * Json response
462
     *
463
     * @param string|int $id the object identifier.
464
     * @param string $type the resource type name.
465
     * @return void
466
     */
467
    public function resources($id, string $type): void
468
    {
469
        $this->getRequest()->allowMethod(['get']);
470
        $query = $this->Query->prepare($this->getRequest()->getQueryParams());
471
        try {
472
            $response = $this->apiClient->get($type, $query);
473
        } catch (BEditaClientException $error) {
474
            $this->log($error, LogLevel::ERROR);
475
476
            $this->set(compact('error'));
477
            $this->setSerialize(['error']);
478
479
            return;
480
        }
481
482
        $this->set((array)$response);
483
        $this->setSerialize(array_keys($response));
484
    }
485
486
    /**
487
     * Relation data load calling api `GET /:object_type/:id/relationships/:relation`
488
     * Json response
489
     *
490
     * @param string|int $id The object ID.
491
     * @param string $relation The relation name.
492
     * @return void
493
     */
494
    public function relationships($id, string $relation): void
495
    {
496
        $this->getRequest()->allowMethod(['get']);
497
        $available = $this->availableRelationshipsUrl($relation);
498
499
        try {
500
            $query = $this->Query->prepare($this->getRequest()->getQueryParams());
501
            $response = $this->apiClient->get($available, $query);
502
503
            $this->Thumbs->urls($response);
504
        } catch (BEditaClientException $ex) {
505
            $this->log($ex->getMessage(), LogLevel::ERROR);
506
507
            $this->set('error', $ex->getMessage());
508
            $this->setSerialize(['error']);
509
510
            return;
511
        }
512
513
        $this->set((array)$response);
514
        $this->setSerialize(array_keys($response));
515
    }
516
517
    /**
518
     * Retrieve URL to get objects available for a relation
519
     *
520
     * @param string $relation The relation name.
521
     * @return string
522
     */
523
    protected function availableRelationshipsUrl(string $relation): string
524
    {
525
        $defaults = [
526
            'children' => '/objects',
527
            'parent' => '/folders',
528
            'parents' => '/folders',
529
        ];
530
        $defaultUrl = (string)Hash::get($defaults, $relation);
531
        if (!empty($defaultUrl)) {
532
            return $defaultUrl;
533
        }
534
535
        $relationsSchema = $this->Schema->getRelationsSchema();
536
        $types = $this->Modules->relatedTypes($relationsSchema, $relation);
537
538
        return count($types) === 1 ? sprintf('/%s', $types[0]) : '/objects?filter[type][]=' . implode('&filter[type][]=', $types);
539
    }
540
541
    /**
542
     * get object properties and format them for index
543
     *
544
     * @param string $objectType objecte type name
545
     * @return array $schema
546
     */
547
    public function getSchemaForIndex($objectType): array
548
    {
549
        $schema = (array)$this->Schema->getSchema($objectType);
550
551
        // if prop is an enum then prepend an empty string for select element
552
        if (!empty($schema['properties'])) {
553
            foreach ($schema['properties'] as &$property) {
554
                if (isset($property['enum'])) {
555
                    array_unshift($property['enum'], '');
556
                }
557
            }
558
        }
559
560
        return $schema;
561
    }
562
563
    /**
564
     * Get objectType
565
     *
566
     * @return string|null
567
     */
568
    public function getObjectType(): ?string
569
    {
570
        return $this->objectType;
571
    }
572
573
    /**
574
     * Set objectType
575
     *
576
     * @param string|null $objectType The object type
577
     * @return void
578
     */
579
    public function setObjectType(?string $objectType): void
580
    {
581
        $this->objectType = $objectType;
582
    }
583
584
    /**
585
     * Set schemasByType and filtersByType, considering relations and schemas.
586
     *
587
     * @param array $relations The relations
588
     * @return void
589
     */
590
    private function setupViewRelations(array $relations): void
591
    {
592
        // setup relations schema
593
        $relationsSchema = $this->Schema->getRelationsSchema();
594
        $this->set('relationsSchema', $relationsSchema);
595
596
        // setup relations metadata
597
        $this->Modules->setupRelationsMeta(
598
            $relationsSchema,
599
            $relations,
600
            $this->Properties->relationsList($this->objectType),
601
            $this->Properties->hiddenRelationsList($this->objectType),
602
            $this->Properties->readonlyRelationsList($this->objectType)
603
        );
604
605
        // set right types, considering the object type relations
606
        $rel = (array)$this->viewBuilder()->getVar('relationsSchema');
607
        $rightTypes = \App\Utility\Schema::rightTypes($rel);
608
        $this->set('rightTypes', $rightTypes);
609
610
        // set schemas for relations right types
611
        $schemasByType = $this->Schema->getSchemasByType($rightTypes);
612
        $this->set('schemasByType', $schemasByType);
613
        $this->set('filtersByType', $this->Properties->filtersByType($rightTypes));
614
    }
615
616
    /**
617
     * Get list of users / no email, no relationships, no links, no schema, no included.
618
     *
619
     * @return void
620
     */
621
    public function users(): void
622
    {
623
        $this->viewBuilder()->setClassName('Json');
624
        $this->getRequest()->allowMethod('get');
625
        $query = array_merge(
626
            $this->getRequest()->getQueryParams(),
627
            ['fields' => 'id,title,username,name,surname']
628
        );
629
        $response = (array)$this->apiClient->get('users', $query);
630
        $response = ApiTools::cleanResponse($response);
631
        $data = (array)Hash::get($response, 'data');
632
        $meta = (array)Hash::get($response, 'meta');
633
        $this->set(compact('data', 'meta'));
634
        $this->setSerialize(['data', 'meta']);
635
    }
636
637
    /**
638
     * Get single resource, minimal data / no relationships, no links, no schema, no included.
639
     *
640
     * @param string $id The object ID
641
     * @return void
642
     */
643
    public function get(string $id): void
644
    {
645
        $this->viewBuilder()->setClassName('Json');
646
        $this->getRequest()->allowMethod('get');
647
        $response = (array)$this->apiClient->getObject($id, 'objects');
648
        $query = array_merge(
649
            $this->getRequest()->getQueryParams(),
650
            ['fields' => 'id,title,description,uname,status,media_url']
651
        );
652
        $response = (array)$this->apiClient->getObject($id, $response['data']['type'], $query);
653
        $response = ApiTools::cleanResponse($response);
654
        $data = (array)Hash::get($response, 'data');
655
        $meta = (array)Hash::get($response, 'meta');
656
        $this->set(compact('data', 'meta'));
657
        $this->setSerialize(['data', 'meta']);
658
    }
659
}
660