Completed
Push — master ( b31119...230834 )
by Stefano
23s queued 11s
created

ModulesController::getThumbsUrls()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 6
nop 1
dl 0
loc 29
rs 9.1111
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\ProjectConfigurationComponent $ProjectConfiguration
27
 * @property \App\Controller\Component\PropertiesComponent $Properties
28
 */
29
class ModulesController extends AppController
30
{
31
    protected const FIXED_RELATIONSHIPS = [
32
        'parent',
33
        'children',
34
        'parents',
35
        'translations',
36
        'streams',
37
        'roles',
38
    ];
39
40
    /**
41
     * Object type currently used
42
     *
43
     * @var string
44
     */
45
    protected $objectType = null;
46
47
    /**
48
     * {@inheritDoc}
49
     */
50
    public function initialize() : void
51
    {
52
        parent::initialize();
53
54
        $this->loadComponent('Properties');
55
        $this->loadComponent('ProjectConfiguration');
56
57
        if (!empty($this->request)) {
58
            $this->objectType = $this->request->getParam('object_type');
59
            $this->Modules->setConfig('currentModuleName', $this->objectType);
60
            $this->Schema->setConfig('type', $this->objectType);
61
        }
62
63
        $this->Security->setConfig('unlockedActions', ['saveJson']);
64
    }
65
66
    /**
67
     * {@inheritDoc}
68
     * @codeCoverageIgnore
69
     */
70
    public function beforeRender(Event $event) : ?Response
71
    {
72
        $this->set('objectType', $this->objectType);
73
74
        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...
75
    }
76
77
    /**
78
     * Display resources list.
79
     *
80
     * @return \Cake\Http\Response|null
81
     */
82
    public function index() : ?Response
83
    {
84
        $this->request->allowMethod(['get']);
85
86
        // handle filter and query parameters using session
87
        $result = $this->applySessionFilter();
88
        if ($result != null) {
89
            return $result;
90
        }
91
92
        try {
93
            $response = $this->apiClient->getObjects($this->objectType, $this->request->getQueryParams());
94
        } catch (BEditaClientException $e) {
95
            $this->log($e, LogLevel::ERROR);
96
            $this->Flash->error($e->getMessage(), ['params' => $e]);
97
            // remove session filter to avoid error repetition
98
            $session = $this->request->getSession();
99
            $session->delete(sprintf('%s.filter', $this->Modules->getConfig('currentModuleName')));
100
101
            return $this->redirect(['_name' => 'dashboard']);
102
        }
103
104
        $this->ProjectConfiguration->read();
105
106
        $objects = (array)$response['data'];
107
        $this->set('objects', $objects);
108
        $this->set('meta', (array)$response['meta']);
109
        $this->set('links', (array)$response['links']);
110
        $this->set('types', ['right' => $this->descendants()]);
111
112
        if (!empty($this->request->getQueryParams()['autocomplete'])) {
113
            $this->render('autocomplete');
114
        }
115
116
        $this->set('properties', $this->Properties->indexList($this->objectType));
117
118
        // base/custom filters for filter view
119
        $this->set('filter', $this->Properties->filterList($this->objectType));
120
121
        // base/custom bulk actions for index view
122
        $this->set('bulkActions', $this->Properties->bulkList($this->objectType));
123
124
        // objectTypes schema
125
        $this->set('schema', $this->getSchemaForIndex($this->objectType));
126
127
        // set prevNext for views navigations
128
        $this->setObjectNav($objects);
129
130
        return null;
131
    }
132
133
    /**
134
     * Retrieve descendants of `$this->objectType` if any
135
     *
136
     * @return array
137
     */
138
    protected function descendants() : array
139
    {
140
        if (!$this->Modules->isAbstract($this->objectType)) {
141
            return [];
142
        }
143
        $filter = [
144
            'parent' => $this->objectType,
145
            'enabled' => true,
146
        ];
147
        $sort = 'name';
148
149
        try {
150
            $descendants = $this->apiClient->get('/model/object_types', compact('filter', 'sort') + ['fields' => 'name']);
151
        } catch (BEditaClientException $e) {
152
            // Error! Return empty list.
153
            $this->log($e, LogLevel::ERROR);
154
155
            return [];
156
        }
157
158
        return (array)Hash::extract($descendants, 'data.{n}.attributes.name');
159
    }
160
161
    /**
162
     * View single resource.
163
     *
164
     * @param string|int $id Resource ID.
165
     * @return \Cake\Http\Response|null
166
     */
167
    public function view($id) : ?Response
168
    {
169
        $this->request->allowMethod(['get']);
170
171
        try {
172
            $response = $this->apiClient->getObject($id, $this->objectType);
173
        } catch (BEditaClientException $e) {
174
            // Error! Back to index.
175
            $this->log($e, LogLevel::ERROR);
176
            $this->Flash->error(__('Error retrieving the requested content'), ['params' => $e]);
177
178
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
179
        }
180
        $this->ProjectConfiguration->read();
181
182
        $revision = Hash::get($response, 'meta.schema.' . $this->objectType . '.revision', null);
183
        $schema = $this->Schema->getSchema($this->objectType, $revision);
184
185
        $object = $response['data'];
186
        $included = (!empty($response['included'])) ? $response['included'] : [];
187
        $this->set(compact('object', 'included', 'schema'));
188
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
189
190
        // relatinos between objects
191
        $relationsSchema = array_intersect_key($this->Schema->getRelationsSchema(), $object['relationships']);
192
        // relations between objects and resources
193
        $resourceRelations = array_diff(array_keys($object['relationships']), array_keys($relationsSchema), self::FIXED_RELATIONSHIPS);
194
195
        $this->set(compact('relationsSchema', 'resourceRelations'));
196
        $this->set('objectRelations', array_keys($relationsSchema));
197
198
        // set objectNav
199
        $objectNav = $this->getObjectNav((string)$id);
200
        $this->set('objectNav', $objectNav);
201
202
        return null;
203
    }
204
205
    /**
206
     * View single resource by id, doing a proper redirect (302) to resource module view by type.
207
     * If no resource found by ID, redirect to referer.
208
     *
209
     * @param string|int $id Resource ID.
210
     * @return \Cake\Http\Response|null
211
     */
212
    public function uname($id) : ?Response
213
    {
214
        try {
215
            $response = $this->apiClient->get(sprintf('/objects/%s', $id));
216
        } catch (BEditaClientException $e) {
217
            if ($e->getCode() === 404) {
218
                $error = sprintf(__('Resource "%s" not found', true), $id);
219
            } else {
220
                $error = sprintf(__('Resource "%s" not available. Error: %s', true), $id, $e->getMessage());
221
            }
222
            $this->Flash->error($error);
223
224
            return $this->redirect($this->referer());
225
        }
226
        $_name = 'modules:view';
227
        $object_type = $response['data']['type'];
228
        $id = $response['data']['id'];
229
230
        return $this->redirect(compact('_name', 'object_type', 'id'));
231
    }
232
233
    /**
234
     * Display new resource form.
235
     *
236
     * @return \Cake\Http\Response|null
237
     */
238
    public function create() : ?Response
239
    {
240
        $this->viewBuilder()->setTemplate('view');
241
242
        // Create stub object with empty `attributes`.
243
        $schema = $this->Schema->getSchema();
244
        if (!is_array($schema)) {
245
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
246
247
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
248
        }
249
        $attributes = array_fill_keys(
250
            array_keys(
251
                array_filter(
252
                    $schema['properties'],
253
                    function ($schema) {
254
                        return empty($schema['readOnly']);
255
                    }
256
                )
257
            ),
258
            ''
259
        );
260
        $object = [
261
            'type' => $this->objectType,
262
            'attributes' => $attributes,
263
        ];
264
265
        $this->set(compact('object', 'schema'));
266
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
267
268
        return null;
269
    }
270
271
    /**
272
     * Create or edit single resource.
273
     *
274
     * @return \Cake\Http\Response|null
275
     */
276
    public function save() : ?Response
277
    {
278
        $this->request->allowMethod(['post']);
279
        $requestData = $this->prepareRequest($this->objectType);
280
281
        try {
282
            if (!empty($requestData['_api'])) {
283
                foreach ($requestData['_api'] as $api) {
284
                    extract($api); // method, id, type, relation, relatedIds
285
                    if (in_array($method, ['addRelated', 'removeRelated', 'replaceRelated'])) {
286
                        $this->apiClient->{$method}($id, $this->objectType, $relation, $relatedIds);
287
                    }
288
                }
289
            }
290
            unset($requestData['_api']);
291
292
            // upload file (if available)
293
            $this->Modules->upload($requestData);
294
295
            // save data
296
            $response = $this->apiClient->save($this->objectType, $requestData);
297
        } catch (InternalErrorException | BEditaClientException | UploadException $e) {
298
            // Error! Back to object view or index.
299
            $this->log($e, LogLevel::ERROR);
300
            $this->Flash->error($e->getMessage(), ['params' => $e]);
301
302
            if ($this->request->getData('id')) {
303
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->request->getData('id')]);
304
            }
305
306
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
307
        }
