[BUGFIX] Add language parameter to preview url in list module
[Packages/TYPO3.CMS.git] / typo3 / sysext / recordlist / Classes / RecordList / AbstractDatabaseRecordList.php
1 <?php
2 namespace TYPO3\CMS\Recordlist\RecordList;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\RecordList\AbstractRecordList;
18 use TYPO3\CMS\Backend\Routing\Router;
19 use TYPO3\CMS\Backend\Routing\UriBuilder;
20 use TYPO3\CMS\Backend\Tree\View\PageTreeView;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23 use TYPO3\CMS\Core\Cache\CacheManager;
24 use TYPO3\CMS\Core\Database\ConnectionPool;
25 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26 use TYPO3\CMS\Core\Database\Query\QueryHelper;
27 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
28 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
29 use TYPO3\CMS\Core\Imaging\Icon;
30 use TYPO3\CMS\Core\Imaging\IconFactory;
31 use TYPO3\CMS\Core\Service\DependencyOrderingService;
32 use TYPO3\CMS\Core\Type\Bitmask\Permission;
33 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
34 use TYPO3\CMS\Core\Utility\GeneralUtility;
35 use TYPO3\CMS\Core\Utility\HttpUtility;
36 use TYPO3\CMS\Core\Utility\MathUtility;
37
38 /**
39 * Child class for rendering of Web > List (not the final class)
40 * Shared between Web>List and Web>Page
41 * @see \TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList
42 */
43 class AbstractDatabaseRecordList extends AbstractRecordList
44 {
45 /**
46 * Specify a list of tables which are the only ones allowed to be displayed.
47 *
48 * @var string
49 */
50 public $tableList = '';
51
52 /**
53 * Return URL
54 *
55 * @var string
56 */
57 public $returnUrl = '';
58
59 /**
60 * Thumbnails on records containing files (pictures)
61 *
62 * @var bool
63 */
64 public $thumbs = 0;
65
66 /**
67 * default Max items shown per table in "multi-table mode", may be overridden by tables.php
68 *
69 * @var int
70 */
71 public $itemsLimitPerTable = 20;
72
73 /**
74 * default Max items shown per table in "single-table mode", may be overridden by tables.php
75 *
76 * @var int
77 */
78 public $itemsLimitSingleTable = 100;
79
80 /**
81 * Current script name
82 *
83 * @var string
84 */
85 public $script = 'index.php';
86
87 /**
88 * Indicates if all available fields for a user should be selected or not.
89 *
90 * @var int
91 */
92 public $allFields = 0;
93
94 /**
95 * Whether to show localization view or not
96 *
97 * @var bool
98 */
99 public $localizationView = false;
100
101 /**
102 * If set, csvList is outputted.
103 *
104 * @var bool
105 */
106 public $csvOutput = false;
107
108 /**
109 * Field, to sort list by
110 *
111 * @var string
112 */
113 public $sortField;
114
115 /**
116 * Field, indicating to sort in reverse order.
117 *
118 * @var bool
119 */
120 public $sortRev;
121
122 /**
123 * Containing which fields to display in extended mode
124 *
125 * @var string[]
126 */
127 public $displayFields;
128
129 /**
130 * String, can contain the field name from a table which must have duplicate values marked.
131 *
132 * @var string
133 */
134 public $duplicateField;
135
136 /**
137 * Page id
138 *
139 * @var int
140 */
141 public $id;
142
143 /**
144 * Tablename if single-table mode
145 *
146 * @var string
147 */
148 public $table = '';
149
150 /**
151 * If TRUE, records are listed only if a specific table is selected.
152 *
153 * @var bool
154 */
155 public $listOnlyInSingleTableMode = false;
156
157 /**
158 * Pointer for browsing list
159 *
160 * @var int
161 */
162 public $firstElementNumber = 0;
163
164 /**
165 * Search string
166 *
167 * @var string
168 */
169 public $searchString = '';
170
171 /**
172 * Levels to search down.
173 *
174 * @var int
175 */
176 public $searchLevels = '';
177
178 /**
179 * Number of records to show
180 *
181 * @var int
182 */
183 public $showLimit = 0;
184
185 /**
186 * Page select permissions
187 *
188 * @var string
189 */
190 public $perms_clause = '';
191
192 /**
193 * Some permissions...
194 *
195 * @var int
196 */
197 public $calcPerms = 0;
198
199 /**
200 * Mode for what happens when a user clicks the title of a record.
201 *
202 * @var string
203 */
204 public $clickTitleMode = '';
205
206 /**
207 * Shared module configuration, used by localization features
208 *
209 * @var array
210 */
211 public $modSharedTSconfig = [];
212
213 /**
214 * Loaded with page record with version overlay if any.
215 *
216 * @var string[]
217 */
218 public $pageRecord = [];
219
220 /**
221 * Tables which should not get listed
222 *
223 * @var string
224 */
225 public $hideTables = '';
226
227 /**
228 * Tables which should not list their translations
229 *
230 * @var string
231 */
232 public $hideTranslations = '';
233
234 /**
235 * TSconfig which overwrites TCA-Settings
236 *
237 * @var mixed[][]
238 */
239 public $tableTSconfigOverTCA = [];
240
241 /**
242 * Array of collapsed / uncollapsed tables in multi table view
243 *
244 * @var int[][]
245 */
246 public $tablesCollapsed = [];
247
248 /**
249 * JavaScript code accumulation
250 *
251 * @var string
252 */
253 public $JScode = '';
254
255 /**
256 * HTML output
257 *
258 * @var string
259 */
260 public $HTMLcode = '';
261
262 /**
263 * "LIMIT " in SQL...
264 *
265 * @var int
266 */
267 public $iLimit = 0;
268
269 /**
270 * Counting the elements no matter what...
271 *
272 * @var int
273 */
274 public $eCounter = 0;
275
276 /**
277 * Set to the total number of items for a table when selecting.
278 *
279 * @var string
280 */
281 public $totalItems = '';
282
283 /**
284 * Cache for record path
285 *
286 * @var mixed[]
287 */
288 public $recPath_cache = [];
289
290 /**
291 * Fields to display for the current table
292 *
293 * @var string[]
294 */
295 public $setFields = [];
296
297 /**
298 * Used for tracking next/prev uids
299 *
300 * @var int[][]
301 */
302 public $currentTable = [];
303
304 /**
305 * Used for tracking duplicate values of fields
306 *
307 * @var string[]
308 */
309 public $duplicateStack = [];
310
311 /**
312 * @var array[] Module configuration
313 */
314 public $modTSconfig;
315
316 /**
317 * Override/add urlparameters in listUrl() method
318 * @var string[]
319 */
320 protected $overrideUrlParameters = [];
321
322 /**
323 * Override the page ids taken into account by getPageIdConstraint()
324 *
325 * @var array
326 */
327 protected $overridePageIdList = [];
328
329 /**
330 * Array with before/after setting for tables
331 * Structure:
332 * 'tableName' => [
333 * 'before' => ['A', ...]
334 * 'after' => []
335 * ]
336 * @var array[]
337 */
338 protected $tableDisplayOrder = [];
339
340 /**
341 * Initializes the list generation
342 *
343 * @param int $id Page id for which the list is rendered. Must be >= 0
344 * @param string $table Tablename - if extended mode where only one table is listed at a time.
345 * @param int $pointer Browsing pointer.
346 * @param string $search Search word, if any
347 * @param int $levels Number of levels to search down the page tree
348 * @param int $showLimit Limit of records to be listed.
349 */
350 public function start($id, $table, $pointer, $search = '', $levels = 0, $showLimit = 0)
351 {
352 $backendUser = $this->getBackendUserAuthentication();
353 // Setting internal variables:
354 // sets the parent id
355 $this->id = (int)$id;
356 if ($GLOBALS['TCA'][$table]) {
357 // Setting single table mode, if table exists:
358 $this->table = $table;
359 }
360 $this->firstElementNumber = $pointer;
361 $this->searchString = trim($search);
362 $this->searchLevels = (int)$levels;
363 $this->showLimit = MathUtility::forceIntegerInRange($showLimit, 0, 10000);
364 // Setting GPvars:
365 $this->csvOutput = (bool)GeneralUtility::_GP('csv');
366 $this->sortField = GeneralUtility::_GP('sortField');
367 $this->sortRev = GeneralUtility::_GP('sortRev');
368 $this->displayFields = GeneralUtility::_GP('displayFields');
369 $this->duplicateField = GeneralUtility::_GP('duplicateField');
370 if (GeneralUtility::_GP('justLocalized')) {
371 $this->localizationRedirect(GeneralUtility::_GP('justLocalized'));
372 }
373 // Init dynamic vars:
374 $this->counter = 0;
375 $this->JScode = '';
376 $this->HTMLcode = '';
377 // Limits
378 if (isset($this->modTSconfig['properties']['itemsLimitPerTable'])) {
379 $this->itemsLimitPerTable = MathUtility::forceIntegerInRange(
380 (int)$this->modTSconfig['properties']['itemsLimitPerTable'],
381 1,
382 10000
383 );
384 }
385 if (isset($this->modTSconfig['properties']['itemsLimitSingleTable'])) {
386 $this->itemsLimitSingleTable = MathUtility::forceIntegerInRange(
387 (int)$this->modTSconfig['properties']['itemsLimitSingleTable'],
388 1,
389 10000
390 );
391 }
392
393 // $table might be NULL at this point in the code. As the expressionBuilder
394 // is used to limit returned records based on the page permissions and the
395 // uid field of the pages it can hardcoded to work on the pages table.
396 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
397 ->getQueryBuilderForTable('pages')
398 ->expr();
399 $permsClause = $expressionBuilder->andX($backendUser->getPagePermsClause(1));
400 // This will hide records from display - it has nothing to do with user rights!!
401 if ($pidList = $backendUser->getTSConfigVal('options.hideRecords.pages')) {
402 $pidList = GeneralUtility::intExplode(',', $pidList, true);
403 if (!empty($pidList)) {
404 $permsClause->add($expressionBuilder->notIn('pages.uid', $pidList));
405 }
406 }
407 $this->perms_clause = (string)$permsClause;
408
409 // Get configuration of collapsed tables from user uc and merge with sanitized GP vars
410 $this->tablesCollapsed = is_array($backendUser->uc['moduleData']['list'])
411 ? $backendUser->uc['moduleData']['list']
412 : [];
413 $collapseOverride = GeneralUtility::_GP('collapse');
414 if (is_array($collapseOverride)) {
415 foreach ($collapseOverride as $collapseTable => $collapseValue) {
416 if (is_array($GLOBALS['TCA'][$collapseTable]) && ($collapseValue == 0 || $collapseValue == 1)) {
417 $this->tablesCollapsed[$collapseTable] = $collapseValue;
418 }
419 }
420 // Save modified user uc
421 $backendUser->uc['moduleData']['list'] = $this->tablesCollapsed;
422 $backendUser->writeUC($backendUser->uc);
423 $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
424 if ($returnUrl !== '') {
425 HttpUtility::redirect($returnUrl);
426 }
427 }
428
429 // Initialize languages:
430 if ($this->localizationView) {
431 $this->initializeLanguages();
432 }
433 }
434
435 /**
436 * Traverses the table(s) to be listed and renders the output code for each:
437 * The HTML is accumulated in $this->HTMLcode
438 * Finishes off with a stopper-gif
439 */
440 public function generateList()
441 {
442 // Set page record in header
443 $this->pageRecord = BackendUtility::getRecordWSOL('pages', $this->id);
444 $hideTablesArray = GeneralUtility::trimExplode(',', $this->hideTables);
445
446 $backendUser = $this->getBackendUserAuthentication();
447
448 // pre-process tables and add sorting instructions
449 $tableNames = array_flip(array_keys($GLOBALS['TCA']));
450 foreach ($tableNames as $tableName => &$config) {
451 $hideTable = false;
452
453 // Checking if the table should be rendered:
454 // Checks that we see only permitted/requested tables:
455 if ($this->table && $tableName !== $this->table
456 || $this->tableList && !GeneralUtility::inList($this->tableList, $tableName)
457 || !$backendUser->check('tables_select', $tableName)
458 ) {
459 $hideTable = true;
460 }
461
462 if (!$hideTable) {
463 // Don't show table if hidden by TCA ctrl section
464 // Don't show table if hidden by pageTSconfig mod.web_list.hideTables
465 $hideTable = $hideTable
466 || !empty($GLOBALS['TCA'][$tableName]['ctrl']['hideTable'])
467 || in_array($tableName, $hideTablesArray, true)
468 || in_array('*', $hideTablesArray, true);
469 // Override previous selection if table is enabled or hidden by TSconfig TCA override mod.web_list.table
470 if (isset($this->tableTSconfigOverTCA[$tableName . '.']['hideTable'])) {
471 $hideTable = (bool)$this->tableTSconfigOverTCA[$tableName . '.']['hideTable'];
472 }
473 }
474 if ($hideTable) {
475 unset($tableNames[$tableName]);
476 } else {
477 if (isset($this->tableDisplayOrder[$tableName])) {
478 // Copy display order information
479 $tableNames[$tableName] = $this->tableDisplayOrder[$tableName];
480 } else {
481 $tableNames[$tableName] = [];
482 }
483 }
484 }
485 unset($config);
486
487 $orderedTableNames = GeneralUtility::makeInstance(DependencyOrderingService::class)
488 ->orderByDependencies($tableNames);
489
490 foreach ($orderedTableNames as $tableName => $_) {
491 // check if we are in single- or multi-table mode
492 if ($this->table) {
493 $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems'])
494 ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems']
495 : $this->itemsLimitSingleTable;
496 } else {
497 // if there are no records in table continue current foreach
498 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
499 ->getQueryBuilderForTable($tableName);
500 $queryBuilder->getRestrictions()
501 ->removeAll()
502 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
503 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
504 $firstRow = $queryBuilder->select('uid')
505 ->from($tableName)
506 ->where($this->getPageIdConstraint($tableName))
507 ->execute()
508 ->fetch();
509 if (!is_array($firstRow)) {
510 continue;
511 }
512 $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxDBListItems'])
513 ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxDBListItems']
514 : $this->itemsLimitPerTable;
515 }
516 if ($this->showLimit) {
517 $this->iLimit = $this->showLimit;
518 }
519 // Setting fields to select:
520 if ($this->allFields) {
521 $fields = $this->makeFieldList($tableName);
522 $fields[] = 'tstamp';
523 $fields[] = 'crdate';
524 $fields[] = '_PATH_';
525 $fields[] = '_CONTROL_';
526 if (is_array($this->setFields[$tableName])) {
527 $fields = array_intersect($fields, $this->setFields[$tableName]);
528 } else {
529 $fields = [];
530 }
531 } else {
532 $fields = [];
533 }
534
535 // Finally, render the list:
536 $this->HTMLcode .= $this->getTable($tableName, $this->id, implode(',', $fields));
537 }
538 }
539
540 /**
541 * To be implemented in extending classes.
542 *
543 * @param string $tableName
544 * @param int $id
545 * @param string $fields List of fields to show in the listing. Pseudo fields will be added including the record header.
546 * @return string HTML code
547 */
548 public function getTable($tableName, $id, $fields = '')
549 {
550 return '';
551 }
552
553 /**
554 * Get viewOnClick link for pages or tt_content records
555 *
556 * @param string $table
557 * @param array $row
558 *
559 * @return string
560 */
561 protected function getOnClickForRow(string $table, array $row): string
562 {
563 if ($table === 'tt_content') {
564 // Link to a content element, possibly translated and with anchor
565 $additionalParams = '';
566 $language = (int)$row[$GLOBALS['TCA']['tt_content']['ctrl']['languageField']];
567 if ($language > 0) {
568 $additionalParams = '&L=' . $language;
569 }
570 $onClick = BackendUtility::viewOnClick(
571 $this->id,
572 '',
573 null,
574 '',
575 '',
576 $additionalParams
577 );
578 } else {
579 // Link to a page in the default language
580 $onClick = BackendUtility::viewOnClick($row['uid']);
581 }
582 return $onClick;
583 }
584
585 /**
586 * Creates the search box
587 *
588 * @param bool $formFields If TRUE, the search box is wrapped in its own form-tags
589 * @return string HTML for the search box
590 */
591 public function getSearchBox($formFields = true)
592 {
593 /** @var $iconFactory IconFactory */
594 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
595 $lang = $this->getLanguageService();
596 // Setting form-elements, if applicable:
597 $formElements = ['', ''];
598 if ($formFields) {
599 $formElements = ['<form action="' . htmlspecialchars($this->listURL('', '-1', 'firstElementNumber,search_field')) . '" method="post">', '</form>'];
600 }
601 // Make level selector:
602 $opt = [];
603
604 // "New" generation of search levels ... based on TS config
605 $config = BackendUtility::getPagesTSconfig($this->id);
606 $searchLevelsFromTSconfig = $config['mod.']['web_list.']['searchLevel.']['items.'];
607 $searchLevelItems = [];
608
609 // get translated labels for search levels from pagets
610 foreach ($searchLevelsFromTSconfig as $keySearchLevel => $labelConfigured) {
611 $label = $lang->sL('LLL:' . $labelConfigured, false);
612 if ($label === '') {
613 $label = $labelConfigured;
614 }
615 $searchLevelItems[$keySearchLevel] = $label;
616 }
617
618 foreach ($searchLevelItems as $kv => $label) {
619 $opt[] = '<option value="' . $kv . '"' . ($kv === $this->searchLevels ? ' selected="selected"' : '') . '>' . htmlspecialchars($label) . '</option>';
620 }
621 $lMenu = '<select class="form-control" name="search_levels" title="' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.title.search_levels')) . '" id="search_levels">' . implode('', $opt) . '</select>';
622 // Table with the search box:
623 $content = '<div class="db_list-searchbox-form db_list-searchbox-toolbar module-docheader-bar module-docheader-bar-search t3js-module-docheader-bar t3js-module-docheader-bar-search" id="db_list-searchbox-toolbar" style="display: ' . ($this->searchString == '' ? 'none' : 'block') . ';">
624 ' . $formElements[0] . '
625 <div id="typo3-dblist-search">
626 <div class="panel panel-default">
627 <div class="panel-body">
628 <div class="row">
629 <div class="form-group col-xs-12">
630 <label for="search_field">' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.label.searchString')) . ': </label>
631 <input class="form-control" type="search" placeholder="' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.enterSearchString')) . '" title="' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.title.searchString')) . '" name="search_field" id="search_field" value="' . htmlspecialchars($this->searchString) . '" />
632 </div>
633 <div class="form-group col-xs-12 col-sm-6">
634 <label for="search_levels">' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.label.search_levels')) . ': </label>
635 ' . $lMenu . '
636 </div>
637 <div class="form-group col-xs-12 col-sm-6">
638 <label for="showLimit">' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.label.limit')) . ': </label>
639 <input class="form-control" type="number" min="0" max="10000" placeholder="10" title="' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.title.limit')) . '" name="showLimit" id="showLimit" value="' . htmlspecialchars(($this->showLimit ? $this->showLimit : '')) . '" />
640 </div>
641 <div class="form-group col-xs-12">
642 <div class="form-control-wrap">
643 <button type="submit" class="btn btn-default" name="search" title="' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.title.search')) . '">
644 ' . $iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render() . ' ' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.search')) . '
645 </button>
646 </div>
647 </div>
648 </div>
649 </div>
650 </div>
651 </div>
652 ' . $formElements[1] . '</div>';
653 return $content;
654 }
655
656 /******************************
657 *
658 * Various helper functions
659 *
660 ******************************/
661 /**
662 * Setting the field names to display in extended list.
663 * Sets the internal variable $this->setFields
664 */
665 public function setDispFields()
666 {
667 $backendUser = $this->getBackendUserAuthentication();
668 // Getting from session:
669 $dispFields = $backendUser->getModuleData('list/displayFields');
670 // If fields has been inputted, then set those as the value and push it to session variable:
671 if (is_array($this->displayFields)) {
672 reset($this->displayFields);
673 $tKey = key($this->displayFields);
674 $dispFields[$tKey] = $this->displayFields[$tKey];
675 $backendUser->pushModuleData('list/displayFields', $dispFields);
676 }
677 // Setting result:
678 $this->setFields = $dispFields;
679 }
680
681 /**
682 * Create thumbnail code for record/field
683 *
684 * @param mixed[] $row Record array
685 * @param string $table Table (record is from)
686 * @param string $field Field name for which thumbnail are to be rendered.
687 * @return string HTML for thumbnails, if any.
688 */
689 public function thumbCode($row, $table, $field)
690 {
691 return BackendUtility::thumbCode($row, $table, $field);
692 }
693
694 /**
695 * Returns the SQL-query array to select the records from a table $table with pid = $id
696 *
697 * @param string $table Table name
698 * @param int $id Page id (NOT USED! $this->pidSelect is used instead)
699 * @param string $addWhere Additional part for where clause
700 * @param string $fieldList Field list to select, * for all (for "SELECT [fieldlist] FROM ...")
701 * @return string[] Returns query array
702 *
703 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9. Please use getQueryBuilder()
704 */
705 public function makeQueryArray($table, $id, $addWhere = '', $fieldList = '*')
706 {
707 GeneralUtility::logDeprecatedFunction();
708 $hookObjectsArr = [];
709 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list.inc']['makeQueryArray'])) {
710 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list.inc']['makeQueryArray'] as $classRef) {
711 $hookObjectsArr[] = GeneralUtility::getUserObj($classRef);
712 }
713 }
714 // Set ORDER BY:
715 $orderBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ? 'ORDER BY ' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : $GLOBALS['TCA'][$table]['ctrl']['default_sortby'];
716 if ($this->sortField) {
717 if (in_array($this->sortField, $this->makeFieldList($table, 1))) {
718 $orderBy = 'ORDER BY ' . $this->sortField;
719 if ($this->sortRev) {
720 $orderBy .= ' DESC';
721 }
722 }
723 }
724 // Set LIMIT:
725 $limit = $this->iLimit ? ($this->firstElementNumber ? $this->firstElementNumber . ',' : '') . $this->iLimit : '';
726 // Filtering on displayable pages (permissions):
727 $pC = $table === 'pages' && $this->perms_clause ? ' AND ' . $this->perms_clause : '';
728 // Adding search constraints:
729 $search = $this->makeSearchString($table, $id);
730 // Compiling query array:
731 $queryParts = [
732 'SELECT' => $fieldList,
733 'FROM' => $table,
734 'WHERE' => $this->getPageIdConstraint($table) . ' ' . $pC . BackendUtility::deleteClause($table) . BackendUtility::versioningPlaceholderClause($table) . ' ' . $addWhere . ' ' . $search,
735 'GROUPBY' => '',
736 'LIMIT' => $limit
737 ];
738 $tempOrderBy = [];
739 foreach (QueryHelper::parseOrderBy($orderBy) as $orderPair) {
740 list($fieldName, $order) = $orderPair;
741 if ($order !== null) {
742 $tempOrderBy[] = implode(' ', $orderPair);
743 } else {
744 $tempOrderBy[] = $fieldName;
745 }
746 }
747 $queryParts['ORDERBY'] = implode(',', $tempOrderBy);
748 // Filter out records that are translated, if TSconfig mod.web_list.hideTranslations is set
749 if ((in_array($table, GeneralUtility::trimExplode(',', $this->hideTranslations)) || $this->hideTranslations === '*') && !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) && $table !== 'pages_language_overlay') {
750 $queryParts['WHERE'] .= ' AND ' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] . '=0 ';
751 }
752 // Apply hook as requested in http://forge.typo3.org/issues/16634
753 foreach ($hookObjectsArr as $hookObj) {
754 if (method_exists($hookObj, 'makeQueryArray_post')) {
755 $_params = [
756 'orderBy' => $orderBy,
757 'limit' => $limit,
758 'pC' => $pC,
759 'search' => $search
760 ];
761 $hookObj->makeQueryArray_post($queryParts, $this, $table, $id, $addWhere, $fieldList, $_params);
762 }
763 }
764 // Return query:
765 return $queryParts;
766 }
767
768 /**
769 * Returns a QueryBuilder configured to select $fields from $table where the pid is restricted
770 * depending on the current searchlevel setting.
771 *
772 * @param string $table Table name
773 * @param int $pageId Page id Only used to build the search constraints, getPageIdConstraint() used for restrictions
774 * @param string[] $additionalConstraints Additional part for where clause
775 * @param string[] $fields Field list to select, * for all
776 * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder
777 */
778 public function getQueryBuilder(
779 string $table,
780 int $pageId,
781 array $additionalConstraints = [],
782 array $fields = ['*']
783 ): QueryBuilder {
784 $queryParameters = $this->buildQueryParameters($table, $pageId, $fields, $additionalConstraints);
785
786 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
787 ->getQueryBuilderForTable($queryParameters['table']);
788 $queryBuilder->getRestrictions()
789 ->removeAll()
790 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
791 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
792 $queryBuilder
793 ->select(...$queryParameters['fields'])
794 ->from($queryParameters['table'])
795 ->where(...$queryParameters['where']);
796
797 if (!empty($queryParameters['orderBy'])) {
798 foreach ($queryParameters['orderBy'] as $fieldNameAndSorting) {
799 list($fieldName, $sorting) = $fieldNameAndSorting;
800 $queryBuilder->addOrderBy($fieldName, $sorting);
801 }
802 }
803
804 if (!empty($queryParameters['firstResult'])) {
805 $queryBuilder->setFirstResult((int)$queryParameters['firstResult']);
806 }
807
808 if (!empty($queryParameters['maxResults'])) {
809 $queryBuilder->setMaxResults((int)$queryParameters['maxResults']);
810 }
811
812 if (!empty($queryParameters['groupBy'])) {
813 $queryBuilder->groupBy($queryParameters['groupBy']);
814 }
815
816 return $queryBuilder;
817 }
818
819 /**
820 * Return the query parameters to select the records from a table $table with pid = $this->pidList
821 *
822 * @param string $table Table name
823 * @param int $pageId Page id Only used to build the search constraints, $this->pidList is used for restrictions
824 * @param string[] $fieldList List of fields to select from the table
825 * @param string[] $additionalConstraints Additional part for where clause
826 * @param bool $addSorting Add sorting fields to query
827 * @return array
828 */
829 protected function buildQueryParameters(
830 string $table,
831 int $pageId,
832 array $fieldList = ['*'],
833 array $additionalConstraints = [],
834 bool $addSorting = true
835 ): array {
836 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
837 ->getQueryBuilderForTable($table)
838 ->expr();
839
840 $parameters = [
841 'table' => $table,
842 'fields' => $fieldList,
843 'groupBy' => null,
844 'orderBy' => null,
845 'firstResult' => $this->firstElementNumber ?: null,
846 'maxResults' => $this->iLimit ? $this->iLimit : null,
847 ];
848
849 if ($addSorting === true) {
850 if ($this->sortField && in_array($this->sortField, $this->makeFieldList($table, 1))) {
851 $parameters['orderBy'][] = $this->sortRev ? [$this->sortField, 'DESC'] : [$this->sortField, 'ASC'];
852 } else {
853 $orderBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?: $GLOBALS['TCA'][$table]['ctrl']['default_sortby'];
854 $parameters['orderBy'] = QueryHelper::parseOrderBy((string)$orderBy);
855 }
856 }
857
858 // Build the query constraints
859 $constraints = [
860 'pidSelect' => $this->getPageIdConstraint($table),
861 'search' => $this->makeSearchString($table, $pageId)
862 ];
863
864 // Filtering on displayable pages (permissions):
865 if ($table === 'pages' && $this->perms_clause) {
866 $constraints['pagePermsClause'] = $this->perms_clause;
867 }
868
869 // Filter out records that are translated, if TSconfig mod.web_list.hideTranslations is set
870 if ((GeneralUtility::inList($this->hideTranslations, $table) || $this->hideTranslations === '*')
871 && !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])
872 && $table !== 'pages_language_overlay'
873 ) {
874 $constraints['transOrigPointerField'] = $expressionBuilder->eq(
875 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
876 0
877 );
878 }
879
880 $parameters['where'] = array_merge($constraints, $additionalConstraints);
881
882 $hookName = DatabaseRecordList::class;
883 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['buildQueryParameters'])) {
884 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['buildQueryParameters'] as $classRef) {
885 $hookObject = GeneralUtility::getUserObj($classRef);
886 if (method_exists($hookObject, 'buildQueryParametersPostProcess')) {
887 $hookObject->buildQueryParametersPostProcess(
888 $parameters,
889 $table,
890 $pageId,
891 $additionalConstraints,
892 $fieldList,
893 $this
894 );
895 }
896 }
897 }
898
899 // array_unique / array_filter used to eliminate empty and duplicate constraints
900 // the array keys are eliminated by this as well to facilitate argument unpacking
901 // when used with the querybuilder.
902 $parameters['where'] = array_unique(array_filter(array_values($parameters['where'])));
903
904 return $parameters;
905 }
906
907 /**
908 * Set the total items for the record list
909 *
910 * @param string $table Table name
911 * @param int $pageId Only used to build the search constraints, $this->pidList is used for restrictions
912 * @param array $constraints Additional constraints for where clause
913 */
914 public function setTotalItems(string $table, int $pageId, array $constraints)
915 {
916 $queryParameters = $this->buildQueryParameters($table, $pageId, ['*'], $constraints, false);
917 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
918 ->getQueryBuilderForTable($queryParameters['table']);
919 $queryBuilder->getRestrictions()
920 ->removeAll()
921 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
922 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
923 $queryBuilder
924 ->from($queryParameters['table'])
925 ->where(...$queryParameters['where']);
926
927 $this->totalItems = (int)$queryBuilder->count('*')
928 ->execute()
929 ->fetchColumn();
930 }
931
932 /**
933 * Creates part of query for searching after a word ($this->searchString)
934 * fields in input table.
935 *
936 * @param string $table Table, in which the fields are being searched.
937 * @param int $currentPid Page id for the possible search limit. -1 only if called from an old XCLASS.
938 * @return string Returns part of WHERE-clause for searching, if applicable.
939 */
940 public function makeSearchString($table, $currentPid = -1)
941 {
942 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
943 $expressionBuilder = $queryBuilder->expr();
944 $constraints = [];
945 $currentPid = (int)$currentPid;
946 $tablePidField = $table === 'pages' ? 'uid' : 'pid';
947 // Make query only if table is valid and a search string is actually defined
948 if (empty($this->searchString)) {
949 return '';
950 }
951
952 $searchableFields = $this->getSearchFields($table);
953 if (MathUtility::canBeInterpretedAsInteger($this->searchString)) {
954 $constraints[] = $expressionBuilder->eq('uid', (int)$this->searchString);
955 foreach ($searchableFields as $fieldName) {
956 if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
957 continue;
958 }
959 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
960 $fieldType = $fieldConfig['type'];
961 $evalRules = $fieldConfig['eval'] ?: '';
962 if ($fieldType === 'input' && $evalRules && GeneralUtility::inList($evalRules, 'int')) {
963 if (is_array($fieldConfig['search'])
964 && in_array('pidonly', $fieldConfig['search'], true)
965 && $currentPid > 0
966 ) {
967 $constraints[] = $expressionBuilder->andX(
968 $expressionBuilder->eq($fieldName, (int)$this->searchString),
969 $expressionBuilder->eq($tablePidField, (int)$currentPid)
970 );
971 }
972 } elseif ($fieldType === 'text'
973 || $fieldType === 'flex'
974 || ($fieldType === 'input' && (!$evalRules || !preg_match('/date|time|int/', $evalRules)))
975 ) {
976 $constraints[] = $expressionBuilder->like(
977 $fieldName,
978 $queryBuilder->quote('%' . (int)$this->searchString . '%')
979 );
980 }
981 }
982 } elseif (!empty($searchableFields)) {
983 $like = $queryBuilder->quote('%' . $queryBuilder->escapeLikeWildcards($this->searchString) . '%');
984 foreach ($searchableFields as $fieldName) {
985 if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
986 continue;
987 }
988 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
989 $fieldType = $fieldConfig['type'];
990 $evalRules = $fieldConfig['eval'] ?: '';
991 $searchConstraint = $expressionBuilder->andX(
992 $expressionBuilder->comparison(
993 'LOWER(' . $queryBuilder->quoteIdentifier($fieldName) . ')',
994 'LIKE',
995 'LOWER(' . $like . ')'
996 )
997 );
998 if (is_array($fieldConfig['search'])) {
999 $searchConfig = $fieldConfig['search'];
1000 if (in_array('case', $searchConfig)) {
1001 // Replace case insensitive default constraint
1002 $searchConstraint = $expressionBuilder->andX($expressionBuilder->like($fieldName, $like));
1003 }
1004 if (in_array('pidonly', $searchConfig) && $currentPid > 0) {
1005 $searchConstraint->add($expressionBuilder->eq($tablePidField, (int)$currentPid));
1006 }
1007 if ($searchConfig['andWhere']) {
1008 $searchConstraint->add(
1009 QueryHelper::stripLogicalOperatorPrefix($fieldConfig['search']['andWhere'])
1010 );
1011 }
1012 }
1013 if ($fieldType === 'text'
1014 || $fieldType === 'flex'
1015 || $fieldType === 'input' && (!$evalRules || !preg_match('/date|time|int/', $evalRules))
1016 ) {
1017 if ($searchConstraint->count() !== 0) {
1018 $constraints[] = $searchConstraint;
1019 }
1020 }
1021 }
1022 }
1023 // If no search field conditions have been built ensure no results are returned
1024 if (empty($constraints)) {
1025 return '0=1';
1026 }
1027
1028 return $expressionBuilder->orX(...$constraints);
1029 }
1030
1031 /**
1032 * Fetches a list of fields to use in the Backend search for the given table.
1033 *
1034 * @param string $tableName
1035 * @return string[]
1036 */
1037 protected function getSearchFields($tableName)
1038 {
1039 $fieldArray = [];
1040 $fieldListWasSet = false;
1041 // Get fields from ctrl section of TCA first
1042 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['searchFields'])) {
1043 $fieldArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$tableName]['ctrl']['searchFields'], true);
1044 $fieldListWasSet = true;
1045 }
1046 // Call hook to add or change the list
1047 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'])) {
1048 $hookParameters = [
1049 'tableHasSearchConfiguration' => $fieldListWasSet,
1050 'tableName' => $tableName,
1051 'searchFields' => &$fieldArray,
1052 'searchString' => $this->searchString
1053 ];
1054 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'] as $hookFunction) {
1055 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
1056 }
1057 }
1058 return $fieldArray;
1059 }
1060
1061 /**
1062 * Returns the title (based on $code) of a table ($table) with the proper link around. For headers over tables.
1063 * The link will cause the display of all extended mode or not for the table.
1064 *
1065 * @param string $table Table name
1066 * @param string $code Table label
1067 * @return string The linked table label
1068 */
1069 public function linkWrapTable($table, $code)
1070 {
1071 if ($this->table !== $table) {
1072 return '<a href="' . htmlspecialchars($this->listURL('', $table, 'firstElementNumber')) . '">' . $code . '</a>';
1073 }
1074 return '<a href="' . htmlspecialchars($this->listURL('', '', 'sortField,sortRev,table,firstElementNumber')) . '">' . $code . '</a>';
1075 }
1076
1077 /**
1078 * Returns the title (based on $code) of a record (from table $table) with the proper link around (that is for 'pages'-records a link to the level of that record...)
1079 *
1080 * @param string $table Table name
1081 * @param int $uid Item uid
1082 * @param string $code Item title (not htmlspecialchars()'ed yet)
1083 * @param mixed[] $row Item row
1084 * @return string The item title. Ready for HTML output (is htmlspecialchars()'ed)
1085 */
1086 public function linkWrapItems($table, $uid, $code, $row)
1087 {
1088 $lang = $this->getLanguageService();
1089 $origCode = $code;
1090 // If the title is blank, make a "no title" label:
1091 if ((string)$code === '') {
1092 $code = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</i> - '
1093 . htmlspecialchars(BackendUtility::getRecordTitle($table, $row));
1094 } else {
1095 $code = htmlspecialchars($code, ENT_QUOTES, 'UTF-8', false);
1096 if ($code != htmlspecialchars($origCode)) {
1097 $code = '<span title="' . htmlspecialchars($origCode, ENT_QUOTES, 'UTF-8', false) . '">' . $code . '</span>';
1098 }
1099 }
1100 switch ((string)$this->clickTitleMode) {
1101 case 'edit':
1102 // If the listed table is 'pages' we have to request the permission settings for each page:
1103 if ($table === 'pages') {
1104 $localCalcPerms = $this->getBackendUserAuthentication()->calcPerms(BackendUtility::getRecord('pages', $row['uid']));
1105 $permsEdit = $localCalcPerms & Permission::PAGE_EDIT;
1106 } else {
1107 $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT;
1108 }
1109 // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id)
1110 if ($permsEdit) {
1111 $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit';
1112 $code = '<a href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1)) . '" title="' . htmlspecialchars($lang->getLL('edit')) . '">' . $code . '</a>';
1113 }
1114 break;
1115 case 'show':
1116 // "Show" link (only pages and tt_content elements)
1117 if ($table === 'pages' || $table === 'tt_content') {
1118 $onClick = $this->getOnClickForRow($table, $row);
1119 $code = '<a href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">' . $code . '</a>';
1120 }
1121 break;
1122 case 'info':
1123 // "Info": (All records)
1124 $code = '<a href="#" onclick="' . htmlspecialchars('top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . ');')
1125 . '" title="' . htmlspecialchars($lang->getLL('showInfo')) . '">' . $code . '</a>';
1126 break;
1127 default:
1128 // Output the label now:
1129 if ($table === 'pages') {
1130 $code = '<a href="' . htmlspecialchars($this->listURL($uid, '', 'firstElementNumber')) . '" onclick="setHighlight(' . (int)$uid . ')">' . $code . '</a>';
1131 } else {
1132 $code = $this->linkUrlMail($code, $origCode);
1133 }
1134 }
1135 return $code;
1136 }
1137
1138 /**
1139 * Wrapping input code in link to URL or email if $testString is either.
1140 *
1141 * @param string $code code to wrap
1142 * @param string $testString String which is tested for being a URL or email and which will be used for the link if so.
1143 * @return string Link-Wrapped $code value, if $testString was URL or email.
1144 */
1145 public function linkUrlMail($code, $testString)
1146 {
1147 // Check for URL:
1148 $scheme = parse_url($testString, PHP_URL_SCHEME);
1149 if ($scheme === 'http' || $scheme === 'https' || $scheme === 'ftp') {
1150 return '<a href="' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
1151 }
1152 // Check for email:
1153 if (GeneralUtility::validEmail($testString)) {
1154 return '<a href="mailto:' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
1155 }
1156 // Return if nothing else...
1157 return $code;
1158 }
1159
1160 /**
1161 * Creates the URL to this script, including all relevant GPvars
1162 * Fixed GPvars are id, table, imagemode, returnUrl, search_field, search_levels and showLimit
1163 * The GPvars "sortField" and "sortRev" are also included UNLESS they are found in the $exclList variable.
1164 *
1165 * @param string $altId Alternative id value. Enter blank string for the current id ($this->id)
1166 * @param string $table Table name to display. Enter "-1" for the current table.
1167 * @param string $exclList Comma separated list of fields NOT to include ("sortField", "sortRev" or "firstElementNumber")
1168 * @return string URL
1169 */
1170 public function listURL($altId = '', $table = '-1', $exclList = '')
1171 {
1172 $urlParameters = [];
1173 if ((string)$altId !== '') {
1174 $urlParameters['id'] = $altId;
1175 } else {
1176 $urlParameters['id'] = $this->id;
1177 }
1178 if ($table === '-1') {
1179 $urlParameters['table'] = $this->table;
1180 } else {
1181 $urlParameters['table'] = $table;
1182 }
1183 if ($this->thumbs) {
1184 $urlParameters['imagemode'] = $this->thumbs;
1185 }
1186 if ($this->returnUrl) {
1187 $urlParameters['returnUrl'] = $this->returnUrl;
1188 }
1189 if ((!$exclList || !GeneralUtility::inList($exclList, 'search_field')) && $this->searchString) {
1190 $urlParameters['search_field'] = $this->searchString;
1191 }
1192 if ($this->searchLevels) {
1193 $urlParameters['search_levels'] = $this->searchLevels;
1194 }
1195 if ($this->showLimit) {
1196 $urlParameters['showLimit'] = $this->showLimit;
1197 }
1198 if ((!$exclList || !GeneralUtility::inList($exclList, 'firstElementNumber')) && $this->firstElementNumber) {
1199 $urlParameters['pointer'] = $this->firstElementNumber;
1200 }
1201 if ((!$exclList || !GeneralUtility::inList($exclList, 'sortField')) && $this->sortField) {
1202 $urlParameters['sortField'] = $this->sortField;
1203 }
1204 if ((!$exclList || !GeneralUtility::inList($exclList, 'sortRev')) && $this->sortRev) {
1205 $urlParameters['sortRev'] = $this->sortRev;
1206 }
1207
1208 $urlParameters = array_merge_recursive($urlParameters, $this->overrideUrlParameters);
1209
1210 if ($routePath = GeneralUtility::_GP('route')) {
1211 $router = GeneralUtility::makeInstance(Router::class);
1212 $route = $router->match($routePath);
1213 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1214 $url = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'), $urlParameters);
1215 } elseif ($moduleName = GeneralUtility::_GP('M')) {
1216 $url = BackendUtility::getModuleUrl($moduleName, $urlParameters);
1217 } else {
1218 $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
1219 }
1220 return $url;
1221 }
1222
1223 /**
1224 * Returns "requestUri" - which is basically listURL
1225 *
1226 * @return string Content of ->listURL()
1227 */
1228 public function requestUri()
1229 {
1230 return $this->listURL();
1231 }
1232
1233 /**
1234 * Makes the list of fields to select for a table
1235 *
1236 * @param string $table Table name
1237 * @param bool $dontCheckUser If set, users access to the field (non-exclude-fields) is NOT checked.
1238 * @param bool $addDateFields If set, also adds crdate and tstamp fields (note: they will also be added if user is admin or dontCheckUser is set)
1239 * @return string[] Array, where values are fieldnames to include in query
1240 */
1241 public function makeFieldList($table, $dontCheckUser = false, $addDateFields = false)
1242 {
1243 $backendUser = $this->getBackendUserAuthentication();
1244 // Init fieldlist array:
1245 $fieldListArr = [];
1246 // Check table:
1247 if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['columns']) && is_array($GLOBALS['TCA'][$table]['columns'])) {
1248 if (isset($GLOBALS['TCA'][$table]['columns']) && is_array($GLOBALS['TCA'][$table]['columns'])) {
1249 // Traverse configured columns and add them to field array, if available for user.
1250 foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fieldValue) {
1251 if ($dontCheckUser || (!$fieldValue['exclude'] || $backendUser->check('non_exclude_fields', $table . ':' . $fN)) && $fieldValue['config']['type'] !== 'passthrough') {
1252 $fieldListArr[] = $fN;
1253 }
1254 }
1255
1256 $fieldListArr[] = 'uid';
1257 $fieldListArr[] = 'pid';
1258
1259 // Add date fields
1260 if ($dontCheckUser || $backendUser->isAdmin() || $addDateFields) {
1261 if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1262 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['tstamp'];
1263 }
1264 if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1265 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['crdate'];
1266 }
1267 }
1268 // Add more special fields:
1269 if ($dontCheckUser || $backendUser->isAdmin()) {
1270 if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1271 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['cruser_id'];
1272 }
1273 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
1274 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
1275 }
1276 if (ExtensionManagementUtility::isLoaded('version') && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1277 $fieldListArr[] = 't3ver_id';
1278 $fieldListArr[] = 't3ver_state';
1279 $fieldListArr[] = 't3ver_wsid';
1280 }
1281 }
1282 } else {
1283 GeneralUtility::sysLog(sprintf('$TCA is broken for the table "%s": no required "columns" entry in $TCA.', $table), 'core', GeneralUtility::SYSLOG_SEVERITY_ERROR);
1284 }
1285 }
1286 return $fieldListArr;
1287 }
1288
1289 /**
1290 * Get all allowed mount pages to be searched in.
1291 *
1292 * @param int $id Page id
1293 * @param int $depth Depth to go down
1294 * @param string $perms_clause select clause
1295 * @return int[]
1296 */
1297 protected function getSearchableWebmounts($id, $depth, $perms_clause)
1298 {
1299 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
1300 $cacheIdentifier = md5('pidList_' . $id . '_' . $depth . '_' . $perms_clause);
1301 $idList = $runtimeCache->get($cacheIdentifier);
1302 if ($idList) {
1303 return $idList;
1304 }
1305
1306 $backendUser = $this->getBackendUserAuthentication();
1307 /** @var PageTreeView $tree */
1308 $tree = GeneralUtility::makeInstance(PageTreeView::class);
1309 $tree->init('AND ' . $perms_clause);
1310 $tree->makeHTML = 0;
1311 $tree->fieldArray = ['uid', 'php_tree_stop'];
1312 $idList = [];
1313
1314 $allowedMounts = !$backendUser->isAdmin() && $id === 0
1315 ? $backendUser->returnWebmounts()
1316 : [$id];
1317
1318 foreach ($allowedMounts as $allowedMount) {
1319 $idList[] = $allowedMount;
1320 if ($depth) {
1321 $tree->getTree($allowedMount, $depth, '');
1322 }
1323 $idList = array_merge($idList, $tree->ids);
1324 }
1325 $runtimeCache->set($cacheIdentifier, $idList);
1326 return $idList;
1327 }
1328
1329 /**
1330 * Redirects to FormEngine if a record is just localized.
1331 *
1332 * @param string $justLocalized String with table, orig uid and language separated by ":
1333 */
1334 public function localizationRedirect($justLocalized)
1335 {
1336 list($table, $orig_uid, $language) = explode(':', $justLocalized);
1337 if ($GLOBALS['TCA'][$table]
1338 && $GLOBALS['TCA'][$table]['ctrl']['languageField']
1339 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
1340 ) {
1341 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1342 $queryBuilder->getRestrictions()
1343 ->removeAll()
1344 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1345 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1346
1347 $localizedRecordUid = $queryBuilder->select('uid')
1348 ->from($table)
1349 ->where(
1350 $queryBuilder->expr()->eq(
1351 $GLOBALS['TCA'][$table]['ctrl']['languageField'],
1352 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
1353 ),
1354 $queryBuilder->expr()->eq(
1355 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
1356 $queryBuilder->createNamedParameter($orig_uid, \PDO::PARAM_INT)
1357 )
1358 )
1359 ->setMaxResults(1)
1360 ->execute()
1361 ->fetchColumn();
1362
1363 if ($localizedRecordUid !== false) {
1364 // Create parameters and finally run the classic page module for creating a new page translation
1365 $url = $this->listURL();
1366 $editUserAccountUrl = BackendUtility::getModuleUrl(
1367 'record_edit',
1368 [
1369 'edit[' . $table . '][' . $localizedRecordUid . ']' => 'edit',
1370 'returnUrl' => $url
1371 ]
1372 );
1373 HttpUtility::redirect($editUserAccountUrl);
1374 }
1375 }
1376 }
1377
1378 /**
1379 * Set URL parameters to override or add in the listUrl() method.
1380 *
1381 * @param string[] $urlParameters
1382 */
1383 public function setOverrideUrlParameters(array $urlParameters)
1384 {
1385 $this->overrideUrlParameters = $urlParameters;
1386 }
1387
1388 /**
1389 * Set table display order information
1390 *
1391 * Structure of $orderInformation:
1392 * 'tableName' => [
1393 * 'before' => // comma-separated string list or array of table names
1394 * 'after' => // comma-separated string list or array of table names
1395 * ]
1396 *
1397 * @param array $orderInformation
1398 * @throws \UnexpectedValueException
1399 */
1400 public function setTableDisplayOrder(array $orderInformation)
1401 {
1402 foreach ($orderInformation as $tableName => &$configuration) {
1403 if (isset($configuration['before'])) {
1404 if (is_string($configuration['before'])) {
1405 $configuration['before'] = GeneralUtility::trimExplode(',', $configuration['before'], true);
1406 } elseif (!is_array($configuration['before'])) {
1407 throw new \UnexpectedValueException('The specified "before" order configuration for table "' . $tableName . '" is invalid.', 1436195933);
1408 }
1409 }
1410 if (isset($configuration['after'])) {
1411 if (is_string($configuration['after'])) {
1412 $configuration['after'] = GeneralUtility::trimExplode(',', $configuration['after'], true);
1413 } elseif (!is_array($configuration['after'])) {
1414 throw new \UnexpectedValueException('The specified "after" order configuration for table "' . $tableName . '" is invalid.', 1436195934);
1415 }
1416 }
1417 }
1418 $this->tableDisplayOrder = $orderInformation;
1419 }
1420
1421 /**
1422 * @return array
1423 */
1424 public function getOverridePageIdList(): array
1425 {
1426 return $this->overridePageIdList;
1427 }
1428
1429 /**
1430 * @param int[]|array $overridePageIdList
1431 */
1432 public function setOverridePageIdList(array $overridePageIdList)
1433 {
1434 $this->overridePageIdList = array_map('intval', $overridePageIdList);
1435 }
1436
1437 /**
1438 * Build SQL fragment to limit a query to a list of page IDs based on
1439 * the current search level setting.
1440 *
1441 * @param string $tableName
1442 * @return string
1443 */
1444 protected function getPageIdConstraint(string $tableName): string
1445 {
1446 // Set search levels:
1447 $searchLevels = $this->searchLevels;
1448
1449 // Set search levels to 999 instead of -1 as the following methods
1450 // do not support -1 as valid value for infinite search.
1451 if ($searchLevels === -1) {
1452 $searchLevels = 999;
1453 }
1454
1455 // Default is to search everywhere
1456 $constraint = '1=1';
1457
1458 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1459 ->getConnectionForTable($tableName)
1460 ->getExpressionBuilder();
1461
1462 if ($searchLevels === 0) {
1463 $constraint = $expressionBuilder->eq($tableName . '.pid', (int)$this->id);
1464 } elseif ($searchLevels > 0) {
1465 $allowedPidList = $this->getSearchableWebmounts($this->id, $searchLevels, $this->perms_clause);
1466 $constraint = $expressionBuilder->in($tableName . '.pid', array_map('intval', $allowedPidList));
1467 }
1468
1469 if (!empty($this->getOverridePageIdList())) {
1470 $constraint = $expressionBuilder->in(
1471 $tableName . '.pid',
1472 $this->getOverridePageIdList()
1473 );
1474 }
1475
1476 return (string)$constraint;
1477 }
1478
1479 /**
1480 * @return BackendUserAuthentication
1481 */
1482 protected function getBackendUserAuthentication()
1483 {
1484 return $GLOBALS['BE_USER'];
1485 }
1486 }