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
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
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 |