Completed
Pull Request — master (#352)
by Dante
01:17
created

AppController::getDataFromFailedSave()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 2
nc 2
nop 0
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 === $value2) || (($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
        if (is_string($value1) && is_string($value2) && strtotime($value1) && strtotime($value2)) {
202
            return strtotime($value1) !== strtotime($value2);
203
        }
204
205
        return true;
206
    }
207
208
    /**
209
     * Check request data by options.
210
     *
211
     *  - $options['allowedMethods']: check allowed method(s)
212
     *  - $options['requiredParameters']: check required parameter(s)
213
     *
214
     * @param array $options The options for request check(s)
215
     * @return array The request data for required parameters, if any
216
     * @throws Cake\Http\Exception\BadRequestException on empty request or empty data by parameter
217
     */
218
    protected function checkRequest(array $options = []) : array
219
    {
220
        // check request
221
        if (empty($this->request)) {
222
            throw new BadRequestException('Empty request');
223
        }
224
225
        // check allowed methods
226
        if (!empty($options['allowedMethods'])) {
227
            $this->request->allowMethod($options['allowedMethods']);
228
        }
229
230
        // check request required parameters, if any
231
        $data = [];
232
        if (!empty($options['requiredParameters'])) {
233
            foreach ($options['requiredParameters'] as $param) {
234
                $val = $this->request->getData($param);
235
                if (empty($val)) {
236
                    throw new BadRequestException(sprintf('Empty %s', $param));
237
                }
238
                $data[$param] = $val;
239
            }
240
        }
241
242
        return $data;
243
    }
244
245
    /**
246
     * Apply session filter (if any): if found, redirect properly.
247
     * Session key: '{$currentModuleName}.filter'
248
     * Scenarios:
249
     *
250
     * Query parameter 'reset=1': remove session key and redirect
251
     * Query parameters found: write them on session with proper key ({currentModuleName}.filter)
252
     * Session data for session key: build uri from session data and redirect to new uri.
253
     *
254
     * @return \Cake\Http\Response|null
255
     */
256
    protected function applySessionFilter() : ?Response
257
    {
258
        $session = $this->request->getSession();
259
        $sessionKey = sprintf('%s.filter', $this->Modules->getConfig('currentModuleName'));
260
261
        // if reset request, delete session data by key and redirect to proper uri
262
        if ($this->request->getQuery('reset') === '1') {
263
            $session->delete($sessionKey);
264
265
            return $this->redirect((string)$this->request->getUri()->withQuery(''));
266
        }
267
268
        // write request query parameters (if any) in session
269
        if (!empty($this->request->getQueryParams())) {
270
            $session->write($sessionKey, $this->request->getQueryParams());
271
272
            return null;
273
        }
274
275
        // read request query parameters from session and redirect to proper page
276
        if ($session->check($sessionKey)) {
277
            $query = http_build_query($session->read($sessionKey), null, '&', PHP_QUERY_RFC3986);
278
279
            return $this->redirect((string)$this->request->getUri()->withQuery($query));
280
        }
281
282
        return null;
283
    }
284
285
    /**
286
     * Set objectNav array and objectNavModule.
287
     * Objects can be in different modules:
288
     *
289
     *  - a document is in "documents" and "objects" index
290
     *  - an image is in "images" and "media" index
291
     *  - etc.
292
     *
293
     * The session variable objectNavModule stores the last module index visited;
294
     * this is used then in controller view, to obtain the proper object nav (@see \App\Controller\AppController::getObjectNav)
295
     *
296
     * @param array $objects The objects to parse to set prev and next data
297
     * @return void
298
     */
299
    protected function setObjectNav($objects) : void
300
    {
301
        $moduleName = $this->Modules->getConfig('currentModuleName');
302
        $total = count(array_keys($objects));
303
        $objectNav = [];
304
        foreach ($objects as $i => $object) {
305
            $objectNav[$moduleName][$object['id']] = [
306
                'prev' => ($i > 0) ? Hash::get($objects, sprintf('%d.id', $i - 1)) : null,
307
                'next' => ($i + 1 < $total) ? Hash::get($objects, sprintf('%d.id', $i + 1)) : null,
308
                'index' => $i + 1,
309
                'total' => $total,
310
                'object_type' => Hash::get($objects, sprintf('%d.object_type', $i)),
311
            ];
312
        }
313
        $session = $this->request->getSession();
314
        $session->write('objectNav', $objectNav);
315
        $session->write('objectNavModule', $moduleName);
316
    }
317
318
    /**
319
     * Get objectNav for ID and current module name
320
     *
321
     * @param string $id The object ID
322
     * @return array
323
     */
324
    protected function getObjectNav($id) : array
325
    {
326
        // get objectNav from session
327
        $session = $this->request->getSession();
328
        $objectNav = (array)$session->read('objectNav');
329
        if (empty($objectNav)) {
330
            return [];
331
        }
332
333
        // get objectNav by session objectNavModule
334
        $objectNavModule = (string)$session->read('objectNavModule');
335
336
        return (array)Hash::get($objectNav, sprintf('%s.%s', $objectNavModule, $id), []);
337
    }
338
339
    /**
340
     * Set session data for key 'failedSaveData'.
341
     *
342
     * @param array $data The data to store into 'failedSaveData'.
343
     * @return void
344
     */
345
    protected function setDataFromFailedSave($data)
346
    {
347
        $session = $this->request->getSession();
348
        $session->write('failedSaveData', $data);
349
    }
350
351
    /**
352
     * Get data from session by key 'failedSaveData'.
353
     * If any, return it and delete session key 'failedSaveData'.
354
     *
355
     * @return array
356
     */
357
    protected function getDataFromFailedSave() : array
358
    {
359
        $session = $this->request->getSession();
360
        $data = (array)$session->read('failedSaveData');
361
        if (empty($data)) {
362
            return [];
363
        }
364
        $session->delete('failedSaveData');
365
        unset($data['id']);
366
367
        return [ 'attributes' => $data ];
368
    }
369
}
370