Passed
Pull Request — master (#652)
by Edoardo
03:11
created

ModulesController::saveCategory()   A

Complexity

Conditions 3
Paths 6

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
c 0
b 0
f 0
dl 0
loc 30
rs 9.584
cc 3
nc 6
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\Event;
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\HistoryComponent $History
25
 * @property \App\Controller\Component\ObjectsEditorsComponent $ObjectsEditors
26
 * @property \App\Controller\Component\ProjectConfigurationComponent $ProjectConfiguration
27
 * @property \App\Controller\Component\PropertiesComponent $Properties
28
 * @property \App\Controller\Component\QueryComponent $Query
29
 * @property \App\Controller\Component\ThumbsComponent $Thumbs
30
 * @property \BEdita\WebTools\Controller\Component\ApiFormatterComponent $ApiFormatter
31
 */
32
class ModulesController extends AppController
33
{
34
    /**
35
     * Object type currently used
36
     *
37
     * @var string
38
     */
39
    protected $objectType = null;
40
41
    /**
42
     * {@inheritDoc}
43
     */
44
    public function initialize(): void
45
    {
46
        parent::initialize();
47
48
        $this->loadComponent('History');
49
        $this->loadComponent('ObjectsEditors');
50
        $this->loadComponent('Properties');
51
        $this->loadComponent('ProjectConfiguration');
52
        $this->loadComponent('Query');
53
        $this->loadComponent('Thumbs');
54
        $this->loadComponent('BEdita/WebTools.ApiFormatter');
55
56
        if (!empty($this->request)) {
57
            $this->objectType = $this->request->getParam('object_type');
58
            $this->Modules->setConfig('currentModuleName', $this->objectType);
59
            $this->Schema->setConfig('type', $this->objectType);
60
        }
61
62
        $this->Security->setConfig('unlockedActions', ['save']);
63
    }
64
65
    /**
66
     * {@inheritDoc}
67
     * @codeCoverageIgnore
68
     */
69
    public function beforeRender(Event $event): ?Response
70
    {
71
        $this->set('objectType', $this->objectType);
72
73
        return parent::beforeRender($event);
0 ignored issues
show
Bug introduced by
Are you sure the usage of parent::beforeRender($event) targeting App\Controller\AppController::beforeRender() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
74
    }
75
76
    /**
77
     * Display resources list.
78
     *
79
     * @return \Cake\Http\Response|null
80
     */
81
    public function index(): ?Response
82
    {
83
        $this->request->allowMethod(['get']);
84
85
        // handle filter and query parameters using session
86
        $result = $this->applySessionFilter();
87
        if ($result != null) {
88
            return $result;
89
        }
90
91
        try {
92
            $response = $this->apiClient->getObjects($this->objectType, $this->Query->index());
93
        } catch (BEditaClientException $e) {
94
            $this->log($e, LogLevel::ERROR);
95
            $this->Flash->error($e->getMessage(), ['params' => $e]);
96
            // remove session filter to avoid error repetition
97
            $session = $this->request->getSession();
98
            $session->delete(sprintf('%s.filter', $this->Modules->getConfig('currentModuleName')));
99
100
            return $this->redirect(['_name' => 'dashboard']);
101
        }
102
103
        $this->ProjectConfiguration->read();
104
105
        $response = $this->ApiFormatter->embedIncluded((array)$response);
106
        $objects = (array)Hash::get($response, 'data');
107
        $this->set('objects', $objects);
108
        $this->set('meta', (array)Hash::get($response, 'meta'));
109
        $this->set('links', (array)Hash::get($response, 'links'));
110
        $this->set('types', ['right' => $this->Schema->descendants($this->objectType)]);
111
112
        $this->set('properties', $this->Properties->indexList($this->objectType));
113
114
        // base/custom filters for filter view
115
        $this->set('filter', $this->Properties->filterList($this->objectType));
116
117
        // base/custom bulk actions for index view
118
        $this->set('bulkActions', $this->Properties->bulkList($this->objectType));
119
120
        // objectTypes schema
121
        $this->set('schema', $this->getSchemaForIndex($this->objectType));
122
123
        // set prevNext for views navigations
124
        $this->setObjectNav($objects);
125
126
        return null;
127
    }
128
129
    /**
130
     * View single resource.
131
     *
132
     * @param string|int $id Resource ID.
133
     * @return \Cake\Http\Response|null
134
     */
135
    public function view($id): ?Response
136
    {
137
        $this->request->allowMethod(['get']);
138
139
        try {
140
            $query = ['count' => 'all'];
141
            $response = $this->apiClient->getObject($id, $this->objectType, $query);
142
        } catch (BEditaClientException $e) {
143
            // Error! Back to index.
144
            $this->log($e, LogLevel::ERROR);
145
            $this->Flash->error(__('Error retrieving the requested content'), ['params' => $e]);
146
147
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
148
        }
149
        $this->ProjectConfiguration->read();
150
151
        $revision = Hash::get($response, 'meta.schema.' . $this->objectType . '.revision', null);
152
        $schema = $this->Schema->getSchema($this->objectType, $revision);
153
154
        $object = $response['data'];
155
156
        // setup `currentAttributes` and recover failure data from session.
157
        $this->Modules->setupAttributes($object);
158
159
        $included = (!empty($response['included'])) ? $response['included'] : [];
160
        $typeIncluded = (array)Hash::combine($included, '{n}.id', '{n}', '{n}.type');
161
        $streams = Hash::get($typeIncluded, 'streams');
162
        $this->History->load($id, $object);
163
        $this->set(compact('object', 'included', 'schema', 'streams'));
164
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
165
166
        // setup relations metadata
167
        $this->Modules->setupRelationsMeta(
168
            $this->Schema->getRelationsSchema(),
169
            $object['relationships'],
170
            $this->Properties->relationsList($this->objectType)
171
        );
172
173
        $rightTypes = \App\Utility\Schema::rightTypes($this->viewVars['relationsSchema']);
174
175
        // set schemas for relations right types
176
        $schemasByType = $this->Schema->getSchemasByType($rightTypes);
177
        $this->set('schemasByType', $schemasByType);
178
179
        $this->set('filtersByType', $this->Properties->filtersByType($rightTypes));
180
181
        // set objectNav
182
        $objectNav = $this->getObjectNav((string)$id);
183
        $this->set('objectNav', $objectNav);
184
185
        $this->ObjectsEditors->update((string)$id);
186
187
        return null;
188
    }
189
190
    /**
191
     * View single resource by id, doing a proper redirect (302) to resource module view by type.
192
     * If no resource found by ID, redirect to referer.
193
     *
194
     * @param string|int $id Resource ID.
195
     * @return \Cake\Http\Response|null
196
     */
197
    public function uname($id): ?Response
198
    {
199
        try {
200
            $response = $this->apiClient->get(sprintf('/objects/%s', $id));
201
        } catch (BEditaClientException $e) {
202
            $msg = $e->getMessage();
203
            $error = $e->getCode() === 404 ?
204
                sprintf(__('Resource "%s" not found', true), $id) :
0 ignored issues
show
Bug introduced by
It seems like __('Resource "%s" not found', true) can also be of type null; however, parameter $format of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

204
                sprintf(/** @scrutinizer ignore-type */ __('Resource "%s" not found', true), $id) :
Loading history...
205
                sprintf(__('Resource "%s" not available. Error: %s', true), $id, $msg);
206
            $this->Flash->error($error);
207
208
            return $this->redirect($this->referer());
209
        }
210
        $_name = 'modules:view';
211
        $object_type = $response['data']['type'];
212
        $id = $response['data']['id'];
213
214
        return $this->redirect(compact('_name', 'object_type', 'id'));
215
    }
216
217
    /**
218
     * Display new resource form.
219
     *
220
     * @return \Cake\Http\Response|null
221
     */
222
    public function create(): ?Response
223
    {
224
        $this->viewBuilder()->setTemplate('view');
225
226
        // Create stub object with empty `attributes`.
227
        $schema = $this->Schema->getSchema();
228
        if (!is_array($schema)) {
229
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
230
231
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
232
        }
233
        $attributes = array_fill_keys(
234
            array_keys(
235
                array_filter(
236
                    $schema['properties'],
237
                    function ($schema) {
238
                        return empty($schema['readOnly']);
239
                    }
240
                )
241
            ),
242
            ''
243
        );
244
        $object = [
245
            'type' => $this->objectType,
246
            'attributes' => $attributes,
247
        ];
248
249
        $this->set(compact('object', 'schema'));
250
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
251
        $this->ProjectConfiguration->read();
252
253
        // setup relations metadata
254
        $relationships = (array)Hash::get($schema, 'relations');
255
        $this->Modules->setupRelationsMeta($this->Schema->getRelationsSchema(), $relationships);
256
257
        return null;
258
    }
259
260
    /**
261
     * Create new object from ajax request.
262
     *
263
     * @return void
264
     */
265
    public function save(): void
266
    {
267
        $this->viewBuilder()->setClassName('Json'); // force json response
268
        $this->request->allowMethod(['post']);
269
        $requestData = $this->prepareRequest($this->objectType);
270
        unset($requestData['_csrfToken']);
271
        // extract related objects data
272
        $relatedData = (array)Hash::get($requestData, '_api');
273
        unset($requestData['_api']);
274
275
        try {
276
            // upload file (if available)
277
            $this->Modules->upload($requestData);
278
279
            // save data
280
            $response = $this->apiClient->save($this->objectType, $requestData);
281
            $objectId = (string)Hash::get($response, 'data.id');
282
            $this->Modules->saveRelated($objectId, $this->objectType, $relatedData);
283
        } catch (BEditaClientException $error) {
284
            $this->log($error->getMessage(), LogLevel::ERROR);
285
            $this->Flash->error($error->getMessage(), ['params' => $error]);
286
287
            $this->set(['error' => $error->getAttributes()]);
288
            $this->set('_serialize', ['error']);
289
290
            // set session data to recover form
291
            $this->Modules->setDataFromFailedSave($this->objectType, $requestData);
292
293
            return;
294
        }
295
        if ($response['data']) {
296
            $response['data'] = [ $response['data'] ];
297
        }
298
299
        $this->Thumbs->urls($response);
300
301
        $this->set((array)$response);
302
        $this->set('_serialize', array_keys($response));
303
    }
304
305
    /**
306
     * Clone single object.
307
     *
308
     * @param string|int $id Object ID.
309
     * @return \Cake\Http\Response|null
310
     */
311
    public function clone($id): ?Response
312
    {
313
        $this->viewBuilder()->setTemplate('view');
314
315
        $schema = $this->Schema->getSchema();
316
        if (!is_array($schema)) {
317
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
318
319
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
320
        }
321
        try {
322
            $response = $this->apiClient->getObject($id, $this->objectType);
323
            $attributes = $response['data']['attributes'];
324
            $attributes['uname'] = '';
325
            unset($attributes['relationships']);
326
            $attributes['title'] = $this->request->getQuery('title');
327
        } catch (BEditaClientException $e) {
328
            $this->log($e, LogLevel::ERROR);
329
            $this->Flash->error($e->getMessage(), ['params' => $e]);
330
331
            return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
332
        }
333
        $object = [
334
            'type' => $this->objectType,
335
            'attributes' => $attributes,
336
        ];
337
        $this->History->load($id, $object);
338
        $this->set(compact('object', 'schema'));
339
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
340
341
        return null;
342
    }
343
344
    /**
345
     * Delete single resource.
346
     *
347
     * @return \Cake\Http\Response|null
348
     */
349
    public function delete(): ?Response
350
    {
351
        $this->request->allowMethod(['post']);
352
        $ids = [];
353
        if (!empty($this->request->getData('ids'))) {
354
            if (is_string($this->request->getData('ids'))) {
355
                $ids = explode(',', $this->request->getData('ids'));
0 ignored issues
show
Bug introduced by
It seems like $this->request->getData('ids') can also be of type array and null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

355
                $ids = explode(',', /** @scrutinizer ignore-type */ $this->request->getData('ids'));
Loading history...
356
            }
357
        } else {
358
            $ids = [$this->request->getData('id')];
359
        }
360
        foreach ($ids as $id) {
361
            try {
362
                $this->apiClient->deleteObject($id, $this->objectType);
363
            } catch (BEditaClientException $e) {
364
                $this->log($e, LogLevel::ERROR);
365
                $this->Flash->error($e->getMessage(), ['params' => $e]);
366
                if (!empty($this->request->getData('id'))) {
367
                    return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->request->getData('id')]);
368
                }
369
370
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
371
            }
372
        }
373
        $this->Flash->success(__('Object(s) deleted'));
374
375
        return $this->redirect([
376
            '_name' => 'modules:list',
377
            'object_type' => $this->objectType,
378
        ]);
379
    }
