Issues (219)

Branch: 4-cactus

BEdita/API/src/Controller/TreesController.php (2 issues)

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2020 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 BEdita\API\Controller;
14
15
use BEdita\Core\Model\Action\GetObjectAction;
16
use BEdita\Core\Model\Entity\ObjectType;
17
use Cake\Core\InstanceConfigTrait;
18
use Cake\Datasource\EntityInterface;
19
use Cake\Http\Exception\NotFoundException;
20
use Cake\ORM\Association;
21
use Cake\ORM\Table;
22
use Cake\ORM\TableRegistry;
23
use Cake\Utility\Hash;
24
25
/**
26
 * Controller for `/trees` endpoint.
27
 *
28
 * @since 4.2.0
29
 */
30
class TreesController extends AppController
31
{
32
    use InstanceConfigTrait;
33
34
    /**
35
     * Objects Table.
36
     *
37
     * @var \BEdita\Core\Model\Table\ObjectsTable
38
     */
39
    protected $Objects;
40
41
    /**
42
     * Trees Table.
43
     *
44
     * @var \BEdita\Core\Model\Table\TreesTable
45
     */
46
    protected $Trees;
47
48
    /**
49
     * Request object Table.
50
     *
51
     * @var \BEdita\Core\Model\Table\ObjectsBaseTable
52
     */
53
    protected $Table;
54
55
    /**
56
     * Path information with ID, object type and uname of each object
57
     * Associative array having keys:
58
     *  - 'ids': ID path list
59
     *  - 'unames': uname path list
60
     *  - 'types': object types id list
61
     *
62
     * @var array
63
     */
64
    protected $pathInfo = [
65
        'ids' => [],
66
        'unames' => [],
67
        'types' => [],
68
    ];
69
70
    /**
71
     * Available configurations are:
72
     *  - `allowedAssociations`: array of relationships of the loaded object
73
     *
74
     * @var array
75
     */
76
    protected $_defaultConfig = [
77
        'allowedAssociations' => [],
78
    ];
79
80
    /**
81
     * Trees node entity.
82
     *
83
     * @var \BEdita\Core\Model\Entity\Tree
84
     */
85
    protected $treesNode;
86
87
    /**
88
     * @inheritDoc
89
     */
90
    public function initialize(): void
91
    {
92
        parent::initialize();
93
94
        $this->Objects = TableRegistry::getTableLocator()->get('Objects');
95
        $this->Trees = TableRegistry::getTableLocator()->get('Trees');
96
    }
97
98
    /**
99
     * Display object on a given path
100
     *
101
     * @param string $path Trees path
102
     * @return \Cake\Http\Response|null
103
     */
104
    public function index(string $path)
105
    {
106
        $this->request->allowMethod(['get']);
107
108
        // populate idList, unameList
109
        $this->pathDetails($path);
110
111
        $this->loadTreesNode();
112
        $parents = $this->parents();
113
114
        $ids = array_values((array)$this->pathInfo['ids']);
115
        $entity = $this->loadObject(end($ids));
116
117
        $this->checkPath($entity, $parents);
118
119
        $entity->set('uname_path', sprintf('/%s', implode('/', $this->pathInfo['unames'])));
120
        $entity->setAccess('uname_path', false);
121
        $entity->set('menu', (bool)$this->treesNode->get('menu'));
122
123
        $this->set('_fields', $this->request->getQuery('fields', []));
124
        $this->set(compact('entity'));
125
        $this->setSerialize(['entity']);
126
127
        return null;
128
    }
129
130
    /**
131
     * Check path validity.
132
     *
133
     * @param \Cake\Datasource\EntityInterface $entity Object entity.
134
     * @param array $parents Parents ID array.
135
     * @return void
136
     */
137
    protected function checkPath(EntityInterface $entity, array $parents): void
138
    {
139
        if ($entity->get('type') === 'folders') {
140
            $idPath = sprintf('/%s', implode('/', $this->pathInfo['ids']));
141
            if ($entity->get('path') !== $idPath) {
142
                throw new NotFoundException(__d('bedita', 'Invalid path'));
143
            }
144
145
            return;
146
        }
147
148
        $pathFound = array_values($parents);
149
        $pathFound[] = (int)$entity->get('id');
150
        if ($this->pathInfo['ids'] !== $pathFound) {
151
            throw new NotFoundException(__d('bedita', 'Invalid path'));
152
        }
153
    }
154
155
    /**
156
     * Populate $pathInfo with path details on ID, uname and type:
157
     *
158
     * @param string $path Requesed object path
159
     * @return void
160
     */
161
    protected function pathDetails(string $path): void
162
    {
163
        $pathList = explode('/', $path);
164
        foreach ($pathList as $p) {
165
            if (is_numeric($p)) {
166
                $item = $this->objectDetails(['id' => (int)$p]);
167
            } else {
168
                $item = $this->objectDetails(['uname' => (string)$p]);
169
            }
170
            if (empty($item)) {
171
                throw new NotFoundException(__d('bedita', 'Invalid path'));
172
            }
173
            $this->pathInfo['ids'][] = $item['id'];
174
            $this->pathInfo['unames'][] = $item['uname'];
175
            $this->pathInfo['types'][] = $item['object_type_id'];
176
        }
177
    }
178
179
    /**
180
     * Get object main fields
181
     *
182
     * @param array $condition Query conditions
183
     * @return string
184
     */
185
    protected function objectDetails(array $condition): array
186
    {
187
        return (array)$this->Objects->find('available')
0 ignored issues
show
Bug Best Practice introduced by
The expression return (array)$this->Obj...bleHydration()->first() returns the type array which is incompatible with the documented return type string.
Loading history...
188
            ->where($condition)
189
            ->select(['id', 'uname', 'object_type_id'])
190
            ->disableHydration()
191
            ->first();
192
    }
193
194
    /**
195
     * Get parents object ID array and check object parent existence
196
     *
197
     * @return array
198
     */
199
    protected function parents(): array
200
    {
201
        $parentId = $this->treesNode->get('parent_id');
202
        if (empty($parentId)) {
203
            return [];
204
        }
205
206
        return $this->Trees->find('pathNodes', [$parentId])
207
            ->find('list', [
208
                'keyField' => 'id',
209
                'valueField' => 'object_id',
210
            ])
211
            ->toArray();
212
    }
213
214
    /**
215
     * Load trees table node of path object.
216
     *
217
     * @return void
218
     */
219
    protected function loadTreesNode(): void
220
    {
221
        $count = count($this->pathInfo['ids']);
222
223
        $id = Hash::get($this->pathInfo['ids'], $count - 1);
224
        $parentId = Hash::get($this->pathInfo['ids'], $count - 2);
225
226
        /** @var \BEdita\Core\Model\Entity\Tree|null $node */
227
        $node = $this->Trees->find()
228
            ->where([
229
                'object_id' => $id,
230
                'parent_id IS' => $parentId,
231
            ])
232
            ->first();
233
        if (empty($node)) {
234
            throw new NotFoundException(__d('bedita', 'Invalid path'));
235
        }
236
237
        $this->treesNode = $node;
238
    }
239
240
    /**
241
     * Load object entity
242
     *
243
     * @param int $id Object ID
244
     * @return \Cake\Datasource\EntityInterface
245
     */
246
    protected function loadObject(int $id): EntityInterface
247
    {
248
        $types = array_values($this->pathInfo['types']);
249
        /** @var \BEdita\Core\Model\Entity\ObjectType $objectType */
250
        $objectType = TableRegistry::getTableLocator()->get('ObjectTypes')->get(end($types));
251
        $this->Table = TableRegistry::getTableLocator()->get($objectType->get('alias'));
252
253
        $action = new GetObjectAction(['table' => $this->Table, 'objectType' => $objectType]);
254
255
        return $action([
256
            'primaryKey' => $id,
257
            'contain' => $this->getContain($objectType),
258
            'lang' => $this->request->getQuery('lang'),
259
        ]);
260
    }
261
262
    /**
263
     * Retrieve `contain` associations array
264
     *
265
     * @param \BEdita\Core\Model\Entity\ObjectType $objectType Object type entity
266
     * @return array
267
     */
268
    protected function getContain(ObjectType $objectType): array
269
    {
270
        $include = $this->request->getQuery('include');
271
        if (empty($include)) {
272
            return [];
273
        }
274
275
        $relations = array_keys($objectType->getRelations());
276
        $this->setConfig('allowedAssociations', $relations);
277
278
        return $this->prepareInclude($include);
279
    }
280
281
    /**
282
     * Find the association corresponding to the relationship name.
283
     *
284
     * @param string $relationship Relationship name.
285
     * @param \Cake\ORM\Table|null $table Table to consider.
286
     * @return \Cake\ORM\Association
287
     * @throws \Cake\Http\Exception\NotFoundException Throws an exception if no association could be found.
288
     */
289
    protected function findAssociation(string $relationship, ?Table $table = null): Association
290
    {
291
        if (in_array($relationship, $this->getConfig('allowedAssociations'))) {
0 ignored issues
show
It seems like $this->getConfig('allowedAssociations') can also be of type null; however, parameter $haystack of in_array() 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

291
        if (in_array($relationship, /** @scrutinizer ignore-type */ $this->getConfig('allowedAssociations'))) {
Loading history...
292
            $association = $this->Table->associations()->getByProperty($relationship);
293
            if ($association !== null) {
294
                return $association;
295
            }
296
        }
297
298
        throw new NotFoundException(__d('bedita', 'Relationship "{0}" does not exist', $relationship));
299
    }
300
}
301