Completed
Push — master ( 4f2776...05c520 )
by Damian
02:03
created

GridFieldQueuedExportButton::getExportPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 4
rs 10
cc 2
eloc 3
nc 2
nop 1
1
<?php
2
3
/**
4
 * A button you can add to a GridField to export that GridField as a CSV. Should work with any sized GridField,
5
 * as the export is done using a queuedjob in the background.
6
 */
7
class GridFieldQueuedExportButton implements GridField_HTMLProvider, GridField_ActionProvider, GridField_URLHandler {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
8
9
    /**
10
     * @var array Map of a property name on the exported objects, with values being the column title in the CSV file.
11
     * Note that titles are only used when {@link $csvHasHeader} is set to TRUE.
12
     */
13
    protected $exportColumns;
14
15
    /**
16
     * @var string
17
     */
18
    protected $csvSeparator = ",";
19
20
    /**
21
     * @var boolean
22
     */
23
    protected $csvHasHeader = true;
24
25
    /**
26
     * Fragment to write the button to
27
     */
28
    protected $targetFragment;
29
30
    /**
31
     * @param string $targetFragment The HTML fragment to write the button into
32
     * @param array $exportColumns The columns to include in the export
33
     */
34
    public function __construct($targetFragment = "after", $exportColumns = null) {
35
        $this->targetFragment = $targetFragment;
36
        $this->exportColumns = $exportColumns;
0 ignored issues
show
Documentation Bug introduced by
It seems like $exportColumns can be null. However, the property $exportColumns is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
37
    }
38
39
    /**
40
     * Place the export button in a <p> tag below the field
41
     */
42
    public function getHTMLFragments($gridField) {
43
        $button = new GridField_FormAction(
44
            $gridField,
45
            'export',
46
            _t('TableListField.CSVEXPORT', 'Export to CSV'),
47
            'export',
48
            null
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
49
        );
50
        $button->setAttribute('data-icon', 'download-csv');
51
        $button->addExtraClass('action_batch_export');
52
        $button->setForm($gridField->getForm());
53
54
        return array(
55
            $this->targetFragment => '<p class="grid-csv-button">' . $button->Field() . '</p>',
56
        );
57
    }
58
59
    /**
60
     * This class is an action button
61
     */
62
    public function getActions($gridField) {
63
        return array('export', 'findgridfield');
64
    }
65
66
    public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
67
        if ($actionName == 'export') {
68
            return $this->startExport($gridField);
69
        } else if ($actionName == 'findgridfield') {
70
            return new GridFieldQueuedExportButton_Response($gridField);
71
        }
72
    }
73
74
    function startExport($gridField) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
75
        $job = new GenerateCSVJob();
76
77
        // Set the parameters that allow re-discovering this gridfield during background execution
78
        $job->setGridField($gridField);
79
        $job->setSession(Controller::curr()->getSession()->get_all());
80
81
        // Set the parameters that control CSV exporting
82
        $job->setSeparator($this->csvSeparator);
83
        $job->setIncludeHeader($this->csvHasHeader);
84
        if ($this->exportColumns) $job->setColumns($this->exportColumns);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->exportColumns 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...
85
86
        // Queue the job
87
        singleton('QueuedJobService')->queueJob($job);
88
89
        // Redirect to the status update page
90
        return Controller::curr()->redirect($gridField->Link('/export/' . $job->getSignature()));
91
    }
92
93
    /**
94
     * This class is also a URL handler
95
     */
96
    public function getURLHandlers($gridField) {
97
        return array(
98
            'export/$ID'          => 'checkExport',
99
            'export_download/$ID' => 'downloadExport'
100
        );
101
    }
102
103
    protected function getExportPath($id) {
104
        if ($id instanceof QueuedJobDescriptor) $id = $id->Signature;
105
        return ASSETS_PATH."/.exports/$id/$id.csv";
106
    }
107
108
    /**
109
     * Handle the export, for both the action button and the URL
110
     */