308
309
        // annoying message removed, restore with https://github.com/bedita/manager/issues/71
310
        // $this->Flash->success(__('Object saved'));
311
312
        return $this->redirect([
313
            '_name' => 'modules:view',
314
            'object_type' => $this->objectType,
315
            'id' => Hash::get($response, 'data.id'),
316
        ]);
317
    }
318
319
    /**
320
     * Create new object from ajax request.
321
     *
322
     * @return void
323
     */
324
    public function saveJson() : void
325
    {
326
        $this->viewBuilder()->setClassName('Json'); // force json response
327
        $this->request->allowMethod(['post']);
328
        $requestData = $this->prepareRequest($this->objectType);
329
330
        try {
331
            // upload file (if available)
332
            $this->Modules->upload($requestData);
333
334
            // save data
335
            $response = $this->apiClient->save($this->objectType, $requestData);
336
        } catch (BEditaClientException $error) {
337
            $this->log($error, LogLevel::ERROR);
338
339
            $this->set(compact('error'));
340
            $this->set('_serialize', ['error']);
341
342
            return;
343
        }
344
        if ($response['data']) {
345
            $response['data'] = [ $response['data'] ];
346
        }
347
348
        $this->getThumbsUrls($response);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $response of App\Controller\ModulesController::getThumbsUrls() 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

348
        $this->getThumbsUrls(/** @scrutinizer ignore-type */ $response);
Loading history...
349
350
        $this->set((array)$response);
351
        $this->set('_serialize', array_keys($response));
352
    }