380
381
    /**
382
     * Relation data load via API => `GET /:object_type/:id/related/:relation`
383
     *
384
     * @param string|int $id The object ID.
385
     * @param string $relation The relation name.
386
     * @return void
387
     */
388
    public function related($id, string $relation): void
389
    {
390
        if ($id === 'new') {
391
            $this->set('data', []);
392
            $this->set('_serialize', ['data']);
393
394
            return;
395
        }
396
397
        $this->request->allowMethod(['get']);
398
        $query = $this->Query->prepare($this->request->getQueryParams());
399
        try {
400
            $response = $this->apiClient->getRelated($id, $this->objectType, $relation, $query);
401
            $response = $this->ApiFormatter->embedIncluded((array)$response);
402
        } catch (BEditaClientException $error) {
403
            $this->log($error, LogLevel::ERROR);
404
405
            $this->set(compact('error'));
406
            $this->set('_serialize', ['error']);
407
408
            return;
409
        }
410
411
        $this->Thumbs->urls($response);
412
413
        $this->set((array)$response);
414
        $this->set('_serialize', array_keys($response));
415
    }
416
417
    /**
418
     * Load resources of $type callig api `GET /:type/`
419
     * Json response
420
     *
421
     * @param string|int $id the object identifier.
422
     * @param string $type the resource type name.
423
     * @return void
424
     */