111
    public function checkExport($gridField, $request = null) {
112
        $id = $request->param('ID');
113
        $job = QueuedJobDescriptor::get()->filter('Signature', $id)->first();
114
115
        $controller = $gridField->getForm()->getController();
116
117
        $breadcrumbs = $controller->Breadcrumbs(false);
118
        $breadcrumbs->push(new ArrayData(array(
119
            'Title' => _t('TableListField.CSVEXPORT', 'Export to CSV'),
120
            'Link'  => false
121
        )));
122
123
        $parents = $controller->Breadcrumbs(false)->items;
124
        $backlink = array_pop($parents)->Link;
125
126
        $data = new ArrayData(array(
127
            'ID'          => $id,
128
            'Link'        => Controller::join_links($gridField->Link(), 'export', $job->Signature),
129
            'Backlink'    => $backlink,
130
            'Breadcrumbs' => $breadcrumbs,
131
            'GridName'    => $gridField->getname()
132
        ));
133
134
        if ($job->JobStatus == QueuedJob::STATUS_COMPLETE) {
135
            if (file_exists($this->getExportPath($id))) {
136
                $data->DownloadLink = $gridField->Link('/export_download/' . $job->Signature);
137
            }
138
            else {
139
                $data->ErrorMessage = _t(
140
                    'GridFieldQueuedExportButton.ERROR_REMOVED',
141
                    'This export has already been downloaded. For security reasons each export can only be downloaded once.'
142
                );
143
            }
144
        } else if ($job->JobStatus == QueuedJob::STATUS_BROKEN) {
145
            $data->ErrorMessage = _t(
146
                'GridFieldQueuedExportButton.ERROR_GENERAL',
147
                'Sorry, but there was an error exporting the CSV'
148
            );
149
        } else if ($job->JobStatus == QueuedJob::STATUS_CANCELLED) {
150
            $data->ErrorMessage = _t(
151
                'GridFieldQueuedExportButton.CANCELLED',
152
                'This export job was cancelled'
153
            );
154
        } else {
155
            $data->Count = $job->StepsProcessed;
156
            $data->Total = $job->TotalSteps;
157
        }
158
159
        Requirements::javascript('gridfieldqueuedexport/client/GridFieldQueuedExportButton.js');
160
        Requirements::css('gridfieldqueuedexport/client/GridFieldQueuedExportButton.css');
161
162
        $return = $data->renderWith('GridFieldQueuedExportButton');
163
164
        if ($request->isAjax()) {
165
            return $return;
166
        } else {
167
            return $controller->customise(array('Content' => $return));
168
        }
169
    }
170
171
    public function downloadExport($gridField, $request = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $gridField is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
172
        $id = $request->param('ID');
173
174
        $now = Date("d-m-Y-H-i");
175
        $servedName = "export-$now.csv";
176
177
        $path = $this->getExportPath($id);
178
        $content = file_get_contents($path);
179
180
        unlink($path);
181
        rmdir(dirname($path));
182
183
        $response = SS_HTTPRequest::send_file($content, $servedName, 'text/csv');
184
        $response->addHeader('Set-Cookie', 'downloaded_'.$id.'=true; Path=/');
185
186
        return $response;
187
    }
188
189
    /**
190
     * @return array
191
     */
192
    public function getExportColumns() {
193
        return $this->exportColumns;
194
    }
195
196
    /**
197
     * @param array
198
     */
199
    public function setExportColumns($cols) {
200
        $this->exportColumns = $cols;
201
        return $this;
202
    }
203
204
    /**
205
     * @return string
206
     */
207
    public function getCsvSeparator() {
208
        return $this->csvSeparator;
209
    }
210
211
    /**
212
     * @param string
213
     */
214
    public function setCsvSeparator($separator) {
215
        $this->csvSeparator = $separator;
216
        return $this;
217
    }
218
219
    /**
220
     * @return boolean
221
     */
222
    public function getCsvHasHeader() {
223
        return $this->csvHasHeader;
224
    }
225
226
    /**
227
     * @param boolean
228
     */
229
    public function setCsvHasHeader($bool) {
230
        $this->csvHasHeader = $bool;
231
        return $this;
232
    }
233
}
234
235
/**
236
 * A special type of SS_HTTPResponse that GridFieldQueuedExportButton returns in response to the "findgridfield"
237
 * action, which includes a reference to the gridfield
238
 */
239
class GridFieldQueuedExportButton_Response extends SS_HTTPResponse {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
240
    private $gridField;
241
242
    public function __construct(GridField $gridField) {
243
        $this->gridField = $gridField;
244
        parent::__construct('', 500);
245
    }
246
247
    public function getGridField() {
248
        return $this->gridField;
249
    }
250
}
251