[BUGFIX] Use type=search for SvgTree filter
[Packages/TYPO3.CMS.git] / typo3 / sysext / recordlist / Classes / Controller / RecordDownloadController.php
1 <?php
2
3 declare(strict_types=1);
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 namespace TYPO3\CMS\Recordlist\Controller;
19
20 use Psr\Http\Message\ResponseFactoryInterface;
21 use Psr\Http\Message\ResponseInterface;
22 use Psr\Http\Message\ServerRequestInterface;
23 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
24 use TYPO3\CMS\Backend\Routing\UriBuilder;
25 use TYPO3\CMS\Backend\Utility\BackendUtility;
26 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27 use TYPO3\CMS\Core\Context\Context;
28 use TYPO3\CMS\Core\Type\Bitmask\Permission;
29 use TYPO3\CMS\Core\Utility\CsvUtility;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Fluid\View\StandaloneView;
32 use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
33 use TYPO3\CMS\Recordlist\RecordList\DownloadRecordList;
34
35 /**
36 * Controller for handling download of records, typically executed from the list module.
37 *
38 * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
39 */
40 class RecordDownloadController
41 {
42 private const DOWNLOAD_FORMATS = [
43 'csv' => [
44 'options' => [
45 'delimiter' => [
46 'comma' => ',',
47 'semicolon' => ';',
48 'pipe' => '|'
49 ],
50 'quote' => [
51 'doublequote' => '"',
52 'singlequote' => '\'',
53 'space' => ' '
54 ]
55 ],
56 'defaults' => [
57 'delimiter' => ',',
58 'quote' => '"'
59 ]
60 ],
61 'json' => [
62 'options' => [
63 'meta' => [
64 'full' => 'full',
65 'prefix' => 'prefix',
66 'none' => 'none'
67 ]
68 ],
69 'defaults' => [
70 'meta' => 'prefix'
71 ]
72 ]
73 ];
74
75 protected int $id = 0;
76 protected string $table = '';
77 protected string $format = '';
78 protected string $filename = '';
79 protected array $modTSconfig = [];
80
81 protected ResponseFactoryInterface $responseFactory;
82 protected UriBuilder $uriBuilder;
83
84 public function __construct(ResponseFactoryInterface $responseFactory, UriBuilder $uriBuilder)
85 {
86 $this->responseFactory = $responseFactory;
87 $this->uriBuilder = $uriBuilder;
88 }
89
90 /**
91 * Handle record download request by evaluating the provided arguments,
92 * checking access, initializing the record list, fetching records and
93 * finally calling the requested download format action (e.g. csv).
94 *
95 * @param ServerRequestInterface $request
96 * @return ResponseInterface
97 */
98 public function handleDownloadRequest(ServerRequestInterface $request): ResponseInterface
99 {
100 $parsedBody = $request->getParsedBody();
101
102 $this->table = (string)($parsedBody['table'] ?? '');
103 if ($this->table === '') {
104 throw new \RuntimeException('No table was given for downloading records', 1623941276);
105 }
106 $this->format = (string)($parsedBody['format'] ?? '');
107 if ($this->format === '' || !isset(self::DOWNLOAD_FORMATS[$this->format])) {
108 throw new \RuntimeException('No or an invalid download format given', 1624562166);
109 }
110
111 $this->filename = $this->generateFilename((string)($parsedBody['filename'] ?? ''));
112 $this->id = (int)($parsedBody['id'] ?? 0);
113
114 // Loading module configuration
115 $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
116
117 // Loading current page record and checking access
118 $backendUser = $this->getBackendUserAuthentication();
119 $perms_clause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
120 $pageinfo = BackendUtility::readPageAccess($this->id, $perms_clause);
121 $searchString = (string)($parsedBody['searchString'] ?? '');
122 $searchLevels = (int)($parsedBody['searchLevels'] ?? 0);
123 if (!is_array($pageinfo) && !($this->id === 0 && $searchString !== '' && $searchLevels !== 0)) {
124 throw new AccessDeniedException('Insufficient permissions for accessing this download', 1623941361);
125 }
126
127 // Initialize database record list
128 $recordList = GeneralUtility::makeInstance(DatabaseRecordList::class);
129 $recordList->modTSconfig = $this->modTSconfig;
130 $recordList->setFields[$this->table] = ($parsedBody['allColumns'] ?? false)
131 ? BackendUtility::getAllowedFieldsForTable($this->table)
132 : $backendUser->getModuleData('list/displayFields')[$this->table] ?? [];
133 $recordList->setLanguagesAllowedForUser($this->getSiteLanguages($request));
134 $recordList->start($this->id, $this->table, 0, $searchString, $searchLevels);
135
136 $columnsToRender = $recordList->getColumnsToRender($this->table, false);
137 $hideTranslations = ($this->modTSconfig['hideTranslations'] ?? '') === '*'
138 || GeneralUtility::inList($this->modTSconfig['hideTranslations'] ?? '', $this->table);
139
140 // Initialize the downloader
141 $downloader = GeneralUtility::makeInstance(
142 DownloadRecordList::class,
143 $recordList,
144 GeneralUtility::makeInstance(TranslationConfigurationProvider::class)
145 );
146
147 // Fetch and process the header row and the records
148 $headerRow = $downloader->getHeaderRow($columnsToRender);
149 $records = $downloader->getRecords(
150 $this->table,
151 $this->id,
152 $columnsToRender,
153 $this->getBackendUserAuthentication(),
154 $hideTranslations,
155 (bool)($parsedBody['rawValues'] ?? false)
156 );
157
158 $downloadAction = $this->format . 'DownloadAction';
159 return $this->{$downloadAction}($request, $headerRow, $records);
160 }
161
162 /**
163 * Generate settings form for the download request
164 *
165 * @param ServerRequestInterface $request
166 * @return ResponseInterface
167 */
168 public function downloadSettingsAction(ServerRequestInterface $request): ResponseInterface
169 {
170 $downloadArguments = $request->getQueryParams();
171
172 $this->table = (string)($downloadArguments['table'] ?? '');
173 if ($this->table === '') {
174 throw new \RuntimeException('No table was given for downloading records', 1624551586);
175 }
176
177 $this->id = (int)($downloadArguments['id'] ?? 0);
178 $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
179
180 $view = GeneralUtility::makeInstance(StandaloneView::class);
181 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
182 'EXT:recordlist/Resources/Private/Templates/RecordDownloadSettings.html'
183 ));
184
185 $view->assignMultiple([
186 'formUrl' => $this->uriBuilder->buildUriFromRoute('record_download'),
187 'table' => $this->table,
188 'downloadArguments' => $downloadArguments,
189 'formats' => array_keys(self::DOWNLOAD_FORMATS),
190 'formatOptions' => $this->getFormatOptionsWithResolvedDefaults(),
191 ]);
192
193 $response = $this->responseFactory->createResponse()
194 ->withHeader('Content-Type', 'text/html; charset=utf-8');
195
196 $response->getBody()->write($view->render());
197 return $response;
198 }
199
200 /**
201 * Generating an download in CSV format
202 *
203 * @param ServerRequestInterface $request
204 * @param array $headerRow
205 * @param array $records
206 * @return ResponseInterface
207 */
208 protected function csvDownloadAction(
209 ServerRequestInterface $request,
210 array $headerRow,
211 array $records
212 ): ResponseInterface {
213 // Fetch csv related format options
214 $csvDelimiter = $this->getFormatOption($request, 'delimiter');
215 $csvQuote = $this->getFormatOption($request, 'quote');
216
217 // Create result
218 $result[] = CsvUtility::csvValues($headerRow, $csvDelimiter, $csvQuote);
219 foreach ($records as $record) {
220 $result[] = CsvUtility::csvValues($record, $csvDelimiter, $csvQuote);
221 }
222
223 return $this->generateDownloadResponse(implode(CRLF, $result));
224 }
225
226 /**
227 * Generating an download in JSON format
228 *
229 * @param ServerRequestInterface $request
230 * @param array $headerRow
231 * @param array $records
232 * @return ResponseInterface
233 */
234 protected function jsonDownloadAction(
235 ServerRequestInterface $request,
236 array $headerRow,
237 array $records
238 ): ResponseInterface {
239 // Fetch and evaluate json related format option
240 switch ($this->getFormatOption($request, 'meta')) {
241 case 'prefix':
242 $result = [$this->table . ':' . $this->id => $records];
243 break;
244 case 'full':
245 $user = $this->getBackendUserAuthentication();
246 $parsedBody = $request->getParsedBody();
247 $result = [
248 'meta' => [
249 'table' => $this->table,
250 'page' => $this->id,
251 'timestamp' => GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp'),
252 'user' => $user->user[$user->username_column] ?? '',
253 'site' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '',
254 'options' => [
255 'columns' => array_values($headerRow),
256 'values' => ($parsedBody['rawvalues'] ?? false) ? 'raw' : 'processed'
257 ]
258 ],
259 'records' => $records
260 ];
261 $searchString = (string)($parsedBody['searchString'] ?? '');
262 $searchLevels = (int)($parsedBody['searchLevels'] ?? 0);
263 if ($searchString !== '' || $searchLevels !== 0) {
264 $result['meta']['search'] = [
265 'searchTerm' => $searchString,
266 'searchLevels' => $searchLevels
267 ];
268 }
269 break;
270 case 'none':
271 default:
272 $result = $records;
273 break;
274 }
275
276 return $this->generateDownloadResponse(json_encode($result) ?: '');
277 }
278
279 /**
280 * Get site languages, available for the current backend user
281 *
282 * @param ServerRequestInterface $request
283 * @return array
284 */
285 protected function getSiteLanguages(ServerRequestInterface $request): array
286 {
287 $site = $request->getAttribute('site');
288 return $site->getAvailableLanguages($this->getBackendUserAuthentication(), false, $this->id);
289 }
290
291 /**
292 * Return an evaluated and processed custom filename or a
293 * default, if non or an invalid custom filename was provided.
294 *
295 * @param string $filename
296 * @return string
297 */
298 protected function generateFilename(string $filename): string
299 {
300 $defaultFilename = $this->table . '_' . date('dmy-Hi') . '.' . $this->format;
301
302 // Return default filename if given filename is empty or not valid
303 if ($filename === '' || !preg_match('/^[0-9a-z._\-]+$/i', $filename)) {
304 return $defaultFilename;
305 }
306
307 $extension = pathinfo($filename, PATHINFO_EXTENSION);
308 if ($extension === '') {
309 // Add original extension in case alternative filename did not contain any
310 $filename = rtrim($filename, '.') . '.' . $this->format;
311 }
312
313 // Check if given or resolved extension matches the original one
314 return pathinfo($filename, PATHINFO_EXTENSION) === $this->format ? $filename : $defaultFilename;
315 }
316
317 /**
318 * Return the format options with resolved default values from TSconfig
319 *
320 * @return array
321 */
322 protected function getFormatOptionsWithResolvedDefaults(): array
323 {
324 $formatOptions = self::DOWNLOAD_FORMATS;
325
326 if ($this->modTSconfig === []) {
327 return $formatOptions;
328 }
329
330 if ($this->modTSconfig['csvDelimiter'] ?? false) {
331 $default = (string)$this->modTSconfig['csvDelimiter'];
332 if (!in_array($default, $formatOptions['csv']['options']['delimiter'], true)) {
333 // In case the user defined option is not yet available as format options, add it
334 $formatOptions['csv']['options']['delimiter']['custom'] = $default;
335 }
336 $formatOptions['csv']['defaults']['delimiter'] = $default;
337 }
338
339 if ($this->modTSconfig['csvQuote'] ?? false) {
340 $default = (string)$this->modTSconfig['csvQuote'];
341 if (!in_array($default, $formatOptions['csv']['options']['quote'], true)) {
342 // In case the user defined option is not yet available as format options, add it
343 $formatOptions['csv']['options']['quote']['custom'] = $default;
344 }
345 $formatOptions['csv']['defaults']['quote'] = $default;
346 }
347
348 return $formatOptions;
349 }
350
351 protected function getFormatOptions(ServerRequestInterface $request): array
352 {
353 return $request->getParsedBody()[$this->format] ?? [];
354 }
355
356 protected function getFormatOption(ServerRequestInterface $request, string $option, $default = null)
357 {
358 return $this->getFormatOptions($request)[$option]
359 ?? $this->getFormatOptionsWithResolvedDefaults()[$this->format]['defaults'][$option]
360 ?? $default;
361 }
362
363 protected function generateDownloadResponse(string $result): ResponseInterface
364 {
365 $response = $this->responseFactory->createResponse()
366 ->withHeader('Content-Type', 'application/octet-stream')
367 ->withHeader('Content-Disposition', 'attachment; filename=' . $this->filename);
368 $response->getBody()->write($result);
369
370 return $response;
371 }
372
373 protected function getBackendUserAuthentication(): BackendUserAuthentication
374 {
375 return $GLOBALS['BE_USER'];
376 }
377 }