425
    public function resources($id, string $type): void
426
    {
427
        $this->request->allowMethod(['get']);
428
        $query = $this->Query->prepare($this->request->getQueryParams());
429
        try {
430
            $response = $this->apiClient->get($type, $query);
431
        } catch (BEditaClientException $error) {
432
            $this->log($error, LogLevel::ERROR);
433
434
            $this->set(compact('error'));
435
            $this->set('_serialize', ['error']);
436
437
            return;
438
        }
439
440
        $this->set((array)$response);
441
        $this->set('_serialize', array_keys($response));
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $array of array_keys() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

441
        $this->set('_serialize', array_keys(/** @scrutinizer ignore-type */ $response));
Loading history...
442
    }
443
444
    /**
445
     * Relation data load callig api `GET /:object_type/:id/relationships/:relation`
446
     * Json response
447
     *
448
     * @param string|int $id The object ID.
449
     * @param string $relation The relation name.
450
     * @return void
451
     */
452
    public function relationships($id, string $relation): void
453
    {
454
        $this->request->allowMethod(['get']);
455
        $available = $this->availableRelationshipsUrl($relation);
456
457
        try {
458
            $query = $this->Query->prepare($this->request->getQueryParams());
459
            $response = $this->apiClient->get($available, $query);
460
461
            $this->Thumbs->urls($response);
462
        } catch (BEditaClientException $ex) {
463
            $this->log($ex, LogLevel::ERROR);
464
465
            $this->set([
466
                'error' => $ex->getMessage(),
467
                '_serialize' => ['error'],
468
            ]);
469
470
            return;
471
        }
472
473
        $this->set((array)$response);
474
        $this->set('_serialize', array_keys($response));
475
    }
