Passed
Push — master ( 597870...6d6cf2 )
by Thomas
12:42
created

ExcelGridFieldExportButton::getURLHandlers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
namespace LeKoala\ExcelImportExport;
4
5
use Generator;
6
use InvalidArgumentException;
7
use SilverStripe\Assets\FileNameFilter;
8
use SilverStripe\Forms\GridField\GridField;
9
use SilverStripe\Forms\GridField\GridFieldPaginator;
10
use SilverStripe\Forms\GridField\GridField_FormAction;
11
use SilverStripe\Forms\GridField\GridField_URLHandler;
12
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
13
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
14
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
15
use SilverStripe\Forms\GridField\GridField_ActionProvider;
16
use LeKoala\SpreadCompat\SpreadCompat;
0 ignored issues
show
Bug introduced by
The type LeKoala\SpreadCompat\SpreadCompat was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use SilverStripe\ORM\DataList;
18
19
/**
20
 * Adds an "Export list" button to the bottom of a {@link GridField}.
21
 */
22
class ExcelGridFieldExportButton implements
23
    GridField_HTMLProvider,
24
    GridField_ActionProvider,
25
    GridField_URLHandler
26
{
27
    /**
28
     * Map of a property name on the exported objects, with values being the column title in the file.
29
     * Note that titles are only used when {@link $hasHeader} is set to TRUE.
30
     */
31
    protected ?array $exportColumns;
32
33
    /**
34
     * Fragment to write the button to
35
     */
36
    protected string $targetFragment;
37
38
    protected bool $hasHeader = true;
39
40
    protected string $exportType = 'xlsx';
41
42
    protected ?string $exportName = null;
43
44
    protected ?string $buttonTitle = null;
45
46
    protected bool $checkCanView = true;
47
48
    protected bool $isLimited = true;
49
50
    protected array $listFilters = [];
51
52
    /**
53
     *
54
     * @var callable
55
     */
56
    protected $afterExportCallback;
57
58
    protected bool $ignoreFilters = false;
59
60
    protected bool $sanitizeXls = true;
61
62
    /**
63
     * @param string $targetFragment The HTML fragment to write the button into
64
     * @param array $exportColumns The columns to include in the export
65
     */
66
    public function __construct($targetFragment = "after", $exportColumns = null)
67
    {
68
        $this->targetFragment = $targetFragment;
69
        $this->exportColumns = $exportColumns;
70
    }
71
72
    /**
73
     * @param GridField $gridField
74
     * @return string
75
     */
76
    public function getActionName($gridField)
77
    {
78
        $name = strtolower($gridField->getName());
79
        return 'excelexport_' . $name;
80
    }
81
82
    /**
83
     * Place the export button in a <p> tag below the field
84
     */
85
    public function getHTMLFragments($gridField)
86
    {
87
        $defaultTitle = _t(
88
            'ExcelImportExport.FORMATEXPORT',
89
            'Export to {format}',
90
            ['format' => $this->exportType]
91
        );
92
        $title = $this->buttonTitle ? $this->buttonTitle : $defaultTitle;
93
94
        $name = $this->getActionName($gridField);
95
96
        $button = new GridField_FormAction(
97
            $gridField,
98
            $name,
99
            $title,
100
            $name,
101
            null
102
        );
103
        $button->addExtraClass('btn btn-secondary no-ajax font-icon-down-circled action_export');
104
        $button->setForm($gridField->getForm());
105
106
        return array(
107
            $this->targetFragment => $button->Field()
108
        );
109
    }
110
111
    /**
112
     * export is an action button
113
     */
114
    public function getActions($gridField)
115
    {
116
        return array($this->getActionName($gridField));
117
    }
118
119
    public function handleAction(
120
        GridField $gridField,
121
        $actionName,
122
        $arguments,
123
        $data
124
    ) {
125
        if (in_array($actionName, $this->getActions($gridField))) {
126
            return $this->handleExport($gridField);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleExport($gridField) targeting LeKoala\ExcelImportExpor...tButton::handleExport() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
127
        }
128
    }
129
130
    /**
131
     * it is also a URL
132
     */
133
    public function getURLHandlers($gridField)
134
    {
135
        return array($this->getActionName($gridField) => 'handleExport');
136
    }
137
138
    /**
139
     * Handle the export, for both the action button and the URL
140
     */
141
    public function handleExport($gridField, $request = null)
142
    {
143
        $now = date("Ymd_Hi");
144
145
        $this->updateExportName($gridField);
146
147
        $data = $this->generateExportFileData($gridField);
148
149
        $ext = $this->exportType;
150
        $name = $this->exportName;
151
        $fileName = "$name-$now.$ext";
152
153
        if ($this->afterExportCallback) {
154
            $func = $this->afterExportCallback;
155
            $func();
156
        }
157
158
        $opts = [
159
            'extension' => $ext,
160
        ];
161
162
        if ($ext != 'csv') {
163
            $end = ExcelImportExport::getLetter(count($this->getRealExportColumns($gridField)));
164
            $opts['creator'] = "SilverStripe";
165
            $opts['autofilter'] = "A1:{$end}1";
166
        }
167
168
        SpreadCompat::$preferredCsvAdapter = SpreadCompat::NATIVE;
169
        SpreadCompat::output($data, $fileName, ...$opts);
170
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
171
    }
172
173
174
    /**
175
     * Make sure export name is a valid file name
176
     * @param GridField|\LeKoala\Tabulator\TabulatorGrid $gridField
0 ignored issues
show
Bug introduced by
The type LeKoala\Tabulator\TabulatorGrid was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
177
     */
178
    protected function updateExportName($gridField)
179
    {
180
        $filter = new FileNameFilter;
181
        if ($this->exportName) {
182
            $this->exportName = $filter->filter($this->exportName);
183
        } else {
184
            $class = $gridField->getModelClass();
185
            $singl = singleton($class);
186
            $plural = $class ? $singl->i18n_plural_name() : '';
187
188
            $this->exportName = $filter->filter('export-' . $plural);
189
        }
190
    }
191
192
    /**
193
     * @param GridField|\LeKoala\Tabulator\TabulatorGrid $gridField
194
     * @return DataList|ArrayList
0 ignored issues
show
Bug introduced by
The type LeKoala\ExcelImportExport\ArrayList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
195
     */
196
    protected function retrieveList($gridField)
197
    {
198
        // Remove GridFieldPaginator as we're going to export the entire list.
199
        $gridField->getConfig()->removeComponentsByType(GridFieldPaginator::class);
200
201
        /** @var DataList|ArrayList $items */
202
        $items = $gridField->getManipulatedList();
203
204
        // Keep filters
205
        if (!$this->ignoreFilters) {
206
            foreach ($gridField->getConfig()->getComponents() as $component) {
207
                if ($component instanceof GridFieldFilterHeader || $component instanceof GridFieldSortableHeader) {
208
                    $items = $component->getManipulatedData($gridField, $items);
209
                }
210
            }
211
        }
212
213
        $list = $items;
214
        $limit = ExcelImportExport::$limit_exports;
215
        if ($list instanceof DataList) {
216
            if ($this->isLimited && $limit > 0) {
217
                $list = $list->limit($limit);
218
            }
219
            if (!empty($this->listFilters)) {
220
                $list = $list->filter($this->listFilters);
221
            }
222
        }
223
224
        return $list;
225
    }
226
227
    /**
228
     * @param GridField|\LeKoala\Tabulator\TabulatorGrid $gridField
229
     */
230
    protected function getRealExportColumns($gridField)
231
    {
232
        $class = $gridField->getModelClass();
233
        return ($this->exportColumns) ? $this->exportColumns : ExcelImportExport::exportFieldsForClass($class);
234
    }
235
236
    /**
237
     * Generate export fields for Excel.
238
     *
239
     * @param GridField|\LeKoala\Tabulator\TabulatorGrid $gridField
240
     */
241
    public function generateExportFileData($gridField): Generator
242
    {
243
        $columns = $this->getRealExportColumns($gridField);
244
245
        if ($this->hasHeader) {
246
            $headers = [];
247
248
            // determine the headers. If a field is callable (e.g. anonymous function) then use the
249
            // source name as the header instead
250
            foreach ($columns as $columnSource => $columnHeader) {
251
                if (is_array($columnHeader) && array_key_exists('title', $columnHeader ?? [])) {
252
                    $headers[] = $columnHeader['title'];
253
                } else {
254
                    $headers[] = (!is_string($columnHeader) && is_callable($columnHeader)) ? $columnSource : $columnHeader;
255
                }
256
            }
257
258
            yield $headers;
259
        }
260
261
        $list = $this->retrieveList($gridField);
262
263
        if (!$list) {
264
            return;
265
        }
266
267
        $sanitize_xls_chars = ExcelImportExport::config()->sanitize_xls_chars ?? "=";
268
        $sanitize_xls_chars_len = strlen($sanitize_xls_chars);
269
270
        // Auto format using DBField methods based on column name
271
        $export_format = ExcelImportExport::config()->export_format;
272
273
        $sanitize = $this->sanitizeXls && $sanitize_xls_chars && $this->exportType == "csv";
274
275
        foreach ($list as $item) {
276
            // This can be really slow for large exports depending on how canView is implemented
277
            if ($this->checkCanView) {
278
                $canView = true;
279
                if ($item->hasMethod('canView') && !$item->canView()) {
280
                    $canView = false;
281
                }
282
                if (!$canView) {
283
                    continue;
284
                }
285
            }
286
287
            $dataRow = [];
288
289
            // Loop and transforms records as needed
290
            foreach ($columns as $columnSource => $columnHeader) {
291
                if (!is_string($columnHeader) && is_callable($columnHeader)) {
292
                    if ($item->hasMethod($columnSource)) {
293
                        $relObj = $item->{$columnSource}();
294
                    } else {
295
                        $relObj = $item->relObject($columnSource);
296
                    }
297
298
                    $value = $columnHeader($relObj);
299
                } else {
300
                    if (is_string($columnSource)) {
301
                        // It can be a method
302
                        if (strpos($columnSource, '(') !== false) {
303
                            $matches = [];
304
                            preg_match('/([a-zA-Z]*)\((.*)\)/', $columnSource, $matches);
305
                            $func = $matches[1];
306
                            $params = explode(",", $matches[2]);
307
                            // Support only one param for now
308
                            $value = $item->$func($params[0]);
309
                        } else {
310
                            if (array_key_exists($columnSource, $export_format)) {
311
                                $format = $export_format[$columnSource];
312
                                $value = $item->dbObject($columnSource)->$format();
313
                            } else {
314
                                $value = $gridField->getDataFieldValue($item, $columnSource);
315
                            }
316
                        }
317
                    } else {
318
                        // We can also use a simple dot notation
319
                        $parts = explode(".", $columnHeader);
320
                        if (count($parts) == 1) {
321
                            $value = $item->$columnHeader;
322
                        } else {
323
                            $value = $item->relObject($parts[0]);
324
                            if ($value) {
325
                                $relObjField = $parts[1];
326
                                $value = $value->$relObjField;
327
                            }
328
                        }
329
                    }
330
                }
331
332
                // @link https://owasp.org/www-community/attacks/CSV_Injection
333
                // [SS-2017-007] Sanitise XLS executable column values with a leading tab
334
                if ($sanitize) {
335
                    // If we have only one char we can make it simpler
336
                    if ($sanitize_xls_chars_len === 1) {
337
                        if ($value && $value[0] === $sanitize_xls_chars) {
338
                            $value = "\t" . $value;
339
                        }
340
                    } else {
341
                        if (preg_match('/^[' . $sanitize_xls_chars . '].*/', $value ?? '')) {
342
                            $value = "\t" . $value;
343
                        }
344
                    }
345
                }
346
347
                $dataRow[] = $value;
348
            }
349
350
            if ($item->hasMethod('destroy')) {
351
                $item->destroy();
352
            }
353
354
            yield $dataRow;
355
        }
356
    }
357
358
    /**
359
     * @return array
360
     */
361
    public function getExportColumns()
362
    {
363
        return $this->exportColumns;
364
    }
365
366
    /**
367
     * @param array
368
     */
369
    public function setExportColumns($cols)
370
    {
371
        $this->exportColumns = $cols;
372
        return $this;
373
    }
374
375
    /**
376
     * @return boolean
377
     */
378
    public function getHasHeader()
379
    {
380
        return $this->hasHeader;
381
    }
382
383
    /**
384
     * @param boolean
385
     */
386
    public function setHasHeader($bool)
387
    {
388
        $this->hasHeader = $bool;
389
        return $this;
390
    }
391
392
    /**
393
     * @return string
394
     */
395
    public function getExportType()
396
    {
397
        return $this->exportType;
398
    }
399
400
    /**
401
     * @param string xlsx (default), xls or csv
0 ignored issues
show
Bug introduced by
The type LeKoala\ExcelImportExport\xlsx was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
402
     */
403
    public function setExportType($exportType)
404
    {
405
        if (!in_array($exportType, ['xls', 'xlsx', 'csv'])) {
406
            throw new InvalidArgumentException("Export type must be one of : xls, xlsx, csv");
407
        }
408
        $this->exportType = $exportType;
409
        return $this;
410
    }
411
412
    /**
413
     * @return string
414
     */
415
    public function getExportName()
416
    {
417
        return $this->exportName;
418
    }
419
420
    /**
421
     * @param string $exportName
422
     * @return ExcelGridFieldExportButton
423
     */
424
    public function setExportName($exportName)
425
    {
426
        $this->exportName = $exportName;
427
        return $this;
428
    }
429
430
    /**
431
     * @return string
432
     */
433
    public function getButtonTitle()
434
    {
435
        return $this->buttonTitle;
436
    }
437
438
    /**
439
     * @param string $buttonTitle
440
     * @return ExcelGridFieldExportButton
441
     */
442
    public function setButtonTitle($buttonTitle)
443
    {
444
        $this->buttonTitle = $buttonTitle;
445
        return $this;
446
    }
447
448
    /**
449
     *
450
     * @return bool
451
     */
452
    public function getCheckCanView()
453
    {
454
        return $this->checkCanView;
455
    }
456
457
    /**
458
     *
459
     * @param bool $checkCanView
460
     * @return ExcelGridFieldExportButton
461
     */
462
    public function setCheckCanView($checkCanView)
463
    {
464
        $this->checkCanView = $checkCanView;
465
        return $this;
466
    }
467
468
    /**
469
     *
470
     * @return array
471
     */
472
    public function getListFilters()
473
    {
474
        return $this->listFilters;
475
    }
476
477
    /**
478
     *
479
     * @param array $listFilters
480
     * @return ExcelGridFieldExportButton
481
     */
482
    public function setListFilters($listFilters)
483
    {
484
        $this->listFilters = $listFilters;
485
        return $this;
486
    }
487
488
    /**
489
     *
490
     * @return callable
491
     */
492
    public function getAfterExportCallback()
493
    {
494
        return $this->afterExportCallback;
495
    }
496
497
    /**
498
     *
499
     * @param callable $afterExportCallback
500
     * @return ExcelGridFieldExportButton
501
     */
502
    public function setAfterExportCallback(callable $afterExportCallback)
503
    {
504
        $this->afterExportCallback = $afterExportCallback;
505
        return $this;
506
    }
507
508
    /**
509
     * Get the value of isLimited
510
     */
511
    public function getIsLimited(): bool
512
    {
513
        return $this->isLimited;
514
    }
515
516
    /**
517
     * Set the value of isLimited
518
     *
519
     * @param bool $isLimited
520
     */
521
    public function setIsLimited(bool $isLimited)
522
    {
523
        $this->isLimited = $isLimited;
524
        return $this;
525
    }
526
527
    /**
528
     * Get the value of ignoreFilters
529
     */
530
    public function getIgnoreFilters(): bool
531
    {
532
        return $this->ignoreFilters;
533
    }
534
535
    /**
536
     * Set the value of ignoreFilters
537
     *
538
     * @param bool $ignoreFilters
539
     */
540
    public function setIgnoreFilters(bool $ignoreFilters): self
541
    {
542
        $this->ignoreFilters = $ignoreFilters;
543
        return $this;
544
    }
545
546
    /**
547
     * Get the value of sanitizeXls
548
     */
549
    public function getSanitizeXls(): bool
550
    {
551
        return $this->sanitizeXls;
552
    }
553
554
    /**
555
     * Set the value of sanitizeXls
556
     *
557
     * @param bool $sanitizeXls
558
     */
559
    public function setSanitizeXls(bool $sanitizeXls): self
560
    {
561
        $this->sanitizeXls = $sanitizeXls;
562
        return $this;
563
    }
564
}
565