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