476
477
    /**
478
     * Retrieve URL to get objects available for a relation
479
     *
480
     * @param string $relation The relation name.
481
     * @return string
482
     */
483
    protected function availableRelationshipsUrl(string $relation): string
484
    {
485
        $defaults = [
486
            'children' => '/objects',
487
            'parent' => '/folders',
488
            'parents' => '/folders',
489
        ];
490
        $defaultUrl = (string)Hash::get($defaults, $relation);
491
        if (!empty($defaultUrl)) {
492
            return $defaultUrl;
493
        }
494
495
        $relationsSchema = $this->Schema->getRelationsSchema();
496
        $types = $this->Modules->relatedTypes($relationsSchema, $relation);
497
        if (count($types) === 1) {
498
            return sprintf('/%s', $types[0]);
499
        }
500
501
        return '/objects?filter[type][]=' . implode('&filter[type][]=', $types);
502
    }
503
504
    /**
505
     * get object properties and format them for index
506
     *
507
     * @param string $objectType objecte type name
508
     *
509
     * @return array $schema
510
     */
511
    public function getSchemaForIndex($objectType): array
512
    {
513
        $schema = (array)$this->Schema->getSchema($objectType);
514
515
        // if prop is an enum then prepend an empty string for select element
516
        if (!empty($schema['properties'])) {
517
            foreach ($schema['properties'] as &$property) {
518
                if (isset($property['enum'])) {
519
                    array_unshift($property['enum'], '');
520
                }
521
            }
522
        }
523
524
        return $schema;
525
    }
