Passed
Push — master ( 05deda...5b86a3 )
by Stefano
07:55
created

ExportController   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 326
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 35
eloc 112
dl 0
loc 326
rs 9.6
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A rows() 0 12 2
A apiPath() 0 3 1
A initialize() 0 7 1
A prepareQuery() 0 17 4
A getFileName() 0 3 1
A rowFields() 0 17 6
A getFieldNames() 0 9 1
A rowsAllRelated() 0 23 4
A limit() 0 5 1
A getValue() 0 7 2
A rowsAll() 0 23 5
A fillDataFromResponse() 0 9 3
A related() 0 26 2
A export() 0 29 2
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 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 Cake\Http\Response;
16
use Cake\Utility\Hash;
17
18
/**
19
 * Export controller: upload and load using filters
20
 *
21
 * @property \App\Controller\Component\ConfigComponent $Config
22
 * @property \App\Controller\Component\ExportComponent $Export
23
 */
24
class ExportController extends AppController
25
{
26
    /**
27
     * Default max number of exported items
28
     *
29
     * @var int
30
     */
31
    public const DEFAULT_EXPORT_LIMIT = 10000;
32
33
    /**
34
     * Default page size
35
     *
36
     * @var int
37
     */
38
    public const DEFAULT_PAGE_SIZE = 500;
39
40
    /**
41
     * {@inheritDoc}
42
     * {@codeCoverageIgnore}
43
     */
44
    public function initialize(): void
45
    {
46
        parent::initialize();
47
48
        $this->loadComponent('Config');
49
        $this->loadComponent('Export');
50
        $this->Security->setConfig('unlockedActions', ['related']);
51
    }
52
53
    /**
54
     * Export data to format specified by user
55
     *
56
     * @return \Cake\Http\Response|null
57
     */
58
    public function export(): ?Response
59
    {
60
        // check request (allowed methods and required parameters)
61
        $data = $this->checkRequest([
62
            'allowedMethods' => ['post'],
63
            'requiredParameters' => ['objectType'],
64
        ]);
65
66
        $format = (string)$this->getRequest()->getData('format');
67
        if (!$this->Export->checkFormat($format)) {
68
            $this->Flash->error(__('Format choosen is not available'));
69
70
            return $this->redirect($this->referer());
71
        }
72
73
        $ids = (string)$this->getRequest()->getData('ids');
74
75
        // load data for objects by object type and ids
76
        $rows = $this->rows($data['objectType'], $ids);
77
78
        // create spreadsheet and return as download
79
        $filename = $this->getFileName($data['objectType'], $format);
80
        $data = $this->Export->format($format, $rows, $filename);
81
82
        // output
83
        $response = $this->getResponse()->withStringBody(Hash::get($data, 'content'));
84
        $response = $response->withType(Hash::get($data, 'contentType'));
85
86
        return $response->withDownload($filename);
87
    }
88
89
    /**
90
     * Export related data to format specified by user
91
     *
92
     * @param string $id The object ID
93
     * @param string $relation The relation name
94
     * @param string $format The file format
95
     * @return \Cake\Http\Response|null
96
     */
97
    public function related(string $id, string $relation, string $format): ?Response
98
    {
99
        // check request (allowed methods and required parameters)
100
        $this->checkRequest([
101
            'allowedMethods' => ['get'],
102
        ]);
103
104
        if (!$this->Export->checkFormat($format)) {
105
            $this->Flash->error(__('Format choosen is not available'));
106
107
            return $this->redirect($this->referer());
108
        }
109
110
        // load related
111
        $objectType = $this->getRequest()->getParam('object_type');
112
        $rows = $this->rowsAllRelated($objectType, $id, $relation);
113
114
        // create spreadsheet and return as download
115
        $filename = sprintf('%s_%s_%s.%s', $objectType, $relation, date('Ymd-His'), $format);
116
        $data = $this->Export->format($format, $rows, $filename);
117
118
        // output
119
        $response = $this->getResponse()->withStringBody(Hash::get($data, 'content'));
120
        $response = $response->withType(Hash::get($data, 'contentType'));
121
122
        return $response->withDownload($filename);
123
    }
124
125
    /**
126
     * Obtain csv rows using api get per object type.
127
     * When using parameter ids, get only specified ids,
128
     * otherwise get all by object type.
129
     * First element of data is the attributes/fields array.
130
     *
131
     * @param string $objectType The object type
132
     * @param string $ids Object IDs comma separated string
133
     * @return array
134
     */
135
    protected function rows(string $objectType, string $ids = ''): array
136
    {
137
        if (empty($ids)) {
138
            return $this->rowsAll($objectType);
139
        }
140
141
        $response = $this->apiClient->get($this->apiPath(), ['filter' => ['id' => $ids]]);
142
        $fields = $this->getFieldNames($response);
143
        $data = [$fields];
144
        $this->fillDataFromResponse($data, $response, $fields);
145
146
        return $data;
147
    }
148
149
    /**
150
     * Get API path.
151
     *
152
     * @return string
153
     */
154
    protected function apiPath(): string
155
    {
156
        return sprintf('/%s', (string)$this->getRequest()->getData('objectType'));
157
    }
158
159
    /**
160
     * Get exported file name.
161
     *
162
     * @param string $type Object or resource type.
163
     * @param string $format The format.
164
     * @return string
165
     */
166
    protected function getFileName(string $type, string $format): string
167
    {
168
        return sprintf('%s_%s.%s', $type, date('Ymd-His'), $format);
169
    }
170
171
    /**
172
     * Get export limit.
173
     *
174
     * @return int
175
     */
176
    protected function limit(): int
177
    {
178
        $export = $this->Config->read('Export');
179
180
        return (int)Hash::get($export, 'limit', self::DEFAULT_EXPORT_LIMIT);
181
    }
182
183
    /**
184
     * Load all data for a given type using limit and query filters.
185
     *
186
     * @param string $objectType Object type
187
     * @return array
188
     */
189
    protected function rowsAll(string $objectType): array
190
    {
191
        $data = $fields = [];
192
        $limit = $this->limit();
193
        $pageCount = $page = 1;
194
        $total = 0;
195
        $pageSize = $limit > self::DEFAULT_PAGE_SIZE ? self::DEFAULT_PAGE_SIZE : $limit;
196
        $query = ['page_size' => $pageSize] + $this->prepareQuery();
197
        while ($total < $limit && $page <= $pageCount) {
198
            $response = (array)$this->apiClient->get($this->apiPath(), $query + compact('page'));
199
            $pageCount = (int)Hash::get($response, 'meta.pagination.page_count');
200
            $total += (int)Hash::get($response, 'meta.pagination.page_items');
201
202
            if ($page === 1) {
203
                $fields = $this->getFieldNames($response);
204
                $data = [$fields];
205
            }
206
207
            $this->fillDataFromResponse($data, $response, $fields);
208
            $page++;
209
        }
210
211
        return $data;
212
    }
213
214
    /**
215
     * Load all related data for a given type and relation using limit and query filters.
216
     *
217
     * @param string $objectType Object type
218
     * @param string $id The object ID
219
     * @param string $relationName The relation name
220
     * @return array
221
     */
222
    protected function rowsAllRelated(string $objectType, string $id, string $relationName): array
223
    {
224
        $data = $fields = [];
225
        $url = sprintf('/%s/%s/%s', $objectType, $id, $relationName);
226
        $limit = $this->limit();
227
        $pageCount = $page = 1;
228
        $total = 0;
229
        $query = ['page_size' => self::DEFAULT_PAGE_SIZE] + $this->prepareQuery();
230
        while ($total < $limit && $page <= $pageCount) {
231
            $response = (array)$this->apiClient->get($url, $query + compact('page'));
232
            $pageCount = (int)Hash::get($response, 'meta.pagination.page_count');
233
            $total += (int)Hash::get($response, 'meta.pagination.page_items');
234
235
            if ($page === 1) {
236
                $fields = $this->getFieldNames($response);
237
                $data = [$fields];
238
            }
239
240
            $this->fillDataFromResponse($data, $response, $fields);
241
            $page++;
242
        }
243
244
        return $data;
245
    }
246
247
    /**
248
     * Prepare additional API query from POST data
249
     *
250
     * @return array
251
     */
252
    protected function prepareQuery(): array
253
    {
254
        $res = [];
255
        $f = (array)$this->getRequest()->getData('filter');
256
        if (!empty($f)) {
257
            $filter = [];
258
            foreach ($f as $v) {
259
                $filter += (array)json_decode($v, true);
260
            }
261
            $res = compact('filter');
262
        }
263
        $q = (string)$this->getRequest()->getData('q');
264
        if (!empty($q)) {
265
            $res += compact('q');
266
        }
267
268
        return $res;
269
    }
270
271
    /**
272
     * Fill data array, using response.
273
     * Return the fields representing each data item.
274
     *
275
     * @param array $data The array of data
276
     * @param array $response The response to use as source for data
277
     * @param array $fields Field names array
278
     * @return void
279
     */
280
    protected function fillDataFromResponse(array &$data, array $response, array $fields): void
281
    {
282
        if (empty($response['data'])) {
283
            return;
284
        }
285
286
        // fill row data from response data
287
        foreach ($response['data'] as $val) {
288
            $data[] = $this->rowFields($val, $fields);
289
        }
290
    }
291
292
    /**
293
     * Get field names array using data first element attributes
294
     *
295
     * @param array $response The response from which extract fields
296
     * @return array
297
     */
298
    protected function getFieldNames($response): array
299
    {
300
        $fields = (array)Hash::get($response, 'data.0.attributes');
301
        $meta = (array)Hash::get($response, 'data.0.meta');
302
        unset($meta['extra']);
303
        $fields = array_merge(['id' => ''], $fields, $meta);
304
        $fields = array_merge($fields, (array)Hash::get($response, 'data.0.meta.extra'));
305
306
        return array_keys($fields);
307
    }
308
309
    /**
310
     * Get row data per fields
311
     *
312
     * @param array $data The data
313
     * @param array $fields The fields
314
     * @return array
315
     */
316
    protected function rowFields(array $data, array $fields): array
317
    {
318
        $row = [];
319
        foreach ($fields as $field) {
320
            $row[$field] = '';
321
            if (isset($data[$field])) {
322
                $row[$field] = $this->getValue($data[$field]);
323
            } elseif (isset($data['attributes'][$field])) {
324
                $row[$field] = $this->getValue($data['attributes'][$field]);
325
            } elseif (isset($data['meta'][$field])) {
326
                $row[$field] = $this->getValue($data['meta'][$field]);
327
            } elseif (isset($data['meta']['extra'][$field])) {
328
                $row[$field] = $this->getValue($data['meta']['extra'][$field]);
329
            }
330
        }
331
332
        return $row;
333
    }
334
335
    /**
336
     * Get value from $value.
337
     * If is an array, return json representation.
338
     * Return value otherwise
339
     *
340
     * @param mixed $value The value
341
     * @return mixed
342
     */
343
    protected function getValue($value)
344
    {
345
        if (is_array($value)) {
346
            return json_encode($value);
347
        }
348
349
        return $value;
350
    }
351
}
352