353
354
    /**
355
     * Clone single object.
356
     *
357
     * @param string|int $id Object ID.
358
     * @return \Cake\Http\Response|null
359
     */
360
    public function clone($id) : ?Response
361
    {
362
        $this->viewBuilder()->setTemplate('view');
363
364
        $schema = $this->Schema->getSchema();
365
        if (!is_array($schema)) {
366
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
367
368
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
369
        }
370
        try {
371
            $response = $this->apiClient->getObject($id, $this->objectType);
372
            $attributes = $response['data']['attributes'];
373
            $attributes['uname'] = '';
374
            unset($attributes['relationships']);
375
            $attributes['title'] = $this->request->getQuery('title');
376
        } catch (BEditaClientException $e) {
377
            $this->log($e, LogLevel::ERROR);
378
            $this->Flash->error($e->getMessage(), ['params' => $e]);
379
380
            return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
381
        }
382
        $object = [
383
            'type' => $this->objectType,
384
            'attributes' => $attributes,
385
        ];
386
        $this->set(compact('object', 'schema'));
387
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
388
389
        return null;
390
    }
391
392
    /**
393
     * Delete single resource.
394
     *
395
     * @return \Cake\Http\Response|null
396
     */
397
    public function delete() : ?Response
398
    {
399
        $this->request->allowMethod(['post']);
400
        $ids = [];
401
        if (!empty($this->request->getData('ids'))) {
402
            if (is_string($this->request->getData('ids'))) {
403
                $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; 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

403
                $ids = explode(',', /** @scrutinizer ignore-type */ $this->request->getData('ids'));
Loading history...
404
            }
405
        } else {
406
            $ids = [$this->request->getData('id')];
407
        }
408
        foreach ($ids as $id) {
409
            try {
410
                $this->apiClient->deleteObject($id, $this->objectType);
411
            } catch (BEditaClientException $e) {
412
                $this->log($e, LogLevel::ERROR);
413
                $this->Flash->error($e->getMessage(), ['params' => $e]);
414
                if (!empty($this->request->getData('id'))) {
415
                    return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->request->getData('id')]);
416
                }
417
418
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType]);
419
            }
