ExportController::initialize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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