Completed
Push — master ( 743575...aa2450 )
by Stefano
20s queued 11s
created

ModulesController::beforeRender()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
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\Core\Exception\UploadException;
16
use BEdita\SDK\BEditaClientException;
17
use Cake\Event\Event;
18
use Cake\Http\Exception\InternalErrorException;
19
use Cake\Http\Response;
20
use Cake\Utility\Hash;
21
use Psr\Log\LogLevel;
22
23
/**
24
 * Modules controller: list, add, edit, remove objects
25
 *
26
 * @property \App\Controller\Component\HistoryComponent $History
27
 * @property \App\Controller\Component\ProjectConfigurationComponent $ProjectConfiguration
28
 * @property \App\Controller\Component\PropertiesComponent $Properties
29
 * @property \App\Controller\Component\QueryComponent $Query
30
 * @property \App\Controller\Component\ThumbsComponent $Thumbs
31
 * @property \BEdita\WebTools\Controller\Component\ApiFormatterComponent $ApiFormatter
32
 */
33
class ModulesController extends AppController
34
{
35
    /**
36
     * Object type currently used
37
     *
38
     * @var string
39
     */
40
    protected $objectType = null;
41
42
    /**
43
     * {@inheritDoc}
44
     */
45
    public function initialize(): void
46
    {
47
        parent::initialize();
48
49
        $this->loadComponent('History');
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', ['saveJson']);
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
        // set objectNav
174
        $objectNav = $this->getObjectNav((string)$id);
175
        $this->set('objectNav', $objectNav);
176
177
        return null;
178
    }
179
180
    /**
181
     * View single resource by id, doing a proper redirect (302) to resource module view by type.
182
     * If no resource found by ID, redirect to referer.
183
     *
184
     * @param string|int $id Resource ID.
185
     * @return \Cake\Http\Response|null
186
     */
187
    public function uname($id): ?Response
188
    {
189
        try {
190
            $response = $this->apiClient->get(sprintf('/objects/%s', $id));
191
        } catch (BEditaClientException $e) {
192
            $msg = $e->getMessage();
193
            $error = $e->getCode() === 404 ?
194
                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

194
                sprintf(/** @scrutinizer ignore-type */ __('Resource "%s" not found', true), $id) :
Loading history...
195
                sprintf(__('Resource "%s" not available. Error: %s', true), $id, $msg);
196
            $this->Flash->error($error);
197
198
            return $this->redirect($this->referer());
199
        }
200
        $_name = 'modules:view';
201
        $object_type = $response['data']['type'];
202
        $id = $response['data']['id'];
203
204
        return $this->redirect(compact('_name', 'object_type', 'id'));
205
    }
206
207
    /**
208
     * Display new resource form.
209
     *
210
     * @return \Cake\Http\Response|null
211
     */
212
    public function create(): ?Response
213
    {
214
        $this->viewBuilder()->setTemplate('view');
215
216
        // Create stub object with empty `attributes`.
217
        $schema = $this->Schema->getSchema();
218
        if (!is_array($schema)) {
219
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
220
221
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
222
        }
223
        $attributes = array_fill_keys(
224
            array_keys(
225
                array_filter(
226
                    $schema['properties'],
227
                    function ($schema) {
228
                        return empty($schema['readOnly']);
229
                    }
230
                )
231
            ),
232
            ''
233
        );
234
        $object = [
235
            'type' => $this->objectType,
236
            'attributes' => $attributes,
237
        ];
238
239
        $this->set(compact('object', 'schema'));
240
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
241
        $this->ProjectConfiguration->read();
242
243
        // setup relations metadata
244
        $relationships = (array)Hash::get($schema, 'relations');
245
        $this->Modules->setupRelationsMeta($this->Schema->getRelationsSchema(), $relationships);
246
247
        return null;
248
    }
249
250
    /**
251
     * Create or edit single resource.
252
     *
253
     * @return \Cake\Http\Response|null
254
     */
255
    public function save(): ?Response
256
    {
257
        $this->request->allowMethod(['post']);
258
        $requestData = $this->prepareRequest($this->objectType);
259
        // extract related objects data
260
        $relatedData = (array)Hash::get($requestData, '_api');
261
        unset($requestData['_api']);
262
263
        try {
264
            // upload file (if available)
265
            $this->Modules->upload($requestData);
266
267
            // save data
268
            $response = $this->apiClient->save($this->objectType, $requestData);
269
            $objectId = (string)Hash::get($response, 'data.id');
270
            $this->Modules->saveRelated($objectId, $this->objectType, $relatedData);
271
        } catch (InternalErrorException | BEditaClientException | UploadException $e) {
272
            // Error! Back to object view or index.
273
            $this->log($e, LogLevel::ERROR);
274
            $this->Flash->error($e->getMessage(), ['params' => $e]);
275
276
            // set session data to recover form
277
            $this->Modules->setDataFromFailedSave($this->objectType, $requestData);
278
279
            if ($this->request->getData('id')) {
280
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->request->getData('id')]);
281
            }
282
283
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
284
        }
