43f7aaeb65b5eccfcda0721e03c55e0ecd176cb6
[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\Database\DatabaseConnection;
24 use TYPO3\CMS\Core\Imaging\Icon;
25 use TYPO3\CMS\Core\Imaging\IconFactory;
26 use TYPO3\CMS\Core\Service\DependencyOrderingService;
27 use TYPO3\CMS\Core\Type\Bitmask\Permission;
28 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30 use TYPO3\CMS\Core\Utility\HttpUtility;
31 use TYPO3\CMS\Core\Utility\MathUtility;
32
33 /**
34 * Child class for rendering of Web > List (not the final class)
35 * Shared between Web>List and Web>Page
36 * @see \TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList
37 */
38 class AbstractDatabaseRecordList extends AbstractRecordList
39 {
40 /**
41 * Specify a list of tables which are the only ones allowed to be displayed.
42 *
43 * @var string
44 */
45 public $tableList = '';
46
47 /**
48 * Return URL
49 *
50 * @var string
51 */
52 public $returnUrl = '';
53
54 /**
55 * Thumbnails on records containing files (pictures)
56 *
57 * @var bool
58 */
59 public $thumbs = 0;
60
61 /**
62 * default Max items shown per table in "multi-table mode", may be overridden by tables.php
63 *
64 * @var int
65 */
66 public $itemsLimitPerTable = 20;
67
68 /**
69 * default Max items shown per table in "single-table mode", may be overridden by tables.php
70 *
71 * @var int
72 */
73 public $itemsLimitSingleTable = 100;
74
75 /**
76 * Current script name
77 *
78 * @var string
79 */
80 public $script = 'index.php';
81
82 /**
83 * Indicates if all available fields for a user should be selected or not.
84 *
85 * @var int
86 */
87 public $allFields = 0;
88
89 /**
90 * Whether to show localization view or not
91 *
92 * @var bool
93 */
94 public $localizationView = false;
95
96 /**
97 * If set, csvList is outputted.
98 *
99 * @var bool
100 */
101 public $csvOutput = false;
102
103 /**
104 * Field, to sort list by
105 *
106 * @var string
107 */
108 public $sortField;
109
110 /**
111 * Field, indicating to sort in reverse order.
112 *
113 * @var bool
114 */
115 public $sortRev;
116
117 /**
118 * Containing which fields to display in extended mode
119 *
120 * @var string[]
121 */
122 public $displayFields;
123
124 /**
125 * String, can contain the field name from a table which must have duplicate values marked.
126 *
127 * @var string
128 */
129 public $duplicateField;
130
131 /**
132 * Page id
133 *
134 * @var int
135 */
136 public $id;
137
138 /**
139 * Tablename if single-table mode
140 *
141 * @var string
142 */
143 public $table = '';
144
145 /**
146 * If TRUE, records are listed only if a specific table is selected.
147 *
148 * @var bool
149 */
150 public $listOnlyInSingleTableMode = false;
151
152 /**
153 * Pointer for browsing list
154 *
155 * @var int
156 */
157 public $firstElementNumber = 0;
158
159 /**
160 * Search string
161 *
162 * @var string
163 */
164 public $searchString = '';
165
166 /**
167 * Levels to search down.
168 *
169 * @var int
170 */
171 public $searchLevels = '';
172
173 /**
174 * Number of records to show
175 *
176 * @var int
177 */
178 public $showLimit = 0;
179
180 /**
181 * Query part for either a list of ids "pid IN (1,2,3)" or a single id "pid = 123" from
182 * which to select/search etc. (when search-levels are set high). See start()
183 *
184 * @var string
185 */
186 public $pidSelect = '';
187
188 /**
189 * Page select permissions
190 *
191 * @var string
192 */
193 public $perms_clause = '';
194
195 /**
196 * Some permissions...
197 *
198 * @var int
199 */
200 public $calcPerms = 0;
201
202 /**
203 * Mode for what happens when a user clicks the title of a record.
204 *
205 * @var string
206 */
207 public $clickTitleMode = '';
208
209 /**
210 * Shared module configuration, used by localization features
211 *
212 * @var array
213 */
214 public $modSharedTSconfig = array();
215
216 /**
217 * Loaded with page record with version overlay if any.
218 *
219 * @var string[]
220 */
221 public $pageRecord = array();
222
223 /**
224 * Tables which should not get listed
225 *
226 * @var string
227 */
228 public $hideTables = '';
229
230 /**
231 * Tables which should not list their translations
232 *
233 * @var string
234 */
235 public $hideTranslations = '';
236
237 /**
238 * TSconfig which overwrites TCA-Settings
239 *
240 * @var mixed[][]
241 */
242 public $tableTSconfigOverTCA = array();
243
244 /**
245 * Array of collapsed / uncollapsed tables in multi table view
246 *
247 * @var int[][]
248 */
249 public $tablesCollapsed = array();
250
251 /**
252 * JavaScript code accumulation
253 *
254 * @var string
255 */
256 public $JScode = '';
257
258 /**
259 * HTML output
260 *
261 * @var string
262 */
263 public $HTMLcode = '';
264
265 /**
266 * "LIMIT " in SQL...
267 *
268 * @var int
269 */
270 public $iLimit = 0;
271
272 /**
273 * Counting the elements no matter what...
274 *
275 * @var int
276 */
277 public $eCounter = 0;
278
279 /**
280 * Set to the total number of items for a table when selecting.
281 *
282 * @var string
283 */
284 public $totalItems = '';
285
286 /**
287 * Cache for record path
288 *
289 * @var mixed[]
290 */
291 public $recPath_cache = array();
292
293 /**
294 * Fields to display for the current table
295 *
296 * @var string[]
297 */
298 public $setFields = array();
299
300 /**
301 * Used for tracking next/prev uids
302 *
303 * @var int[][]
304 */
305 public $currentTable = array();
306
307 /**
308 * Used for tracking duplicate values of fields
309 *
310 * @var string[]
311 */
312 public $duplicateStack = array();
313
314 /**
315 * @var array[] Module configuration
316 */
317 public $modTSconfig;
318
319 /**
320 * Override/add urlparameters in listUrl() method
321 * @var string[]
322 */
323 protected $overrideUrlParameters = array();
324
325 /**
326 * Array with before/after setting for tables
327 * Structure:
328 * 'tableName' => [
329 * 'before' => ['A', ...]
330 * 'after' => []
331 * ]
332 * @var array[]
333 */
334 protected $tableDisplayOrder = [];
335
336 /**
337 * Initializes the list generation
338 *
339 * @param int $id Page id for which the list is rendered. Must be >= 0
340 * @param string $table Tablename - if extended mode where only one table is listed at a time.
341 * @param int $pointer Browsing pointer.
342 * @param string $search Search word, if any
343 * @param int $levels Number of levels to search down the page tree
344 * @param int $showLimit Limit of records to be listed.
345 * @return void
346 */
347 public function start($id, $table, $pointer, $search = '', $levels = 0, $showLimit = 0)
348 {
349 $backendUser = $this->getBackendUserAuthentication();
350 $db = $this->getDatabaseConnection();
351 // Setting internal variables:
352 // sets the parent id
353 $this->id = (int)$id;
354 if ($GLOBALS['TCA'][$table]) {
355 // Setting single table mode, if table exists:
356 $this->table = $table;
357 }
358 $this->firstElementNumber = $pointer;
359 $this->searchString = trim($search);
360 $this->searchLevels = (int)$levels;
361 $this->showLimit = MathUtility::forceIntegerInRange($showLimit, 0, 10000);
362 // Setting GPvars:
363 $this->csvOutput = (bool)GeneralUtility::_GP('csv');
364 $this->sortField = GeneralUtility::_GP('sortField');
365 $this->sortRev = GeneralUtility::_GP('sortRev');
366 $this->displayFields = GeneralUtility::_GP('displayFields');
367 $this->duplicateField = GeneralUtility::_GP('duplicateField');
368 if (GeneralUtility::_GP('justLocalized')) {
369 $this->localizationRedirect(GeneralUtility::_GP('justLocalized'));
370 }
371 // Init dynamic vars:
372 $this->counter = 0;
373 $this->JScode = '';
374 $this->HTMLcode = '';
375 // Limits
376 if (isset($this->modTSconfig['properties']['itemsLimitPerTable'])) {
377 $this->itemsLimitPerTable = MathUtility::forceIntegerInRange((int)$this->modTSconfig['properties']['itemsLimitPerTable'], 1, 10000);
378 }
379 if (isset($this->modTSconfig['properties']['itemsLimitSingleTable'])) {
380 $this->itemsLimitSingleTable = MathUtility::forceIntegerInRange((int)$this->modTSconfig['properties']['itemsLimitSingleTable'], 1, 10000);
381 }
382 // Set search levels:
383 $searchLevels = $this->searchLevels;
384 $this->perms_clause = $backendUser->getPagePermsClause(1);
385 // This will hide records from display - it has nothing to do with user rights!!
386 if ($pidList = $backendUser->getTSConfigVal('options.hideRecords.pages')) {
387 if ($pidList = $db->cleanIntList($pidList)) {
388 $this->perms_clause .= ' AND pages.uid NOT IN (' . $pidList . ')';
389 }
390 }
391 // Get configuration of collapsed tables from user uc and merge with sanitized GP vars
392 $this->tablesCollapsed = is_array($backendUser->uc['moduleData']['list']) ? $backendUser->uc['moduleData']['list'] : array();
393 $collapseOverride = GeneralUtility::_GP('collapse');
394 if (is_array($collapseOverride)) {
395 foreach ($collapseOverride as $collapseTable => $collapseValue) {
396 if (is_array($GLOBALS['TCA'][$collapseTable]) && ($collapseValue == 0 || $collapseValue == 1)) {
397 $this->tablesCollapsed[$collapseTable] = $collapseValue;
398 }
399 }
400 // Save modified user uc
401 $backendUser->uc['moduleData']['list'] = $this->tablesCollapsed;
402 $backendUser->writeUC($backendUser->uc);
403 $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
404 if ($returnUrl !== '') {
405 HttpUtility::redirect($returnUrl);
406 }
407 }
408 if ($searchLevels > 0) {
409 $allowedMounts = $this->getSearchableWebmounts($this->id, $searchLevels, $this->perms_clause);
410 $pidList = implode(',', $db->cleanIntArray($allowedMounts));
411 $this->pidSelect = 'pid IN (' . $pidList . ')';
412 } elseif ($searchLevels < 0) {
413 // Search everywhere
414 $this->pidSelect = '1=1';
415 } else {
416 $this->pidSelect = 'pid=' . (int)$id;
417 }
418 // Initialize languages:
419 if ($this->localizationView) {
420 $this->initializeLanguages();
421 }
422 }
423
424 /**
425 * Traverses the table(s) to be listed and renders the output code for each:
426 * The HTML is accumulated in $this->HTMLcode
427 * Finishes off with a stopper-gif
428 *
429 * @return void
430 */
431 public function generateList()
432 {
433 // Set page record in header
434 $this->pageRecord = BackendUtility::getRecordWSOL('pages', $this->id);
435 $hideTablesArray = GeneralUtility::trimExplode(',', $this->hideTables);
436
437 $backendUser = $this->getBackendUserAuthentication();
438
439 // pre-process tables and add sorting instructions
440 $tableNames = array_flip(array_keys($GLOBALS['TCA']));
441 foreach ($tableNames as $tableName => &$config) {
442 $hideTable = false;
443
444 // Checking if the table should be rendered:
445 // Checks that we see only permitted/requested tables:
446 if ($this->table && $tableName !== $this->table
447 || $this->tableList && !GeneralUtility::inList($this->tableList, $tableName)
448 || !$backendUser->check('tables_select', $tableName)
449 ) {
450 $hideTable = true;
451 }
452
453 if (!$hideTable) {
454 // Don't show table if hidden by TCA ctrl section
455 // Don't show table if hidden by pageTSconfig mod.web_list.hideTables
456 $hideTable = $hideTable || !empty($GLOBALS['TCA'][$tableName]['ctrl']['hideTable']) || in_array($tableName, $hideTablesArray, true) || in_array('*', $hideTablesArray, true);
457 // Override previous selection if table is enabled or hidden by TSconfig TCA override mod.web_list.table
458 if (isset($this->tableTSconfigOverTCA[$tableName . '.']['hideTable'])) {
459 $hideTable = (bool)$this->tableTSconfigOverTCA[$tableName . '.']['hideTable'];
460 }
461 }
462 if ($hideTable) {
463 unset($tableNames[$tableName]);
464 } else {
465 if (isset($this->tableDisplayOrder[$tableName])) {
466 // Copy display order information
467 $tableNames[$tableName] = $this->tableDisplayOrder[$tableName];
468 } else {
469 $tableNames[$tableName] = [];
470 }
471 }
472 }
473 unset($config);
474
475 $orderedTableNames = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($tableNames);
476
477 $db = $this->getDatabaseConnection();
478 foreach ($orderedTableNames as $tableName => $_) {
479 // check if we are in single- or multi-table mode
480 if ($this->table) {
481 $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems']) ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems'] : $this->itemsLimitSingleTable;
482 } else {
483 // if there are no records in table continue current foreach
484 $firstRow = $db->exec_SELECTgetSingleRow(
485 'uid',
486 $tableName,
487 $this->pidSelect . BackendUtility::deleteClause($tableName) . BackendUtility::versioningPlaceholderClause($tableName)
488 );
489 if ($firstRow === false) {
490 continue;
491 }
492 $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxDBListItems']) ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxDBListItems'] : $this->itemsLimitPerTable;
493 }
494 if ($this->showLimit) {
495 $this->iLimit = $this->showLimit;
496 }
497 // Setting fields to select:
498 if ($this->allFields) {
499 $fields = $this->makeFieldList($tableName);
500 $fields[] = 'tstamp';
501 $fields[] = 'crdate';
502 $fields[] = '_PATH_';
503 $fields[] = '_CONTROL_';
504 if (is_array($this->setFields[$tableName])) {
505 $fields = array_intersect($fields, $this->setFields[$tableName]);
506 } else {
507 $fields = array();
508 }
509 } else {
510 $fields = array();
511 }
512 // Find ID to use (might be different for "versioning_followPages" tables)
513 if ($this->searchLevels === 0) {
514 $this->pidSelect = 'pid=' . (int)$this->id;
515 }
516 // Finally, render the list:
517 $this->HTMLcode .= $this->getTable($tableName, $this->id, implode(',', $fields));
518 }
519 }
520
521 /**
522 * To be implemented in extending classes.
523 *
524 * @param string $tableName
525 * @param int $id
526 * @param string $fields List of fields to show in the listing. Pseudo fields will be added including the record header.
527 * @return string HTML code
528 */
529 public function getTable($tableName, $id, $fields = '')
530 {
531 return '';
532 }
533
534 /**
535 * Creates the search box
536 *
537 * @param bool $formFields If TRUE, the search box is wrapped in its own form-tags
538 * @return string HTML for the search box
539 */
540 public function getSearchBox($formFields = true)
541 {
542 /** @var $iconFactory IconFactory */
543 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
544 $lang = $this->getLanguageService();
545 // Setting form-elements, if applicable:
546 $formElements = array('', '');
547 if ($formFields) {
548 $formElements = array('<form action="' . htmlspecialchars($this->listURL('', '-1', 'firstElementNumber,search_field')) . '" method="post">', '</form>');
549 }
550 // Make level selector:
551 $opt = array();
552
553 // "New" generation of search levels ... based on TS config
554 $config = BackendUtility::getPagesTSconfig($this->id);
555 $searchLevelsFromTSconfig = $config['mod.']['web_list.']['searchLevel.']['items.'];
556 $searchLevelItems = array();
557
558 // get translated labels for search levels from pagets
559 foreach ($searchLevelsFromTSconfig as $keySearchLevel => $labelConfigured) {
560 $label = $lang->sL('LLL:' . $labelConfigured, false);
561 if ($label === '') {
562 $label = $labelConfigured;
563 }
564 $searchLevelItems[$keySearchLevel] = $label;
565 }
566
567 foreach ($searchLevelItems as $kv => $label) {
568 $opt[] = '<option value="' . $kv . '"' . ($kv === $this->searchLevels ? ' selected="selected"' : '') . '>' . htmlspecialchars($label) . '</option>';
569 }
570 $lMenu = '<select class="form-control" name="search_levels" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.title.search_levels', true) . '" id="search_levels">' . implode('', $opt) . '</select>';
571 // Table with the search box:
572 $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') . ';">
573 ' . $formElements[0] . '
574 <div id="typo3-dblist-search">
575 <div class="panel panel-default">
576 <div class="panel-body">
577 <div class="form-inline form-inline-spaced">
578 <div class="form-group">
579 <input class="form-control" type="search" placeholder="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.enterSearchString', true) . '" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.title.searchString', true) . '" name="search_field" id="search_field" value="' . htmlspecialchars($this->searchString) . '" />
580 </div>
581 <div class="form-group">
582 <label for="search_levels">' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.label.search_levels', true) . ': </label>
583 ' . $lMenu . '
584 </div>
585 <div class="form-group">
586 <label for="showLimit">' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.label.limit', true) . ': </label>
587 <input class="form-control" type="number" min="0" max="10000" placeholder="10" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.title.limit', true) . '" name="showLimit" id="showLimit" value="' . htmlspecialchars(($this->showLimit ? $this->showLimit : '')) . '" />
588 </div>
589 <div class="form-group">
590 <button type="submit" class="btn btn-default" name="search" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.title.search', true) . '">
591 ' . $iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render() . ' ' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.search', true) . '
592 </button>
593 </div>
594 </div>
595 </div>
596 </div>
597 </div>
598 ' . $formElements[1] . '</div>';
599 return $content;
600 }
601
602 /******************************
603 *
604 * Various helper functions
605 *
606 ******************************/
607 /**
608 * Setting the field names to display in extended list.
609 * Sets the internal variable $this->setFields
610 *
611 * @return void
612 */
613 public function setDispFields()
614 {
615 $backendUser = $this->getBackendUserAuthentication();
616 // Getting from session:
617 $dispFields = $backendUser->getModuleData('list/displayFields');
618 // If fields has been inputted, then set those as the value and push it to session variable:
619 if (is_array($this->displayFields)) {
620 reset($this->displayFields);
621 $tKey = key($this->displayFields);
622 $dispFields[$tKey] = $this->displayFields[$tKey];
623 $backendUser->pushModuleData('list/displayFields', $dispFields);
624 }
625 // Setting result:
626 $this->setFields = $dispFields;
627 }
628
629 /**
630 * Create thumbnail code for record/field
631 *
632 * @param mixed[] $row Record array
633 * @param string $table Table (record is from)
634 * @param string $field Field name for which thumbnail are to be rendered.
635 * @return string HTML for thumbnails, if any.
636 */
637 public function thumbCode($row, $table, $field)
638 {
639 return BackendUtility::thumbCode($row, $table, $field);
640 }
641
642 /**
643 * Returns the SQL-query array to select the records from a table $table with pid = $id
644 *
645 * @param string $table Table name
646 * @param int $id Page id (NOT USED! $this->pidSelect is used instead)
647 * @param string $addWhere Additional part for where clause
648 * @param string $fieldList Field list to select, * for all (for "SELECT [fieldlist] FROM ...")
649 * @return string[] Returns query array
650 */
651 public function makeQueryArray($table, $id, $addWhere = '', $fieldList = '*')
652 {
653 $hookObjectsArr = array();
654 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list.inc']['makeQueryArray'])) {
655 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list.inc']['makeQueryArray'] as $classRef) {
656 $hookObjectsArr[] = GeneralUtility::getUserObj($classRef);
657 }
658 }
659 // Set ORDER BY:
660 $orderBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ? 'ORDER BY ' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : $GLOBALS['TCA'][$table]['ctrl']['default_sortby'];
661 if ($this->sortField) {
662 if (in_array($this->sortField, $this->makeFieldList($table, 1))) {
663 $orderBy = 'ORDER BY ' . $this->sortField;
664 if ($this->sortRev) {
665 $orderBy .= ' DESC';
666 }
667 }
668 }
669 // Set LIMIT:
670 $limit = $this->iLimit ? ($this->firstElementNumber ? $this->firstElementNumber . ',' : '') . ($this->iLimit + 1) : '';
671 // Filtering on displayable pages (permissions):
672 $pC = $table == 'pages' && $this->perms_clause ? ' AND ' . $this->perms_clause : '';
673 // Adding search constraints:
674 $search = $this->makeSearchString($table, $id);
675 // Compiling query array:
676 $queryParts = array(
677 'SELECT' => $fieldList,
678 'FROM' => $table,
679 'WHERE' => $this->pidSelect . ' ' . $pC . BackendUtility::deleteClause($table) . BackendUtility::versioningPlaceholderClause($table) . ' ' . $addWhere . ' ' . $search,
680 'GROUPBY' => '',
681 'ORDERBY' => $this->getDatabaseConnection()->stripOrderBy($orderBy),
682 'LIMIT' => $limit
683 );
684 // Filter out records that are translated, if TSconfig mod.web_list.hideTranslations is set
685 if ((in_array($table, GeneralUtility::trimExplode(',', $this->hideTranslations)) || $this->hideTranslations === '*') && !empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) && $table !== 'pages_language_overlay') {
686 $queryParts['WHERE'] .= ' AND ' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] . '=0 ';
687 }
688 // Apply hook as requested in http://forge.typo3.org/issues/16634
689 foreach ($hookObjectsArr as $hookObj) {
690 if (method_exists($hookObj, 'makeQueryArray_post')) {
691 $_params = array(
692 'orderBy' => $orderBy,
693 'limit' => $limit,
694 'pC' => $pC,
695 'search' => $search
696 );
697 $hookObj->makeQueryArray_post($queryParts, $this, $table, $id, $addWhere, $fieldList, $_params);
698 }
699 }
700 // Return query:
701 return $queryParts;
702 }
703
704 /**
705 * Based on input query array (query for selecting count(*) from a table) it will select the number of records and set the value in $this->totalItems
706 *
707 * @param string[] $queryParts Query array
708 * @return void
709 * @see makeQueryArray()
710 */
711 public function setTotalItems($queryParts)
712 {
713 $this->totalItems = $this->getDatabaseConnection()->exec_SELECTcountRows('*', $queryParts['FROM'], $queryParts['WHERE']);
714 }
715
716 /**
717 * Creates part of query for searching after a word ($this->searchString)
718 * fields in input table.
719 *
720 * @param string $table Table, in which the fields are being searched.
721 * @param int $currentPid Page id for the possible search limit. -1 only if called from an old XCLASS.
722 * @return string Returns part of WHERE-clause for searching, if applicable.
723 */
724 public function makeSearchString($table, $currentPid = -1)
725 {
726 $result = '';
727 $currentPid = (int)$currentPid;
728 $tablePidField = $table === 'pages' ? 'uid' : 'pid';
729 // Make query, only if table is valid and a search string is actually defined:
730 if ($this->searchString) {
731 $result = ' AND 0=1';
732 $searchableFields = $this->getSearchFields($table);
733 if (!empty($searchableFields)) {
734 if (MathUtility::canBeInterpretedAsInteger($this->searchString)) {
735 $whereParts = array(
736 'uid=' . $this->searchString
737 );
738 foreach ($searchableFields as $fieldName) {
739 if (isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
740 $fieldConfig = &$GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
741 $condition = $fieldName . '=' . $this->searchString;
742 if ($fieldConfig['type'] == 'input' && $fieldConfig['eval'] && GeneralUtility::inList($fieldConfig['eval'], 'int')) {
743 if (is_array($fieldConfig['search']) && in_array('pidonly', $fieldConfig['search']) && $currentPid > 0) {
744 $condition = '(' . $condition . ' AND ' . $tablePidField . '=' . $currentPid . ')';
745 }
746 $whereParts[] = $condition;
747 } elseif ($fieldConfig['type'] == 'text' ||
748 $fieldConfig['type'] == 'flex' ||
749 ($fieldConfig['type'] == 'input' && (!$fieldConfig['eval'] || !preg_match('/date|time|int/', $fieldConfig['eval'])))) {
750 $condition = $fieldName . ' LIKE \'%' . $this->searchString . '%\'';
751 $whereParts[] = $condition;
752 }
753 }
754 }
755 } else {
756 $whereParts = array();
757 $db = $this->getDatabaseConnection();
758 $like = '\'%' . $db->quoteStr($db->escapeStrForLike($this->searchString, $table), $table) . '%\'';
759 foreach ($searchableFields as $fieldName) {
760 if (isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
761 $fieldConfig = &$GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
762 $format = 'LOWER(%s) LIKE LOWER(%s)';
763 if (is_array($fieldConfig['search'])) {
764 if (in_array('case', $fieldConfig['search'])) {
765 $format = '%s LIKE %s';
766 }
767 if (in_array('pidonly', $fieldConfig['search']) && $currentPid > 0) {
768 $format = '(' . $format . ' AND ' . $tablePidField . '=' . $currentPid . ')';
769 }
770 if ($fieldConfig['search']['andWhere']) {
771 $format = '((' . $fieldConfig['search']['andWhere'] . ') AND (' . $format . '))';
772 }
773 }
774 if ($fieldConfig['type'] == 'text' || $fieldConfig['type'] == 'flex' || $fieldConfig['type'] == 'input' && (!$fieldConfig['eval'] || !preg_match('/date|time|int/', $fieldConfig['eval']))) {
775 $whereParts[] = sprintf($format, $fieldName, $like);
776 }
777 }
778 }
779 }
780 // If search-fields were defined (and there always are) we create the query:
781 if (!empty($whereParts)) {
782 $result = ' AND (' . implode(' OR ', $whereParts) . ')';
783 }
784 }
785 }
786 return $result;
787 }
788
789 /**
790 * Fetches a list of fields to use in the Backend search for the given table.
791 *
792 * @param string $tableName
793 * @return string[]
794 */
795 protected function getSearchFields($tableName)
796 {
797 $fieldArray = array();
798 $fieldListWasSet = false;
799 // Get fields from ctrl section of TCA first
800 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['searchFields'])) {
801 $fieldArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$tableName]['ctrl']['searchFields'], true);
802 $fieldListWasSet = true;
803 }
804 // Call hook to add or change the list
805 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'])) {
806 $hookParameters = array(
807 'tableHasSearchConfiguration' => $fieldListWasSet,
808 'tableName' => $tableName,
809 'searchFields' => &$fieldArray,
810 'searchString' => $this->searchString
811 );
812 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'] as $hookFunction) {
813 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
814 }
815 }
816 return $fieldArray;
817 }
818
819 /**
820 * Returns the title (based on $code) of a table ($table) with the proper link around. For headers over tables.
821 * The link will cause the display of all extended mode or not for the table.
822 *
823 * @param string $table Table name
824 * @param string $code Table label
825 * @return string The linked table label
826 */
827 public function linkWrapTable($table, $code)
828 {
829 if ($this->table !== $table) {
830 return '<a href="' . htmlspecialchars($this->listURL('', $table, 'firstElementNumber')) . '">' . $code . '</a>';
831 }
832 return '<a href="' . htmlspecialchars($this->listURL('', '', 'sortField,sortRev,table,firstElementNumber')) . '">' . $code . '</a>';
833 }
834
835 /**
836 * 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...)
837 *
838 * @param string $table Table name
839 * @param int $uid Item uid
840 * @param string $code Item title (not htmlspecialchars()'ed yet)
841 * @param mixed[] $row Item row
842 * @return string The item title. Ready for HTML output (is htmlspecialchars()'ed)
843 */
844 public function linkWrapItems($table, $uid, $code, $row)
845 {
846 $lang = $this->getLanguageService();
847 $origCode = $code;
848 // If the title is blank, make a "no title" label:
849 if ((string)$code === '') {
850 $code = '<i>[' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.no_title', 1) . ']</i> - ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs(
851 BackendUtility::getRecordTitle($table, $row),
852 $this->getBackendUserAuthentication()->uc['titleLen']
853 ));
854 } else {
855 $code = htmlspecialchars(GeneralUtility::fixed_lgd_cs($code, $this->fixedL), ENT_QUOTES, 'UTF-8', false);
856 if ($code != htmlspecialchars($origCode)) {
857 $code = '<span title="' . htmlspecialchars($origCode, ENT_QUOTES, 'UTF-8', false) . '">' . $code . '</span>';
858 }
859 }
860 switch ((string)$this->clickTitleMode) {
861 case 'edit':
862 // If the listed table is 'pages' we have to request the permission settings for each page:
863 if ($table == 'pages') {
864 $localCalcPerms = $this->getBackendUserAuthentication()->calcPerms(BackendUtility::getRecord('pages', $row['uid']));
865 $permsEdit = $localCalcPerms & Permission::PAGE_EDIT;
866 } else {
867 $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT;
868 }
869 // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id)
870 if ($permsEdit) {
871 $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit';
872 $code = '<a href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1)) . '" title="' . htmlspecialchars($lang->getLL('edit')) . '">' . $code . '</a>';
873 }
874 break;
875 case 'show':
876 // "Show" link (only pages and tt_content elements)
877 if ($table == 'pages' || $table == 'tt_content') {
878 $code = '<a href="#" onclick="' . htmlspecialchars(
879 BackendUtility::viewOnClick(($table == 'tt_content' ? $this->id . '#' . $row['uid'] : $row['uid']))
880 ) . '" title="' . $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.showPage', true) . '">' . $code . '</a>';
881 }
882 break;
883 case 'info':
884 // "Info": (All records)
885 $code = '<a href="#" onclick="' . htmlspecialchars(('top.launchView(\'' . $table . '\', \'' . $row['uid'] . '\'); return false;')) . '" title="' . htmlspecialchars($lang->getLL('showInfo')) . '">' . $code . '</a>';
886 break;
887 default:
888 // Output the label now:
889 if ($table == 'pages') {
890 $code = '<a href="' . htmlspecialchars($this->listURL($uid, '', 'firstElementNumber')) . '" onclick="setHighlight(' . $uid . ')">' . $code . '</a>';
891 } else {
892 $code = $this->linkUrlMail($code, $origCode);
893 }
894 }
895 return $code;
896 }
897
898 /**
899 * Wrapping input code in link to URL or email if $testString is either.
900 *
901 * @param string $code code to wrap
902 * @param string $testString String which is tested for being a URL or email and which will be used for the link if so.
903 * @return string Link-Wrapped $code value, if $testString was URL or email.
904 */
905 public function linkUrlMail($code, $testString)
906 {
907 // Check for URL:
908 $scheme = parse_url($testString, PHP_URL_SCHEME);
909 if ($scheme === 'http' || $scheme === 'https' || $scheme === 'ftp') {
910 return '<a href="' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
911 }
912 // Check for email:
913 if (GeneralUtility::validEmail($testString)) {
914 return '<a href="mailto:' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
915 }
916 // Return if nothing else...
917 return $code;
918 }
919
920 /**
921 * Creates the URL to this script, including all relevant GPvars
922 * Fixed GPvars are id, table, imagemode, returnUrl, search_field, search_levels and showLimit
923 * The GPvars "sortField" and "sortRev" are also included UNLESS they are found in the $exclList variable.
924 *
925 * @param string $altId Alternative id value. Enter blank string for the current id ($this->id)
926 * @param string $table Table name to display. Enter "-1" for the current table.
927 * @param string $exclList Comma separated list of fields NOT to include ("sortField", "sortRev" or "firstElementNumber")
928 * @return string URL
929 */
930 public function listURL($altId = '', $table = '-1', $exclList = '')
931 {
932 $urlParameters = array();
933 if ((string)$altId !== '') {
934 $urlParameters['id'] = $altId;
935 } else {
936 $urlParameters['id'] = $this->id;
937 }
938 if ($table === '-1') {
939 $urlParameters['table'] = $this->table;
940 } else {
941 $urlParameters['table'] = $table;
942 }
943 if ($this->thumbs) {
944 $urlParameters['imagemode'] = $this->thumbs;
945 }
946 if ($this->returnUrl) {
947 $urlParameters['returnUrl'] = $this->returnUrl;
948 }
949 if ((!$exclList || !GeneralUtility::inList($exclList, 'search_field')) && $this->searchString) {
950 $urlParameters['search_field'] = $this->searchString;
951 }
952 if ($this->searchLevels) {
953 $urlParameters['search_levels'] = $this->searchLevels;
954 }
955 if ($this->showLimit) {
956 $urlParameters['showLimit'] = $this->showLimit;
957 }
958 if ((!$exclList || !GeneralUtility::inList($exclList, 'firstElementNumber')) && $this->firstElementNumber) {
959 $urlParameters['pointer'] = $this->firstElementNumber;
960 }
961 if ((!$exclList || !GeneralUtility::inList($exclList, 'sortField')) && $this->sortField) {
962 $urlParameters['sortField'] = $this->sortField;
963 }
964 if ((!$exclList || !GeneralUtility::inList($exclList, 'sortRev')) && $this->sortRev) {
965 $urlParameters['sortRev'] = $this->sortRev;
966 }
967
968 $urlParameters = array_merge_recursive($urlParameters, $this->overrideUrlParameters);
969
970 if ($routePath = GeneralUtility::_GP('route')) {
971 $router = GeneralUtility::makeInstance(Router::class);
972 $route = $router->match($routePath);
973 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
974 $url = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'), $urlParameters);
975 } elseif ($moduleName = GeneralUtility::_GP('M')) {
976 $url = BackendUtility::getModuleUrl($moduleName, $urlParameters);
977 } else {
978 $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
979 }
980 return $url;
981 }
982
983 /**
984 * Returns "requestUri" - which is basically listURL
985 *
986 * @return string Content of ->listURL()
987 */
988 public function requestUri()
989 {
990 return $this->listURL();
991 }
992
993 /**
994 * Makes the list of fields to select for a table
995 *
996 * @param string $table Table name
997 * @param bool $dontCheckUser If set, users access to the field (non-exclude-fields) is NOT checked.
998 * @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)
999 * @return string[] Array, where values are fieldnames to include in query
1000 */
1001 public function makeFieldList($table, $dontCheckUser = false, $addDateFields = false)
1002 {
1003 $backendUser = $this->getBackendUserAuthentication();
1004 // Init fieldlist array:
1005 $fieldListArr = array();
1006 // Check table:
1007 if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['columns']) && is_array($GLOBALS['TCA'][$table]['columns'])) {
1008 if (isset($GLOBALS['TCA'][$table]['columns']) && is_array($GLOBALS['TCA'][$table]['columns'])) {
1009 // Traverse configured columns and add them to field array, if available for user.
1010 foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fieldValue) {
1011 if ($dontCheckUser || (!$fieldValue['exclude'] || $backendUser->check('non_exclude_fields', $table . ':' . $fN)) && $fieldValue['config']['type'] != 'passthrough') {
1012 $fieldListArr[] = $fN;
1013 }
1014 }
1015
1016 $fieldListArr[] = 'uid';
1017 $fieldListArr[] = 'pid';
1018
1019 // Add date fields
1020 if ($dontCheckUser || $backendUser->isAdmin() || $addDateFields) {
1021 if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1022 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['tstamp'];
1023 }
1024 if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1025 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['crdate'];
1026 }
1027 }
1028 // Add more special fields:
1029 if ($dontCheckUser || $backendUser->isAdmin()) {
1030 if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1031 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['cruser_id'];
1032 }
1033 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
1034 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
1035 }
1036 if (ExtensionManagementUtility::isLoaded('version') && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1037 $fieldListArr[] = 't3ver_id';
1038 $fieldListArr[] = 't3ver_state';
1039 $fieldListArr[] = 't3ver_wsid';
1040 }
1041 }
1042 } else {
1043 GeneralUtility::sysLog(sprintf('$TCA is broken for the table "%s": no required "columns" entry in $TCA.', $table), 'core', GeneralUtility::SYSLOG_SEVERITY_ERROR);
1044 }
1045 }
1046 return $fieldListArr;
1047 }
1048
1049 /**
1050 * Get all allowed mount pages to be searched in.
1051 *
1052 * @param int $id Page id
1053 * @param int $depth Depth to go down
1054 * @param string $perms_clause select clause
1055 * @return int[]
1056 */
1057 protected function getSearchableWebmounts($id, $depth, $perms_clause)
1058 {
1059 $backendUser = $this->getBackendUserAuthentication();
1060 /** @var PageTreeView $tree */
1061 $tree = GeneralUtility::makeInstance(PageTreeView::class);
1062 $tree->init('AND ' . $perms_clause);
1063 $tree->makeHTML = 0;
1064 $tree->fieldArray = array('uid', 'php_tree_stop');
1065 $idList = array();
1066
1067 $allowedMounts = !$backendUser->isAdmin() && $id === 0
1068 ? $backendUser->returnWebmounts()
1069 : array($id);
1070
1071 foreach ($allowedMounts as $allowedMount) {
1072 $idList[] = $allowedMount;
1073 if ($depth) {
1074 $tree->getTree($allowedMount, $depth, '');
1075 }
1076 $idList = array_merge($idList, $tree->ids);
1077 }
1078
1079 return $idList;
1080 }
1081
1082 /**
1083 * Redirects to FormEngine if a record is just localized.
1084 *
1085 * @param string $justLocalized String with table, orig uid and language separated by ":
1086 * @return void
1087 */
1088 public function localizationRedirect($justLocalized)
1089 {
1090 list($table, $orig_uid, $language) = explode(':', $justLocalized);
1091 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
1092 $localizedRecord = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('uid', $table, $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '=' . (int)$language . ' AND ' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] . '=' . (int)$orig_uid . BackendUtility::deleteClause($table) . BackendUtility::versioningPlaceholderClause($table));
1093 if (is_array($localizedRecord)) {
1094 // Create parameters and finally run the classic page module for creating a new page translation
1095 $url = $this->listURL();
1096 $editUserAccountUrl = BackendUtility::getModuleUrl(
1097 'record_edit',
1098 array(
1099 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
1100 'returnUrl' => $url
1101 )
1102 );
1103 HttpUtility::redirect($editUserAccountUrl);
1104 }
1105 }
1106 }
1107
1108 /**
1109 * Set URL parameters to override or add in the listUrl() method.
1110 *
1111 * @param string[] $urlParameters
1112 * @return void
1113 */
1114 public function setOverrideUrlParameters(array $urlParameters)
1115 {
1116 $this->overrideUrlParameters = $urlParameters;
1117 }
1118
1119 /**
1120 * Set table display order information
1121 *
1122 * Structure of $orderInformation:
1123 * 'tableName' => [
1124 * 'before' => // comma-separated string list or array of table names
1125 * 'after' => // comma-separated string list or array of table names
1126 * ]
1127 *
1128 * @param array $orderInformation
1129 * @throws \UnexpectedValueException
1130 */
1131 public function setTableDisplayOrder(array $orderInformation)
1132 {
1133 foreach ($orderInformation as $tableName => &$configuration) {
1134 if (isset($configuration['before'])) {
1135 if (is_string($configuration['before'])) {
1136 $configuration['before'] = GeneralUtility::trimExplode(',', $configuration['before'], true);
1137 } elseif (!is_array($configuration['before'])) {
1138 throw new \UnexpectedValueException('The specified "before" order configuration for table "' . $tableName . '" is invalid.', 1436195933);
1139 }
1140 }
1141 if (isset($configuration['after'])) {
1142 if (is_string($configuration['after'])) {
1143 $configuration['after'] = GeneralUtility::trimExplode(',', $configuration['after'], true);
1144 } elseif (!is_array($configuration['after'])) {
1145 throw new \UnexpectedValueException('The specified "after" order configuration for table "' . $tableName . '" is invalid.', 1436195934);
1146 }
1147 }
1148 }
1149 $this->tableDisplayOrder = $orderInformation;
1150 }
1151
1152 /**
1153 * @return BackendUserAuthentication
1154 */
1155 protected function getBackendUserAuthentication()
1156 {
1157 return $GLOBALS['BE_USER'];
1158 }
1159
1160 /**
1161 * @return DatabaseConnection
1162 */
1163 protected function getDatabaseConnection()
1164 {
1165 return $GLOBALS['TYPO3_DB'];
1166 }
1167 }