420
        }
421
        $this->Flash->success(__('Object(s) deleted'));
422
423
        return $this->redirect([
424
            '_name' => 'modules:list',
425
            'object_type' => $this->objectType,
426
        ]);
427
    }
428
429
    /**
430
     * Relation data load callig api `GET /:object_type/:id/related/:relation`
431
     *
432
     * @param string|int $id the object identifier.
433
     * @param string $relation the relating name.
434
     * @return void
435
     */
436
    public function relatedJson($id, string $relation) : void
437
    {
438
        $this->request->allowMethod(['get']);
439
        try {
440
            $response = $this->apiClient->getRelated($id, $this->objectType, $relation, $this->request->getQueryParams());
441
        } catch (BEditaClientException $error) {
442
            $this->log($error, LogLevel::ERROR);
443
444
            $this->set(compact('error'));
445
            $this->set('_serialize', ['error']);
446
447
            return;
448
        }
449
450
        $this->getThumbsUrls($response);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $response of App\Controller\ModulesController::getThumbsUrls() 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

450
        $this->getThumbsUrls(/** @scrutinizer ignore-type */ $response);
Loading history...
451
452
        $this->set((array)$response);
453
        $this->set('_serialize', array_keys($response));
454
    }
455
456
    /**
457
     * Load resources of $type callig api `GET /:type/`
458
     * Json response
459
     *
460
     * @param string|int $id the object identifier.
461
     * @param string $type the resource type name.
462
     * @return void
463
     */
464
    public function resourcesJson($id, string $type) : void
465
    {
466
        $this->request->allowMethod(['get']);
467
468
        try {
469
            $response = $this->apiClient->get($type, $this->request->getQueryParams());
470
        } catch (BEditaClientException $error) {
471
            $this->log($error, LogLevel::ERROR);
472
473
            $this->set(compact('error'));
474
            $this->set('_serialize', ['error']);
475
476
            return;
477
        }
478
479
        $this->set((array)$response);
480
        $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 $input 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

480
        $this->set('_serialize', array_keys(/** @scrutinizer ignore-type */ $response));
Loading history...
481
    }
482
483
    /**
484
     * Relation data load callig api `GET /:object_type/:id/relationships/:relation`
485
     * Json response
486
     *
487
     * @param string|int $id the object identifier.
488
     * @param string $relation the relating name.
489
     * @return void
490
     */
491
    public function relationshipsJson($id, string $relation) : void
492
    {
493
        $this->request->allowMethod(['get']);
494
        $path = sprintf('/%s/%s/%s', $this->objectType, $id, $relation);
495
496
        try {
497
            switch ($relation) {
498
                case 'children':
499
                    $available = '/objects';
500
                    break;
501
                case 'parent':
502
                case 'parents':
503
                    $available = '/folders';
504
                    break;
505
                default:
506
                    $response = $this->apiClient->get($path, ['page_size' => 1]); // page_size 1: we need just the available
507
                    $available = $response['links']['available'];
508
            }
509
510
            $response = $this->apiClient->get($available, $this->request->getQueryParams());
511
512
            $this->getThumbsUrls($response);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $response of App\Controller\ModulesController::getThumbsUrls() 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

512
            $this->getThumbsUrls(/** @scrutinizer ignore-type */ $response);
Loading history...
513
        } catch (BEditaClientException $ex) {
514
            $this->log($ex, LogLevel::ERROR);
515
516
            $this->set([
517
                'error' => $ex->getMessage(),
518
                '_serialize' => ['error'],
519
            ]);
520
521
            return;
522
        }
523
524
        $this->set((array)$response);
525
        $this->set('_serialize', array_keys($response));
526
    }
