SearchKit - Autogenerate default display table for saved searches; calculate links...
[civicrm-core.git] / ext / search_kit / Civi / Api4 / Action / SearchDisplay / Download.php
1 <?php
2
3 namespace Civi\Api4\Action\SearchDisplay;
4
5 use League\Csv\Writer;
6 use PhpOffice\PhpSpreadsheet\IOFactory;
7 use PhpOffice\PhpSpreadsheet\Spreadsheet;
8
9 /**
10 * Download the results of a SearchDisplay as a spreadsheet.
11 *
12 * Note: unlike other APIs this action will directly output a file
13 * if 'format' is set to anything other than 'array'.
14 *
15 * @method $this setFormat(string $format)
16 * @method string getFormat()
17 * @package Civi\Api4\Action\SearchDisplay
18 */
19 class Download extends AbstractRunAction {
20
21 /**
22 * Requested file format.
23 *
24 * 'array' will return a normal api result, with table headers as the first row.
25 * 'csv', etc. will directly output a file to the browser.
26 *
27 * @var string
28 * @required
29 * @options array,csv,xlsx,ods,pdf
30 */
31 protected $format = 'array';
32
33 private $formats = [
34 'xlsx' => [
35 'writer' => 'Xlsx',
36 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
37 ],
38 'ods' => [
39 'writer' => 'Ods',
40 'mime' => 'application/vnd.oasis.opendocument.spreadsheet',
41 ],
42 'pdf' => [
43 'writer' => 'Dompdf',
44 'mime' => 'application/pdf',
45 ],
46 ];
47
48 /**
49 * @param \Civi\Api4\Generic\Result $result
50 * @throws \API_Exception
51 */
52 protected function processResult(\Civi\Api4\Generic\Result $result) {
53 $entityName = $this->savedSearch['api_entity'];
54 $apiParams =& $this->savedSearch['api_params'];
55 $settings = $this->display['settings'];
56
57 // Displays are only exportable if they have actions enabled
58 if (empty($settings['actions'])) {
59 \CRM_Utils_System::permissionDenied();
60 }
61
62 // Force limit if the display has no pager
63 if (!isset($settings['pager']) && !empty($settings['limit'])) {
64 $apiParams['limit'] = $settings['limit'];
65 }
66 $apiParams['orderBy'] = $this->getOrderByFromSort();
67 $this->augmentSelectClause($apiParams);
68
69 $this->applyFilters();
70
71 $apiResult = civicrm_api4($entityName, 'get', $apiParams);
72
73 $rows = $this->formatResult($apiResult);
74
75 $columns = [];
76 foreach ($this->display['settings']['columns'] as $index => $col) {
77 $col += ['type' => NULL, 'label' => '', 'rewrite' => FALSE];
78 if ($col['type'] === 'field' && !empty($col['key'])) {
79 $columns[$index] = $col;
80 }
81 }
82
83 // Unicode-safe filename for download
84 $fileName = \CRM_Utils_File::makeFilenameWithUnicode($this->display['label']) . '.' . $this->format;
85
86 switch ($this->format) {
87 case 'array':
88 $result[] = array_column($columns, 'label');
89 foreach ($rows as $data) {
90 $row = array_column(array_intersect_key($data['columns'], $columns), 'val');
91 $result[] = $row;
92 }
93 return;
94
95 case 'csv':
96 $this->outputCSV($rows, $columns, $fileName);
97 break;
98
99 default:
100 $this->sendHeaders($fileName);
101 $this->outputSpreadsheet($rows, $columns);
102 }
103
104 \CRM_Utils_System::civiExit();
105 }
106
107 /**
108 * Outputs headers and CSV directly to browser for download
109 * @param array $rows
110 * @param array $columns
111 * @param string $fileName
112 */
113 private function outputCSV(array $rows, array $columns, string $fileName) {
114 $csv = Writer::createFromFileObject(new \SplTempFileObject());
115 $csv->setOutputBOM(Writer::BOM_UTF8);
116
117 // Header row
118 $csv->insertOne(array_column($columns, 'label'));
119
120 foreach ($rows as $data) {
121 $row = array_column(array_intersect_key($data['columns'], $columns), 'val');
122 foreach ($row as &$val) {
123 if (is_array($val)) {
124 $val = implode(', ', $val);
125 }
126 }
127 $csv->insertOne($row);
128 }
129 // Echo headers and content directly to browser
130 $csv->output($fileName);
131 }
132
133 /**
134 * Create PhpSpreadsheet document and output directly to browser for download
135 * @param array $rows
136 * @param array $columns
137 */
138 private function outputSpreadsheet(array $rows, array $columns) {
139 $document = new Spreadsheet();
140 $document->getProperties()
141 ->setTitle($this->display['label']);
142 $sheet = $document->getActiveSheet();
143
144 // Header row
145 foreach (array_values($columns) as $index => $col) {
146 $sheet->setCellValueByColumnAndRow($index + 1, 1, $col['label']);
147 }
148
149 foreach ($rows as $rowNum => $data) {
150 $colNum = 1;
151 foreach ($columns as $index => $col) {
152 $sheet->setCellValueByColumnAndRow($colNum++, $rowNum + 2, $this->formatColumnValue($col, $data['columns'][$index]));
153 }
154 }
155
156 $writer = IOFactory::createWriter($document, $this->formats[$this->format]['writer']);
157
158 $writer->save('php://output');
159 }
160
161 /**
162 * Returns final formatted column value
163 *
164 * @param array $col
165 * @param array $value
166 * @return string
167 */
168 protected function formatColumnValue(array $col, array $value) {
169 $val = $value['val'] ?? '';
170 return is_array($val) ? implode(', ', $val) : $val;
171 }
172
173 /**
174 * Sets headers based on content type and file name
175 *
176 * @param string $fileName
177 */
178 protected function sendHeaders(string $fileName) {
179 header('Content-Type: ' . $this->formats[$this->format]['mime']);
180 header('Content-Transfer-Encoding: binary');
181 header('Content-Description: File Transfer');
182 header('Content-Disposition: ' . $this->getContentDisposition($fileName));
183 }
184
185 /**
186 * Copied from \League\Csv\AbstractCsv::sendHeaders()
187 * @param string $fileName
188 * @return string
189 */
190 protected function getContentDisposition(string $fileName) {
191 $flag = FILTER_FLAG_STRIP_LOW;
192 if (strlen($fileName) !== mb_strlen($fileName)) {
193 $flag |= FILTER_FLAG_STRIP_HIGH;
194 }
195
196 $filenameFallback = str_replace('%', '', filter_var($fileName, FILTER_SANITIZE_STRING, $flag));
197
198 $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
199 if ($fileName !== $filenameFallback) {
200 $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($fileName));
201 }
202 return $disposition;
203 }
204
205 }