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