285
286
        // annoying message removed, restore with https://github.com/bedita/manager/issues/71
287
        // $this->Flash->success(__('Object saved'));
288
289
        return $this->redirect([
290
            '_name' => 'modules:view',
291
            'object_type' => $this->objectType,
292
            'id' => $objectId,
293
        ]);
294
    }
295
296
    /**
297
     * Create new object from ajax request.
298
     *
299
     * @return void
300
     */
301
    public function saveJson(): void
302
    {
303
        $this->viewBuilder()->setClassName('Json'); // force json response
304
        $this->request->allowMethod(['post']);
305
        $requestData = $this->prepareRequest($this->objectType);
306
307
        try {
308
            // upload file (if available)
309
            $this->Modules->upload($requestData);
310
311
            // save data
312
            $response = $this->apiClient->save($this->objectType, $requestData);
313
        } catch (BEditaClientException $error) {
314
            $this->log($error, LogLevel::ERROR);
315
316
            $this->set(compact('error'));
317
            $this->set('_serialize', ['error']);
318
319
            return;
320
        }
321
        if ($response['data']) {
322
            $response['data'] = [ $response['data'] ];
323
        }
324
325
        $this->Thumbs->urls($response);
326
327
        $this->set((array)$response);
328
        $this->set('_serialize', array_keys($response));
329
    }
330
331
    /**
332
     * Clone single object.
333
     *
334
     * @param string|int $id Object ID.
335
     * @return \Cake\Http\Response|null
336
     */
337
    public function clone($id): ?Response
338
    {
339
        $this->viewBuilder()->setTemplate('view');
340
341
        $schema = $this->Schema->getSchema();
342
        if (!is_array($schema)) {
343
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
344
345
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
346
        }
347
        try {
348
            $response = $this->apiClient->getObject($id, $this->objectType);
349
            $attributes = $response['data']['attributes'];
350
            $attributes['uname'] = '';
351
            unset($attributes['relationships']);
352
            $attributes['title'] = $this->request->getQuery('title');
353
        } catch (BEditaClientException $e) {
354
            $this->log($e, LogLevel::ERROR);
355
            $this->Flash->error($e->getMessage(), ['params' => $e]);
356
357
            return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
358
        }
359
        $object = [
360
            'type' => $this->objectType,
361
            'attributes' => $attributes,
362
        ];
363
        $this->History->load($id, $object);
364
        $this->set(compact('object', 'schema'));
365
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
366
367
        return null;
368
    }
369
370
    /**
371
     * Delete single resource.
372
     *
373
     * @return \Cake\Http\Response|null
374
     */
375
    public function delete(): ?Response
376
    {
377
        $this->request->allowMethod(['post']);
378
        $ids = [];
379
        if (!empty($this->request->getData('ids'))) {
380
            if (is_string($this->request->getData('ids'))) {
381
                $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

381
                $ids = explode(',', /** @scrutinizer ignore-type */ $this->request->getData('ids'));
Loading history...
382
            }
383
        } else {
384
            $ids = [$this->request->getData('id')];
385
        }
386
        foreach ($ids as $id) {
387
            try {
388
                $this->apiClient->deleteObject($id, $this->objectType);
389
            } catch (BEditaClientException $e) {
390
                $this->log($e, LogLevel::ERROR);
391
                $this->Flash->error($e->getMessage(), ['params' => $e]);
392
                if (!empty($this->request->getData('id'))) {
393
                    return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->request->getData('id')]);
394
                }
395
396
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
397
            }
398
        }
399
        $this->Flash->success(__('Object(s) deleted'));
400
401
        return $this->redirect([
402
            '_name' => 'modules:list',
403
            'object_type' => $this->objectType,
404
        ]);
405
    }
406
407
    /**
408
     * Relation data load via API => `GET /:object_type/:id/related/:relation`
409
     *
410
     * @param string|int $id The object ID.
411
     * @param string $relation The relation name.
412
     * @return void
413
     */
414
    public function relatedJson($id, string $relation): void
415
    {
416
        if ($id === 'new') {
417
            $this->set('data', []);
418
            $this->set('_serialize', ['data']);
419
420
            return;
421
        }
422
423
        $this->request->allowMethod(['get']);
424
        $query = $this->Query->prepare($this->request->getQueryParams());
425
        try {
426
            $response = $this->apiClient->getRelated($id, $this->objectType, $relation, $query);
427
            $response = $this->ApiFormatter->embedIncluded((array)$response);
428
        } catch (BEditaClientException $error) {
429
            $this->log($error, LogLevel::ERROR);
430
431
            $this->set(compact('error'));
432
            $this->set('_serialize', ['error']);
433
434
            return;
435
        }
436
437
        $this->Thumbs->urls($response);
438
439
        $this->set((array)$response);
440
        $this->set('_serialize', array_keys($response));
441
    }
