1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* BEdita, API-first content management framework |
4
|
|
|
* Copyright 2019 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\WebTools\ApiClientProvider; |
16
|
|
|
use Cake\Controller\Controller; |
17
|
|
|
use Cake\Core\Configure; |
18
|
|
|
use Cake\Event\Event; |
19
|
|
|
use Cake\Http\Exception\BadRequestException; |
20
|
|
|
use Cake\Http\Response; |
21
|
|
|
use Cake\Utility\Hash; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Base Application Controller. |
25
|
|
|
* |
26
|
|
|
* @property \App\Controller\Component\ModulesComponent $Modules |
27
|
|
|
* @property \App\Controller\Component\SchemaComponent $Schema |
28
|
|
|
*/ |
29
|
|
|
class AppController extends Controller |
30
|
|
|
{ |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* BEdita4 API client |
34
|
|
|
* |
35
|
|
|
* @var \BEdita\SDK\BEditaClient |
36
|
|
|
*/ |
37
|
|
|
protected $apiClient = null; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* {@inheritDoc} |
41
|
|
|
*/ |
42
|
|
|
public function initialize() : void |
43
|
|
|
{ |
44
|
|
|
parent::initialize(); |
45
|
|
|
|
46
|
|
|
$this->loadComponent('RequestHandler', ['enableBeforeRedirect' => false]); |
47
|
|
|
$this->loadComponent('App.Flash', ['clear' => true]); |
48
|
|
|
$this->loadComponent('Security'); |
49
|
|
|
|
50
|
|
|
$options = ['Log' => (array)Configure::read('API.log', [])]; |
51
|
|
|
$this->apiClient = ApiClientProvider::getApiClient($options); |
52
|
|
|
|
53
|
|
|
$this->loadComponent('Auth', [ |
54
|
|
|
'authenticate' => [ |
55
|
|
|
'BEdita/WebTools.Api' => [], |
56
|
|
|
], |
57
|
|
|
'loginAction' => ['_name' => 'login'], |
58
|
|
|
'loginRedirect' => ['_name' => 'dashboard'], |
59
|
|
|
]); |
60
|
|
|
|
61
|
|
|
$this->Auth->deny(); |
62
|
|
|
|
63
|
|
|
$this->loadComponent('Modules', [ |
64
|
|
|
'currentModuleName' => $this->name, |
65
|
|
|
]); |
66
|
|
|
$this->loadComponent('Schema'); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* {@inheritDoc} |
71
|
|
|
*/ |
72
|
|
|
public function beforeFilter(Event $event) : ?Response |
73
|
|
|
{ |
74
|
|
|
$tokens = $this->Auth->user('tokens'); |
75
|
|
|
if (!empty($tokens)) { |
76
|
|
|
$this->apiClient->setupTokens($tokens); |
77
|
|
|
} elseif (!in_array($this->request->getPath(), ['/login'])) { |
78
|
|
|
$route = ['_name' => 'login']; |
79
|
|
|
$redirect = $this->request->getUri()->getPath(); |
80
|
|
|
if ($redirect !== $this->request->getAttribute('webroot')) { |
81
|
|
|
$route += compact('redirect'); |
82
|
|
|
} |
83
|
|
|
$this->Flash->error(__('Login required')); |
84
|
|
|
|
85
|
|
|
return $this->redirect($route); |
86
|
|
|
} |
87
|
|
|
$this->setupOutputTimezone(); |
88
|
|
|
|
89
|
|
|
return null; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Setup output timezone from user session |
94
|
|
|
* |
95
|
|
|
* @return void |
96
|
|
|
*/ |
97
|
|
|
protected function setupOutputTimezone(): void |
98
|
|
|
{ |
99
|
|
|
$timezone = $this->Auth->user('timezone'); |
100
|
|
|
if ($timezone) { |
101
|
|
|
Configure::write('I18n.timezone', $timezone); |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* {@inheritDoc} |
107
|
|
|
* |
108
|
|
|
* Update session tokens if updated/refreshed by client |
109
|
|
|
*/ |
110
|
|
|
public function beforeRender(Event $event) : ?Response |
111
|
|
|
{ |
112
|
|
|
if ($this->Auth && $this->Auth->user()) { |
113
|
|
|
$user = $this->Auth->user(); |
114
|
|
|
$tokens = $this->apiClient->getTokens(); |
115
|
|
|
if ($tokens && $user['tokens'] !== $tokens) { |
|
|
|
|
116
|
|
|
// Update tokens in session. |
117
|
|
|
$user['tokens'] = $tokens; |
118
|
|
|
$this->Auth->setUser($user); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
$this->set(compact('user')); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
$this->viewBuilder()->setTemplatePath('Pages/' . $this->name); |
125
|
|
|
|
126
|
|
|
return null; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Prepare request, set properly json data. |
131
|
|
|
* |
132
|
|
|
* @param string $type Object type |
133
|
|
|
* @return array request data |
134
|
|
|
*/ |
135
|
|
|
protected function prepareRequest($type) : array |
136
|
|
|
{ |
137
|
|
|
// prepare json fields before saving |
138
|
|
|
$data = (array)$this->request->getData(); |
139
|
|
|
|
140
|
|
|
// when saving users, if password is empty, unset it |
141
|
|
|
if ($type === 'users' && array_key_exists('password', $data) && empty($data['password'])) { |
142
|
|
|
unset($data['password']); |
143
|
|
|
unset($data['confirm-password']); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
if (!empty($data['_jsonKeys'])) { |
147
|
|
|
$keys = explode(',', $data['_jsonKeys']); |
148
|
|
|
foreach ($keys as $key) { |
149
|
|
|
$data[$key] = json_decode($data[$key], true); |
150
|
|
|
} |
151
|
|
|
unset($data['_jsonKeys']); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
// relations data for view/save - prepare api calls |
155
|
|
|
if (!empty($data['relations'])) { |
156
|
|
|
$api = []; |
157
|
|
|
foreach ($data['relations'] as $relation => $relationData) { |
158
|
|
|
$id = $data['id']; |
159
|
|
|
|
160
|
|
|
foreach ($relationData as $method => $ids) { |
161
|
|
|
$relatedIds = json_decode($ids, true); |
162
|
|
|
if (!empty($relatedIds)) { |
163
|
|
|
$api[] = compact('method', 'id', 'relation', 'relatedIds'); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
$data['_api'] = $api; |
168
|
|
|
} |
169
|
|
|
unset($data['relations']); |
170
|
|
|
|
171
|
|
|
// prepare attributes: only modified attributes |
172
|
|
|
if (!empty($data['_actualAttributes'])) { |
173
|
|
|
$attributes = json_decode($data['_actualAttributes'], true); |
174
|
|
|
foreach ($attributes as $key => $value) { |
175
|
|
|
// remove unchanged attributes from $data |
176
|
|
|
if (isset($data[$key]) && !$this->hasFieldChanged($value, $data[$key])) { |
177
|
|
|
unset($data[$key]); |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
unset($data['_actualAttributes']); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
return $data; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Return true if $value1 equals $value2 or both are empty (null|'') |
188
|
|
|
* |
189
|
|
|
* @param mixed $value1 The first value | field value in model data (db) |
190
|
|
|
* @param mixed $value2 The second value | field value from form |
191
|
|
|
* @return bool |
192
|
|
|
*/ |
193
|
|
|
protected function hasFieldChanged($value1, $value2) |
194
|
|
|
{ |
195
|
|
|
if (($value1 === null || $value1 === '') && ($value2 === null || $value2 === '')) { |
196
|
|
|
return false; |
197
|
|
|
} |
198
|
|
|
if (is_bool($value1) && !is_bool($value2)) { // i.e. true / "1" |
199
|
|
|
return $value1 !== boolval($value2); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
return $value1 !== $value2; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Check request data by options. |
207
|
|
|
* |
208
|
|
|
* - $options['allowedMethods']: check allowed method(s) |
209
|
|
|
* - $options['requiredParameters']: check required parameter(s) |
210
|
|
|
* |
211
|
|
|
* @param array $options The options for request check(s) |
212
|
|
|
* @return array The request data for required parameters, if any |
213
|
|
|
* @throws Cake\Http\Exception\BadRequestException on empty request or empty data by parameter |
214
|
|
|
*/ |
215
|
|
|
protected function checkRequest(array $options = []) : array |
216
|
|
|
{ |
217
|
|
|
// check request |
218
|
|
|
if (empty($this->request)) { |
219
|
|
|
throw new BadRequestException('Empty request'); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
// check allowed methods |
223
|
|
|
if (!empty($options['allowedMethods'])) { |
224
|
|
|
$this->request->allowMethod($options['allowedMethods']); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
// check request required parameters, if any |
228
|
|
|
$data = []; |
229
|
|
|
if (!empty($options['requiredParameters'])) { |
230
|
|
|
foreach ($options['requiredParameters'] as $param) { |
231
|
|
|
$val = $this->request->getData($param); |
232
|
|
|
if (empty($val)) { |
233
|
|
|
throw new BadRequestException(sprintf('Empty %s', $param)); |
234
|
|
|
} |
235
|
|
|
$data[$param] = $val; |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
return $data; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Apply session filter (if any): if found, redirect properly. |
244
|
|
|
* Session key: '{$currentModuleName}.filter' |
245
|
|
|
* Scenarios: |
246
|
|
|
* |
247
|
|
|
* Query parameter 'reset=1': remove session key and redirect |
248
|
|
|
* Query parameters found: write them on session with proper key ({currentModuleName}.filter) |
249
|
|
|
* Session data for session key: build uri from session data and redirect to new uri. |
250
|
|
|
* |
251
|
|
|
* @return \Cake\Http\Response|null |
252
|
|
|
*/ |
253
|
|
|
protected function applySessionFilter() : ?Response |
254
|
|
|
{ |
255
|
|
|
$session = $this->request->getSession(); |
256
|
|
|
$sessionKey = sprintf('%s.filter', $this->Modules->getConfig('currentModuleName')); |
257
|
|
|
|
258
|
|
|
// if reset request, delete session data by key and redirect to proper uri |
259
|
|
|
if ($this->request->getQuery('reset') === '1') { |
260
|
|
|
$session->delete($sessionKey); |
261
|
|
|
|
262
|
|
|
return $this->redirect((string)$this->request->getUri()->withQuery('')); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
// write request query parameters (if any) in session |
266
|
|
|
if (!empty($this->request->getQueryParams())) { |
267
|
|
|
$session->write($sessionKey, $this->request->getQueryParams()); |
268
|
|
|
|
269
|
|
|
return null; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
// read request query parameters from session and redirect to proper page |
273
|
|
|
if ($session->check($sessionKey)) { |
274
|
|
|
$query = http_build_query($session->read($sessionKey), null, '&', PHP_QUERY_RFC3986); |
275
|
|
|
|
276
|
|
|
return $this->redirect((string)$this->request->getUri()->withQuery($query)); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
return null; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Set objectNav array and objectNavModule. |
284
|
|
|
* Objects can be in different modules: |
285
|
|
|
* |
286
|
|
|
* - a document is in "documents" and "objects" index |
287
|
|
|
* - an image is in "images" and "media" index |
288
|
|
|
* - etc. |
289
|
|
|
* |
290
|
|
|
* The session variable objectNavModule stores the last module index visited; |
291
|
|
|
* this is used then in controller view, to obtain the proper object nav (@see \App\Controller\AppController::getObjectNav) |
292
|
|
|
* |
293
|
|
|
* @param array $objects The objects to parse to set prev and next data |
294
|
|
|
* @return void |
295
|
|
|
*/ |
296
|
|
|
protected function setObjectNav($objects) : void |
297
|
|
|
{ |
298
|
|
|
$moduleName = $this->Modules->getConfig('currentModuleName'); |
299
|
|
|
$total = count(array_keys($objects)); |
300
|
|
|
$objectNav = []; |
301
|
|
|
foreach ($objects as $i => $object) { |
302
|
|
|
$objectNav[$moduleName][$object['id']] = [ |
303
|
|
|
'prev' => ($i > 0) ? Hash::get($objects, sprintf('%d.id', $i - 1)) : null, |
304
|
|
|
'next' => ($i + 1 < $total) ? Hash::get($objects, sprintf('%d.id', $i + 1)) : null, |
305
|
|
|
'index' => $i + 1, |
306
|
|
|
'total' => $total, |
307
|
|
|
'object_type' => Hash::get($objects, sprintf('%d.object_type', $i)), |
308
|
|
|
]; |
309
|
|
|
} |
310
|
|
|
$session = $this->request->getSession(); |
311
|
|
|
$session->write('objectNav', $objectNav); |
312
|
|
|
$session->write('objectNavModule', $moduleName); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Get objectNav for ID and current module name |
317
|
|
|
* |
318
|
|
|
* @param string $id The object ID |
319
|
|
|
* @return array |
320
|
|
|
*/ |
321
|
|
|
protected function getObjectNav($id) : array |
322
|
|
|
{ |
323
|
|
|
// get objectNav from session |
324
|
|
|
$session = $this->request->getSession(); |
325
|
|
|
$objectNav = (array)$session->read('objectNav'); |
326
|
|
|
if (empty($objectNav)) { |
327
|
|
|
return []; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
// get objectNav by session objectNavModule |
331
|
|
|
$objectNavModule = (string)$session->read('objectNavModule'); |
332
|
|
|
|
333
|
|
|
return (array)Hash::get($objectNav, sprintf('%s.%s', $objectNavModule, $id), []); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Set session data for key 'failedSaveData'. |
338
|
|
|
* |
339
|
|
|
* @param array $data The data to store into 'failedSaveData'. |
340
|
|
|
* @return void |
341
|
|
|
*/ |
342
|
|
|
protected function setDataFromFailedSave($data) |
343
|
|
|
{ |
344
|
|
|
$session = $this->request->getSession(); |
345
|
|
|
$session->write('failedSaveData', $data); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Get data from session by key 'failedSaveData'. |
350
|
|
|
* If any, return it and delete session key 'failedSaveData'. |
351
|
|
|
* |
352
|
|
|
* @return array |
353
|
|
|
*/ |
354
|
|
|
protected function getDataFromFailedSave() : array |
355
|
|
|
{ |
356
|
|
|
$session = $this->request->getSession(); |
357
|
|
|
$data = (array)$session->read('failedSaveData'); |
358
|
|
|
if (empty($data)) { |
359
|
|
|
return []; |
360
|
|
|
} |
361
|
|
|
$session->delete('failedSaveData'); |
362
|
|
|
unset($data['id']); |
363
|
|
|
|
364
|
|
|
return [ 'attributes' => $data ]; |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.