Completed
Pull Request — master (#352)
by Dante
13:56
created

AppController::getDataFromFailedSave()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 11
rs 10
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tokens of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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