442
443
    /**
444
     * Load resources of $type callig api `GET /:type/`
445
     * Json response
446
     *
447
     * @param string|int $id the object identifier.
448
     * @param string $type the resource type name.
449
     * @return void
450
     */
451
    public function resourcesJson($id, string $type): void
452
    {
453
        $this->request->allowMethod(['get']);
454
        $query = $this->Query->prepare($this->request->getQueryParams());
455
        try {
456
            $response = $this->apiClient->get($type, $query);
457
        } catch (BEditaClientException $error) {
458
            $this->log($error, LogLevel::ERROR);
459
460
            $this->set(compact('error'));
461
            $this->set('_serialize', ['error']);
462
463
            return;
464
        }
465
466
        $this->set((array)$response);
467
        $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

467
        $this->set('_serialize', array_keys(/** @scrutinizer ignore-type */ $response));
Loading history...
468
    }
469
470
    /**
471
     * Relation data load callig api `GET /:object_type/:id/relationships/:relation`
472
     * Json response
473
     *
474
     * @param string|int $id The object ID.
475
     * @param string $relation The relation name.
476
     * @return void
477
     */
478
    public function relationshipsJson($id, string $relation): void
479
    {
480
        $this->request->allowMethod(['get']);
481
        $available = $this->availableRelationshipsUrl($relation);
482
483
        try {
484
            $query = $this->Query->prepare($this->request->getQueryParams());
485
            $response = $this->apiClient->get($available, $query);
486
487
            $this->Thumbs->urls($response);
488
        } catch (BEditaClientException $ex) {
489
            $this->log($ex, LogLevel::ERROR);
490
491
            $this->set([
492
                'error' => $ex->getMessage(),
493
                '_serialize' => ['error'],
494
            ]);
495
496
            return;
497
        }
498
499
        $this->set((array)$response);
500
        $this->set('_serialize', array_keys($response));
501
    }
502
503
    /**
504
     * Retrieve URL to get objects available for a relation
505
     *
506
     * @param string $relation The relation name.
507
     * @return string
508
     */
509
    protected function availableRelationshipsUrl(string $relation): string
510
    {
511
        $defaults = [
512
            'children' => '/objects',
513
            'parent' => '/folders',
514
            'parents' => '/folders',
515
        ];
516
        $defaultUrl = (string)Hash::get($defaults, $relation);
517
        if (!empty($defaultUrl)) {
518
            return $defaultUrl;
519
        }
520
521
        $relationsSchema = $this->Schema->getRelationsSchema();
522
        $types = $this->Modules->relatedTypes($relationsSchema, $relation);
523
        if (count($types) === 1) {
524
            return sprintf('/%s', $types[0]);
525
        }
526
527
        return '/objects?filter[type][]=' . implode('&filter[type][]=', $types);
528
    }
529
530
    /**
531
     * Bulk change actions for objects
532
     *
533
     * @return \Cake\Http\Response|null
534
     */
535
    public function bulkActions(): ?Response
536
    {
537
        $requestData = $this->request->getData();
538
        $this->request->allowMethod(['post']);
539
540
        if (!empty($requestData['ids'] && is_string($requestData['ids']))) {
541
            $ids = $requestData['ids'];
542
            $errors = [];
543
544
            // extract valid attributes to change
545
            $attributes = array_filter(
546
                $requestData['attributes'],
547
                function ($value) {
548
                    return ($value !== null && $value !== '');
549
                }
550
            );
551
552
            // export selected (filter by id)
553
            $ids = explode(',', $ids);
554
            foreach ($ids as $id) {
555
                $data = array_merge($attributes, ['id' => $id]);
556
                try {
557
                    $this->apiClient->save($this->objectType, $data);
558
                } catch (BEditaClientException $e) {
559
                    $errors[] = [
560
                        'id' => $id,
561
                        'message' => $e->getAttributes(),
562
                    ];
563
                }
564
            }
565
566
            // if errors occured on any single save show error message
567
            if (!empty($errors)) {
568
                $this->log($errors, LogLevel::ERROR);
569
                $this->Flash->error(__('Bulk Action failed on: '), ['params' => $errors]);
570
            }
571
        }
572
573
        return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType, '?' => $this->request->getQuery()]);
574
    }
575
576
    /**
577
     * get object properties and format them for index
578
     *
579
     * @param string $objectType objecte type name
580
     *
581
     * @return array $schema
582
     */
583
    public function getSchemaForIndex($objectType): array
584
    {
585
        $schema = (array)$this->Schema->getSchema($objectType);
586
587
        // if prop is an enum then prepend an empty string for select element
588
        if (!empty($schema['properties'])) {
589
            foreach ($schema['properties'] as &$property) {
590
                if (isset($property['enum'])) {
591
                    array_unshift($property['enum'], '');
592
                }
593
            }
594
        }
595
596
        return $schema;
597
    }
598
}
599