‪TYPO3CMS  11.5
RecordDownloadController.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;
25 use TYPO3\CMS\Backend\Utility\BackendUtility;
34 
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 
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 
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 
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 
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 
285  protected function getSiteLanguages(ServerRequestInterface $request): array
286  {
287  $site = $request->getAttribute('site');
288  return $site->getAvailableLanguages($this->getBackendUserAuthentication(), false, $this->id);
289  }
290 
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 
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 }
‪TYPO3\CMS\Recordlist\Controller\RecordDownloadController
Definition: RecordDownloadController.php:41
‪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
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Recordlist\Controller\AccessDeniedException
Definition: AccessDeniedException.php:25
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList
Definition: DatabaseRecordList.php:59
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:40
‪TYPO3\CMS\Recordlist\Controller
Definition: AbstractLinkBrowserController.php:16
‪TYPO3\CMS\Core\Utility\CsvUtility
Definition: CsvUtility.php:24
‪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\Configuration\TranslationConfigurationProvider
Definition: TranslationConfigurationProvider.php:37
‪TYPO3\CMS\Fluid\View\StandaloneView
Definition: StandaloneView.php:31
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Recordlist\RecordList\DownloadRecordList
Definition: DownloadRecordList.php:35
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50