526
527
    /**
528
     * List categories for the object type.
529
     *
530
     * @return \Cake\Http\Response|null
531
     */
532
    public function listCategories()
533
    {
534
        $this->viewBuilder()->setTemplate('categories');
535
536
        $this->request->allowMethod(['get']);
537
        $query = $this->request->getQueryParams() + [
538
            'filter' => [
539
                'type' => $this->objectType,
540
            ],
541
            'page_size' => 500,
542
        ];
543
544
        try {
545
            $response = $this->apiClient->get('/model/categories', $query);
546
        } catch (BEditaClientException $e) {
547
            $this->log($e, 'error');
548
            $this->Flash->error($e->getMessage(), ['params' => $e]);
549
550
            return $this->redirect(['_name' => 'dashboard']);
551
        }
552
553
        $resources = [];
554
        foreach ((array)$response['data'] as $category) {
555
            $resources[$category['id']] = $category;
556
        }
557
558
        $grouped = [
559
            '_' => [],
560
        ];
561
        foreach ($resources as $category) {
562
            if (empty($category['attributes']['parent_id'])) {
563
                $grouped['_'][] = $category['id'];
564
            } else {
565
                $grouped[$category['attributes']['parent_id']][] = $category['id'];
566
            }
567
        }
568
569
        $this->set(compact('resources', 'grouped'));
570
        $this->set('meta', (array)$response['meta']);
571
        $this->set('links', (array)$response['links']);
572
        $this->set('schema', $this->Schema->getSchema());
573
        $this->set('properties', $this->Properties->indexList('categories'));
574
        $this->set('filter', $this->Properties->filterList('categories'));
575
        $this->set('object_types', [$this->objectType]);
576
577
        return null;
578
    }
579
580
    /**
581
     * Save category.
582
     *
583
     * @return \Cake\Http\Response|null
584
     */
585
    public function saveCategory(): ?Response
586
    {
587
        $this->request->allowMethod(['post']);
588
589
        $data = $this->request->getData();
590
        $id = Hash::get($data, 'id');
591
        unset($data['id']);
592
        $body = [
593
            'data' => [
594
                'type' => 'categories',
595
                'attributes' => $data,
596
            ],
597
        ];
598
599
        try {
600
            if (empty($id)) {
601
                $response = $this->apiClient->post('/model/categories', json_encode($body));
602
                $id = Hash::get($response, 'data.id');
0 ignored issues
show
Unused Code introduced by
The assignment to $id is dead and can be removed.
Loading history...
603
            } else {
604
                $body['data']['id'] = $id;
605
                $this->apiClient->patch(sprintf('/model/categories/%s', $id), json_encode($body));
606
            }
607
        } catch (BEditaClientException $e) {
608
            $this->log($e, 'error');
609
            $this->Flash->error($e->getMessage(), ['params' => $e]);
610
        }
611
612
        return $this->redirect([
613
            '_name' => 'modules:categories:index',
614
            'object_type' => $this->objectType,
615
        ]);
616
    }
617
618
    /**
619
     * Remove single category.
620
     *
621
     * @param string $id Category ID.
622
     *
623
     * @return \Cake\Http\Response|null
624
     */
625
    public function removeCategory(string $id): ?Response
626
    {
627
        try {
628
            $this->apiClient->delete(sprintf('/model/categories/%s', $id));
629
        } catch (BEditaClientException $e) {
630
            $this->log($e, 'error');
631
            $this->Flash->error($e->getMessage(), ['params' => $e]);
632
        }
633
634
        return $this->redirect([
635
            '_name' => 'modules:categories:index',
636
            'object_type' => $this->objectType,
637
        ]);
638
    }
639
}
640