3 namespace Civi\Api4\Action\SearchDisplay
;
6 use PhpOffice\PhpSpreadsheet\IOFactory
;
7 use PhpOffice\PhpSpreadsheet\Spreadsheet
;
10 * Download the results of a SearchDisplay as a spreadsheet.
12 * Note: unlike other APIs this action will directly output a file
13 * if 'format' is set to anything other than 'array'.
15 * @method $this setFormat(string $format)
16 * @method string getFormat()
17 * @package Civi\Api4\Action\SearchDisplay
19 class Download
extends AbstractRunAction
{
22 * Requested file format.
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.
29 * @options array,csv,xlsx,ods,pdf
31 protected $format = 'array';
36 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
40 'mime' => 'application/vnd.oasis.opendocument.spreadsheet',
44 'mime' => 'application/pdf',
49 * @param \Civi\Api4\Generic\Result $result
50 * @throws \API_Exception
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'];
57 // Displays are only exportable if they have actions enabled
58 if (empty($settings['actions'])) {
59 \CRM_Utils_System
::permissionDenied();
62 // Force limit if the display has no pager
63 if (!isset($settings['pager']) && !empty($settings['limit'])) {
64 $apiParams['limit'] = $settings['limit'];
66 $apiParams['orderBy'] = $this->getOrderByFromSort();
67 $this->augmentSelectClause($apiParams);
69 $this->applyFilters();
71 $apiResult = civicrm_api4($entityName, 'get', $apiParams);
73 $rows = $this->formatResult($apiResult);
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;
83 // Unicode-safe filename for download
84 $fileName = \CRM_Utils_File
::makeFilenameWithUnicode($this->display
['label']) . '.' . $this->format
;
86 switch ($this->format
) {
88 $result[] = array_column($columns, 'label');
89 foreach ($rows as $data) {
90 $row = array_column(array_intersect_key($data['columns'], $columns), 'val');
96 $this->outputCSV($rows, $columns, $fileName);
100 $this->sendHeaders($fileName);
101 $this->outputSpreadsheet($rows, $columns);
104 \CRM_Utils_System
::civiExit();
108 * Outputs headers and CSV directly to browser for download
110 * @param array $columns
111 * @param string $fileName
113 private function outputCSV(array $rows, array $columns, string $fileName) {
114 $csv = Writer
::createFromFileObject(new \
SplTempFileObject());
115 $csv->setOutputBOM(Writer
::BOM_UTF8
);
118 $csv->insertOne(array_column($columns, 'label'));
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);
127 $csv->insertOne($row);
129 // Echo headers and content directly to browser
130 $csv->output($fileName);
134 * Create PhpSpreadsheet document and output directly to browser for download
136 * @param array $columns
138 private function outputSpreadsheet(array $rows, array $columns) {
139 $document = new Spreadsheet();
140 $document->getProperties()
141 ->setTitle($this->display
['label']);
142 $sheet = $document->getActiveSheet();
145 foreach (array_values($columns) as $index => $col) {
146 $sheet->setCellValueByColumnAndRow($index +
1, 1, $col['label']);
149 foreach ($rows as $rowNum => $data) {
151 foreach ($columns as $index => $col) {
152 $sheet->setCellValueByColumnAndRow($colNum++
, $rowNum +
2, $this->formatColumnValue($col, $data['columns'][$index]));
156 $writer = IOFactory
::createWriter($document, $this->formats
[$this->format
]['writer']);
158 $writer->save('php://output');
162 * Returns final formatted column value
165 * @param array $value
168 protected function formatColumnValue(array $col, array $value) {
169 $val = $value['val'] ??
'';
170 return is_array($val) ?
implode(', ', $val) : $val;
174 * Sets headers based on content type and file name
176 * @param string $fileName
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));
186 * Copied from \League\Csv\AbstractCsv::sendHeaders()
187 * @param string $fileName
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
;
196 $filenameFallback = str_replace('%', '', filter_var($fileName, FILTER_SANITIZE_STRING
, $flag));
198 $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
199 if ($fileName !== $filenameFallback) {
200 $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($fileName));