‪TYPO3CMS  ‪main
RecordListDownloadController.php
Go to the documentation of this file.
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 
19 
20 use Psr\Http\Message\ResponseFactoryInterface;
21 use Psr\Http\Message\ResponseInterface;
22 use Psr\Http\Message\ServerRequestInterface;
28 use TYPO3\CMS\Backend\Utility\BackendUtility;
35 
41 #[AsController]
43 {
44  private const DOWNLOAD_FORMATS = [
45  'csv' => [
46  'options' => [
47  'delimiter' => [
48  'comma' => ',',
49  'semicolon' => ';',
50  'pipe' => '|',
51  ],
52  'quote' => [
53  'doublequote' => '"',
54  'singlequote' => '\'',
55  'space' => ' ',
56  ],
57  ],
58  'defaults' => [
59  'delimiter' => ',',
60  'quote' => '"',
61  ],
62  ],
63  'json' => [
64  'options' => [
65  'meta' => [
66  'full' => 'full',
67  'prefix' => 'prefix',
68  'none' => 'none',
69  ],
70  ],
71  'defaults' => [
72  'meta' => 'prefix',
73  ],
74  ],
75  ];
76 
77  protected int $id = 0;
78  protected string $table = '';
79  protected string $format = '';
80  protected string $filename = '';
81  protected array $modTSconfig = [];
82 
83  public function ‪__construct(
84  protected readonly ResponseFactoryInterface $responseFactory,
85  protected readonly ‪BackendViewFactory $backendViewFactory,
86  ) {}
87 
93  public function handleDownloadRequest(ServerRequestInterface $request): ResponseInterface
94  {
95  $parsedBody = $request->getParsedBody();
96 
97  $this->table = (string)($parsedBody['table'] ?? '');
98  if ($this->table === '') {
99  throw new \RuntimeException('No table was given for downloading records', 1623941276);
100  }
101  $this->format = (string)($parsedBody['format'] ?? '');
102  if ($this->format === '' || !isset(self::DOWNLOAD_FORMATS[$this->format])) {
103  throw new \RuntimeException('No or an invalid download format given', 1624562166);
104  }
105 
106  $this->filename = $this->generateFilename((string)($parsedBody['filename'] ?? ''));
107  $this->id = (int)($parsedBody['id'] ?? 0);
108 
109  // Loading module configuration
110  $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
111 
112  // Loading current page record and checking access
113  $backendUser = $this->getBackendUserAuthentication();
114  $perms_clause = $backendUser->getPagePermsClause(‪Permission::PAGE_SHOW);
115  $pageinfo = BackendUtility::readPageAccess($this->id, $perms_clause);
116  $searchString = (string)($parsedBody['searchString'] ?? '');
117  $searchLevels = (int)($parsedBody['searchLevels'] ?? 0);
118  if (!is_array($pageinfo) && !($this->id === 0 && $searchString !== '' && $searchLevels !== 0)) {
119  throw new ‪AccessDeniedException('Insufficient permissions for accessing this download', 1623941361);
120  }
121 
122  // Initialize database record list
123  $recordList = GeneralUtility::makeInstance(DatabaseRecordList::class);
124  $recordList->setRequest($request);
125  $recordList->modTSconfig = $this->modTSconfig;
126  $recordList->setLanguagesAllowedForUser($this->getSiteLanguages($request));
127  $recordList->start($this->id, $this->table, 0, $searchString, $searchLevels);
128  if (($parsedBody['allColumns'] ?? false)) {
129  // Overwrite setFields in case all allowed columns should be included.
130  $recordList->setFields[$this->table] = BackendUtility::getAllowedFieldsForTable($this->table);
131  }
132  $columnsToRender = $recordList->getColumnsToRender($this->table, false);
133  $hideTranslations = ($this->modTSconfig['hideTranslations'] ?? '') === '*'
134  || ‪GeneralUtility::inList($this->modTSconfig['hideTranslations'] ?? '', $this->table);
135 
136  // Initialize the downloader
137  $downloader = GeneralUtility::makeInstance(
138  DownloadRecordList::class,
139  $recordList,
140  GeneralUtility::makeInstance(TranslationConfigurationProvider::class)
141  );
142 
143  // Fetch and process the header row and the records
144  $headerRow = $downloader->getHeaderRow($columnsToRender);
145  $records = $downloader->getRecords(
146  $this->table,
147  $columnsToRender,
148  $this->getBackendUserAuthentication(),
149  $hideTranslations,
150  (bool)($parsedBody['rawValues'] ?? false)
151  );
152 
153  $downloadAction = $this->format . 'DownloadAction';
154  return $this->{$downloadAction}($request, $headerRow, $records);
155  }
156 
160  public function downloadSettingsAction(ServerRequestInterface $request): ResponseInterface
161  {
162  $downloadArguments = $request->getQueryParams();
163 
164  $this->table = (string)($downloadArguments['table'] ?? '');
165  if ($this->table === '') {
166  throw new \RuntimeException('No table was given for downloading records', 1624551586);
167  }
168 
169  $this->id = (int)($downloadArguments['id'] ?? 0);
170  $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
171 
172  $view = $this->backendViewFactory->create($request);
173  $view->assignMultiple([
174  'table' => $this->table,
175  'downloadArguments' => $downloadArguments,
176  'formats' => array_keys(self::DOWNLOAD_FORMATS),
177  'formatOptions' => $this->getFormatOptionsWithResolvedDefaults(),
178  ]);
179 
180  $response = $this->responseFactory->createResponse()
181  ->withHeader('Content-Type', 'text/html; charset=utf-8');
182 
183  $response->getBody()->write($view->render('RecordDownloadSettings'));
184  return $response;
185  }
186 
190  protected function csvDownloadAction(
191  ServerRequestInterface $request,
192  array $headerRow,
193  array $records
194  ): ResponseInterface {
195  // Fetch csv related format options
196  $csvDelimiter = (string)$this->getFormatOption($request, 'delimiter');
197  $csvQuote = (string)$this->getFormatOption($request, 'quote');
198 
199  // Create result
200  $result[] = ‪CsvUtility::csvValues($headerRow, $csvDelimiter, $csvQuote);
201  foreach ($records as ‪$record) {
202  $result[] = ‪CsvUtility::csvValues(‪$record, $csvDelimiter, $csvQuote);
203  }
204 
205  return $this->generateDownloadResponse(implode(CRLF, $result));
206  }
207 
211  protected function jsonDownloadAction(
212  ServerRequestInterface $request,
213  array $headerRow,
214  array $records
215  ): ResponseInterface {
216  // Fetch and evaluate json related format option
217  switch ($this->getFormatOption($request, 'meta')) {
218  case 'prefix':
219  $result = [$this->table . ':' . $this->id => $records];
220  break;
221  case 'full':
222  $user = $this->getBackendUserAuthentication();
223  $parsedBody = $request->getParsedBody();
224  $result = [
225  'meta' => [
226  'table' => $this->table,
227  'page' => $this->id,
228  'timestamp' => GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp'),
229  'user' => $user->getUserName() ?? '',
230  'site' => ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '',
231  'options' => [
232  'columns' => array_values($headerRow),
233  'values' => ($parsedBody['rawvalues'] ?? false) ? 'raw' : 'processed',
234  ],
235  ],
236  'records' => $records,
237  ];
238  $searchString = (string)($parsedBody['searchString'] ?? '');
239  $searchLevels = (int)($parsedBody['searchLevels'] ?? 0);
240  if ($searchString !== '' || $searchLevels !== 0) {
241  $result['meta']['search'] = [
242  'searchTerm' => $searchString,
243  'searchLevels' => $searchLevels,
244  ];
245  }
246  break;
247  case 'none':
248  default:
249  $result = $records;
250  break;
251  }
252 
253  return $this->generateDownloadResponse(json_encode($result) ?: '');
254  }
255 
259  protected function getSiteLanguages(ServerRequestInterface $request): array
260  {
261  $site = $request->getAttribute('site');
262  return $site->getAvailableLanguages($this->getBackendUserAuthentication(), false, $this->id);
263  }
264 
269  protected function generateFilename(string $filename): string
270  {
271  $defaultFilename = $this->table . '_' . date('dmy-Hi') . '.' . $this->format;
272 
273  // Return default filename if given filename is empty or not valid
274  if ($filename === '' || !preg_match('/^[0-9a-z._\-]+$/i', $filename)) {
275  return $defaultFilename;
276  }
277 
278  $extension = pathinfo($filename, PATHINFO_EXTENSION);
279  if ($extension === '') {
280  // Add original extension in case alternative filename did not contain any
281  $filename = rtrim($filename, '.') . '.' . $this->format;
282  }
283 
284  // Check if given or resolved extension matches the original one
285  return pathinfo($filename, PATHINFO_EXTENSION) === $this->format ? $filename : $defaultFilename;
286  }
287 
291  protected function getFormatOptionsWithResolvedDefaults(): array
292  {
293  $formatOptions = self::DOWNLOAD_FORMATS;
294 
295  if ($this->modTSconfig === []) {
296  return $formatOptions;
297  }
298 
299  if ($this->modTSconfig['csvDelimiter'] ?? false) {
300  $default = (string)$this->modTSconfig['csvDelimiter'];
301  if (!in_array($default, $formatOptions['csv']['options']['delimiter'], true)) {
302  // In case the user defined option is not yet available as format options, add it
303  $formatOptions['csv']['options']['delimiter']['custom'] = $default;
304  }
305  $formatOptions['csv']['defaults']['delimiter'] = $default;
306  }
307 
308  if ($this->modTSconfig['csvQuote'] ?? false) {
309  $default = (string)$this->modTSconfig['csvQuote'];
310  if (!in_array($default, $formatOptions['csv']['options']['quote'], true)) {
311  // In case the user defined option is not yet available as format options, add it
312  $formatOptions['csv']['options']['quote']['custom'] = $default;
313  }
314  $formatOptions['csv']['defaults']['quote'] = $default;
315  }
316 
317  return $formatOptions;
318  }
319 
320  protected function getFormatOptions(ServerRequestInterface $request): array
321  {
322  return $request->getParsedBody()[$this->format] ?? [];
323  }
324 
325  protected function getFormatOption(ServerRequestInterface $request, string $option, $default = null)
326  {
327  return $this->getFormatOptions($request)[$option]
328  ?? $this->getFormatOptionsWithResolvedDefaults()[$this->format]['defaults'][$option]
329  ?? $default;
330  }
331 
332  protected function generateDownloadResponse(string $result): ResponseInterface
333  {
334  $response = $this->responseFactory->createResponse()
335  ->withHeader('Content-Type', 'application/octet-stream')
336  ->withHeader('Content-Disposition', 'attachment; filename=' . $this->filename);
337  $response->getBody()->write($result);
338 
339  return $response;
340  }
341 
342  protected function getBackendUserAuthentication(): ‪BackendUserAuthentication
343  {
344  return ‪$GLOBALS['BE_USER'];
345  }
346 }
‪TYPO3\CMS\Backend\Attribute\AsController\__construct
‪__construct()
Definition: AsController.php:28
‪TYPO3\CMS\Backend\View\BackendViewFactory
Definition: BackendViewFactory.php:35
‪TYPO3\CMS\Backend\RecordList\DownloadRecordList
Definition: DownloadRecordList.php:35
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList
Definition: DatabaseRecordList.php:68
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Webhooks\Message\$record
‪identifier readonly int readonly array $record
Definition: PageModificationMessage.php:36
‪TYPO3\CMS\Core\Utility\CsvUtility
Definition: CsvUtility.php:26
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Backend\Exception\AccessDeniedException
Definition: AccessDeniedException.php:25
‪TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider
Definition: TranslationConfigurationProvider.php:39
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Controller\RecordListDownloadController
Definition: RecordListDownloadController.php:43
‪TYPO3\CMS\Core\Utility\GeneralUtility\inList
‪static bool inList($list, $item)
Definition: GeneralUtility.php:422
‪TYPO3\CMS\Backend\Attribute\AsController
Definition: AsController.php:25
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Controller
Definition: AboutController.php:18
‪TYPO3\CMS\Core\Utility\CsvUtility\csvValues
‪static string csvValues(array $row, string $delim=',', string $quote='"', int $type = self::TYPE_REMOVE_CONTROLS)
Definition: CsvUtility.php:100