527
528
    /**
529
     * Retrieve thumbnails URL of related objects in `meta.url` if present.
530
     *
531
     * @param array $response Related objects response.
532
     * @return void
533
     */
534
    public function getThumbsUrls(array &$response) : void
535
    {
536
        if (empty($response['data'])) {
537
            return;
538
        }
539
540
        // extract ids of objects
541
        $ids = (array)Hash::extract($response, 'data.{n}[type=/images|videos/].id');
542
        if (empty($ids)) {
543
            return;
544
        }
545
546
        $thumbs = '/media/thumbs?ids=' . implode(',', $ids) . '&options[w]=400'; // TO-DO this hardcoded 400 should be in param/conf of some sort
547
548
        $thumbsResponse = $this->apiClient->get($thumbs, $this->request->getQueryParams());
549
550
        $thumbsUrl = $thumbsResponse['meta']['thumbnails'];
551
552
        foreach ($response['data'] as &$object) {
553
            $thumbnail = Hash::get($object, 'attributes.provider_thumbnail');
554
            if ($thumbnail) {
555
                $object['meta']['thumb_url'] = $thumbnail;
556
                continue; // if provider_thumbnail is found there's no need to extract it from thumbsResponse
557
            }
558
559
            // extract url of the matching objectid's thumb
560
            $thumbnail = (array)Hash::extract($thumbsUrl, sprintf('{*}[id=%s].url', $object['id']));
561
            if (count($thumbnail)) {
562
                $object['meta']['thumb_url'] = $thumbnail[0];
563
            }
564
        }
565
    }
566
567
    /**
568
     * Bulk change actions for objects
569
     *
570
     * @return \Cake\Http\Response|null
571
     */
572
    public function bulkActions() : ?Response
573
    {
574
        $requestData = $this->request->getData();
575
        $this->request->allowMethod(['post']);
576
577
        if (!empty($requestData['ids'] && is_string($requestData['ids']))) {
578
            $ids = $requestData['ids'];
579
            $errors = [];
580
581
            // extract valid attributes to change
582
            $attributes = array_filter(
583
                $requestData['attributes'],
584
                function ($value) {
585
                    return ($value !== null && $value !== '');
586
                }
587
            );
588
589
            // export selected (filter by id)
590
            $ids = explode(',', $ids);
591
            foreach ($ids as $id) {
592
                $data = array_merge($attributes, ['id' => $id]);
593
                try {
594
                    $this->apiClient->save($this->objectType, $data);
595
                } catch (BEditaClientException $e) {
596
                    $errors[] = [
597
                        'id' => $id,
598
                        'message' => $e->getAttributes()
599
                    ];
600
                }
601
            }
602
603
            // if errors occured on any single save show error message
604
            if (!empty($errors)) {
605
                $this->log($errors, LogLevel::ERROR);
606
                $this->Flash->error(__('Bulk Action failed on: '), ['params' => $errors]);
607
            }
608
        }
609
610
        return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType, '?' => $this->request->getQuery()]);
611
    }
612
613
    /**
614
     * get object properties and format them for index
615
     *
616
     * @param string $objectType objecte type name
617
     *
618
     * @return array $schema
619
     */
620
    public function getSchemaForIndex($objectType) : array
621
    {
622
        $schema = (array)$this->Schema->getSchema($objectType);
623
624
        // if prop is an enum then prepend an empty string for select element
625
        if (!empty($schema['properties'])) {
626
            foreach ($schema['properties'] as &$property) {
627
                if (isset($property['enum'])) {
628
                    array_unshift($property['enum'], '');
629
                }
630
            }
631
        }
632
633
        return $schema;
634
    }
635
}
636