Page module * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API. */ class PageLayoutView implements LoggerAwareInterface { use LoggerAwareTrait; /** * If TRUE, users/groups are shown in the page info box. * * @var bool */ public $pI_showUser = false; /** * The number of successive records to edit when showing content elements. * * @var int */ public $nextThree = 3; /** * If TRUE, disables the edit-column icon for tt_content elements * * @var bool */ public $pages_noEditColumns = false; /** * If TRUE, new-wizards are linked to rather than the regular new-element list. * * @var bool */ public $option_newWizard = true; /** * If set to "1", will link a big button to content element wizard. * * @var int */ public $ext_function = 0; /** * If TRUE, elements will have edit icons (probably this is whether the user has permission to edit the page content). Set externally. * * @var bool */ public $doEdit = true; /** * Age prefixes for displaying times. May be set externally to localized values. * * @var string */ public $agePrefixes = ' min| hrs| days| yrs| min| hour| day| year'; /** * Array of tables to be listed by the Web > Page module in addition to the default tables. * * @var array */ public $externalTables = []; /** * "Pseudo" Description -table name * * @var string */ public $descrTable; /** * If set TRUE, the language mode of tt_content elements will be rendered with hard binding between * default language content elements and their translations! * * @var bool */ public $defLangBinding = false; /** * External, static: Configuration of tt_content element display: * * @var array */ public $tt_contentConfig = [ // Boolean: Display info-marks or not 'showInfo' => 1, // Boolean: Display up/down arrows and edit icons for tt_content records 'showCommands' => 1, 'languageCols' => 0, 'languageMode' => 0, 'languageColsPointer' => 0, 'showHidden' => 1, // Displays hidden records as well 'sys_language_uid' => 0, // Which language 'cols' => '1,0,2,3', 'activeCols' => '1,0,2,3' // Which columns can be accessed by current BE user ]; /** * Contains icon/title of pages which are listed in the tables menu (see getTableMenu() function ) * * @var array */ public $activeTables = []; /** * @var array */ public $tt_contentData = [ 'nextThree' => [], 'prev' => [], 'next' => [] ]; /** * Used to store labels for CTypes for tt_content elements * * @var array */ public $CType_labels = []; /** * Used to store labels for the various fields in tt_content elements * * @var array */ public $itemLabels = []; /** * Indicates if all available fields for a user should be selected or not. * * @var int */ public $allFields = 0; /** * Number of records to show * * @var int */ public $showLimit = 0; /** * Shared module configuration, used by localization features * * @var array */ public $modSharedTSconfig = []; /** * Tables which should not get listed * * @var string */ public $hideTables = ''; /** * Containing which fields to display in extended mode * * @var string[] */ public $displayFields; /** * Tables which should not list their translations * * @var string */ public $hideTranslations = ''; /** * If set, csvList is outputted. * * @var bool */ public $csvOutput = false; /** * Cache for record path * * @var mixed[] */ public $recPath_cache = []; /** * Field, to sort list by * * @var string */ public $sortField; /** * default Max items shown per table in "multi-table mode", may be overridden by tables.php * * @var int */ public $itemsLimitPerTable = 20; /** * Page select permissions * * @var string */ public $perms_clause = ''; /** * Page id * * @var int */ public $id; /** * Return URL * * @var string */ public $returnUrl = ''; /** * Tablename if single-table mode * * @var string */ public $table = ''; /** * Some permissions... * * @var int */ public $calcPerms = 0; /** * Mode for what happens when a user clicks the title of a record. * * @var string */ public $clickTitleMode = ''; /** * Levels to search down. * * @var int */ public $searchLevels = ''; /** * "LIMIT " in SQL... * * @var int */ public $iLimit = 0; /** * Set to the total number of items for a table when selecting. * * @var string */ public $totalItems = ''; /** * TSconfig which overwrites TCA-Settings * * @var mixed[][] */ public $tableTSconfigOverTCA = []; /** * Loaded with page record with version overlay if any. * * @var string[] */ public $pageRecord = []; /** * Used for tracking duplicate values of fields * * @var string[] */ public $duplicateStack = []; /** * Fields to display for the current table * * @var string[] */ public $setFields = []; /** * Current script name * * @var string */ public $script = 'index.php'; /** * If TRUE, records are listed only if a specific table is selected. * * @var bool */ public $listOnlyInSingleTableMode = false; /** * JavaScript code accumulation * * @var string */ public $JScode = ''; /** * Pointer for browsing list * * @var int */ public $firstElementNumber = 0; /** * Counting the elements no matter what... * * @var int */ public $eCounter = 0; /** * Search string * * @var string */ public $searchString = ''; /** * default Max items shown per table in "single-table mode", may be overridden by tables.php * * @var int */ public $itemsLimitSingleTable = 100; /** * Field, indicating to sort in reverse order. * * @var bool */ public $sortRev; /** * String, can contain the field name from a table which must have duplicate values marked. * * @var string */ public $duplicateField; /** * Specify a list of tables which are the only ones allowed to be displayed. * * @var string */ public $tableList = ''; /** * Array of collapsed / uncollapsed tables in multi table view * * @var int[][] */ public $tablesCollapsed = []; /** * @var array[] Module configuration */ public $modTSconfig; /** * HTML output * * @var string */ public $HTMLcode = ''; /** * Thumbnails on records containing files (pictures) * * @var bool */ public $thumbs = 0; /** * Used for tracking next/prev uids * * @var int[][] */ public $currentTable = []; /** * OBSOLETE - NOT USED ANYMORE. leftMargin * * @var int */ public $leftMargin = 0; /** * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column. * * @var array */ public $fieldArray = []; /** * Set to zero, if you don't want a left-margin with addElement function * * @var int */ public $setLMargin = 1; /** * Contains page translation languages * * @var array */ public $pageOverlays = []; /** * Counter increased for each element. Used to index elements for the JavaScript-code that transfers to the clipboard * * @var int */ public $counter = 0; /** * Contains sys language icons and titles * * @var array * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Use site languages instead. */ public $languageIconTitles = []; /** * Contains site languages for this page ID * * @var SiteLanguage[] */ protected $siteLanguages = []; /** * Script URL * * @var string */ public $thisScript = ''; /** * If set this is CSS-classname for odd columns in addElement. Used with db_layout / pages section * * @var string */ public $oddColumnsCssClass = ''; /** * Not used in this class - but maybe extension classes... * Max length of strings * * @var int */ public $fixedL = 30; /** * @var TranslationConfigurationProvider * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public $translateTools; /** * Keys are fieldnames and values are td-parameters to add in addElement(), please use $addElement_tdCSSClass for CSS-classes; * * @var array */ public $addElement_tdParams = []; /** * @var int */ public $no_noWrap = 0; /** * @var int */ public $showIcon = 1; /** * Keys are fieldnames and values are td-css-classes to add in addElement(); * * @var array */ public $addElement_tdCssClass = []; /** * @var \TYPO3\CMS\Backend\Clipboard\Clipboard */ protected $clipboard; /** * User permissions * * @var int */ public $ext_CALC_PERMS; /** * Current ids page record * * @var array */ protected $pageinfo; /** * Caches the available languages in a colPos * * @var array */ protected $languagesInColumnCache = []; /** * Caches the amount of content elements as a matrix * * @var array * @internal */ protected $contentElementCache = []; /** * @var IconFactory */ protected $iconFactory; /** * Stores whether a certain language has translations in it * * @var array */ protected $languageHasTranslationsCache = []; /** * @var LocalizationController */ protected $localizationController; /** * Override the page ids taken into account by getPageIdConstraint() * * @var array */ protected $overridePageIdList = []; /** * Override/add urlparameters in listUrl() method * * @var string[] */ protected $overrideUrlParameters = []; /** * Array with before/after setting for tables * Structure: * 'tableName' => [ * 'before' => ['A', ...] * 'after' => [] * ] * @var array[] */ protected $tableDisplayOrder = []; /** * Cache the number of references to a record * * @var array */ protected $referenceCount = []; /** * Construct to initialize class variables. */ public function __construct() { if (isset($GLOBALS['BE_USER']->uc['titleLen']) && $GLOBALS['BE_USER']->uc['titleLen'] > 0) { $this->fixedL = $GLOBALS['BE_USER']->uc['titleLen']; } $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Remove this instance along with the property. $this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class); $this->determineScriptUrl(); $this->localizationController = GeneralUtility::makeInstance(LocalizationController::class); $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf'); $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip'); $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Localization'); } /***************************************** * * Renderings * *****************************************/ /** * Adds the code of a single table * * @param string $table Table name * @param int $id Current page id * @param string $fields * @return string HTML for listing. */ public function getTable($table, $id, $fields = '') { if (isset($this->externalTables[$table])) { return $this->getExternalTables($id, $table); } // Branch out based on table name: switch ($table) { case 'pages': return $this->getTable_pages($id); case 'tt_content': return $this->getTable_tt_content($id); default: return ''; } } /** * Renders an external table from page id * * @param int $id Page id * @param string $table Name of the table * @return string HTML for the listing */ public function getExternalTables($id, $table) { $this->pageinfo = BackendUtility::readPageAccess($id, ''); $type = $this->getPageLayoutController()->MOD_SETTINGS[$table]; if (!isset($type)) { $type = 0; } // eg. "name;title;email;company,image" $fList = $this->externalTables[$table][$type]['fList']; // The columns are separeted by comma ','. // Values separated by semicolon ';' are shown in the same column. $icon = $this->externalTables[$table][$type]['icon']; $addWhere = $this->externalTables[$table][$type]['addWhere']; // Create listing $out = $this->makeOrdinaryList($table, $id, $fList, $icon, $addWhere); return $out; } /** * Renders records from the pages table from page id * (Used to get information about the page tree content by "Web>Info"!) * * @param int $id Page id * @return string HTML for the listing */ public function getTable_pages($id) { // Initializing: $out = ''; $lang = $this->getLanguageService(); // Select current page: if (!$id) { // The root has a pseudo record in pageinfo... $row = $this->getPageLayoutController()->pageinfo; } else { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('pages'); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $row = $queryBuilder ->select('*') ->from('pages') ->where( $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)), $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) ) ->execute() ->fetch(); BackendUtility::workspaceOL('pages', $row); } // If there was found a page: if (is_array($row)) { // Getting select-depth: $depth = (int)$this->getPageLayoutController()->MOD_SETTINGS['pages_levels']; // Overriding a few things: $this->no_noWrap = 0; // Items $this->eCounter = $this->firstElementNumber; // Creating elements: list($flag, $code) = $this->fwd_rwd_nav(); $out .= $code; $editUids = []; if ($flag) { // Getting children: $theRows = $this->getPageRecordsRecursive($row['uid'], $depth); if ($this->getBackendUser()->doesUserHaveAccess($row, 2) && $row['uid'] > 0) { $editUids[] = $row['uid']; } $out .= $this->pages_drawItem($row, $this->fieldArray); // Traverse all pages selected: foreach ($theRows as $sRow) { if ($this->getBackendUser()->doesUserHaveAccess($sRow, 2)) { $editUids[] = $sRow['uid']; } $out .= $this->pages_drawItem($sRow, $this->fieldArray); } $this->eCounter++; } // Header line is drawn $theData = []; $editIdList = implode(',', $editUids); // Traverse fields (as set above) in order to create header values: foreach ($this->fieldArray as $field) { if ($editIdList && isset($GLOBALS['TCA']['pages']['columns'][$field]) && $field !== 'uid' && !$this->pages_noEditColumns ) { $iTitle = sprintf( $lang->getLL('editThisColumn'), rtrim(trim($lang->sL(BackendUtility::getItemLabel('pages', $field))), ':') ); $urlParameters = [ 'edit' => [ 'pages' => [ $editIdList => 'edit' ] ], 'columnsOnly' => $field, 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); $eI = '' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . ''; } else { $eI = ''; } switch ($field) { case 'title': $theData[$field] = $eI . ' ' . $lang->sL($GLOBALS['TCA']['pages']['columns'][$field]['label']) . ''; break; case 'uid': $theData[$field] = ''; break; default: if (strpos($field, 'table_') === 0) { $f2 = substr($field, 6); if ($GLOBALS['TCA'][$f2]) { $theData[$field] = ' ' . '' . $this->iconFactory->getIconForRecord($f2, [], Icon::SIZE_SMALL)->render() . ''; } } else { $theData[$field] = $eI . ' ' . htmlspecialchars($lang->sL($GLOBALS['TCA']['pages']['columns'][$field]['label'])) . ''; } } } $out = '
' . '' . '' . $this->addElement(1, '', $theData) . '' . '' . $out . '' . '
' . '
'; } return $out; } /** * Renders Content Elements from the tt_content table from page id * * @param int $id Page id * @return string HTML for the listing */ public function getTable_tt_content($id) { $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionForTable('tt_content') ->getExpressionBuilder(); $this->pageinfo = BackendUtility::readPageAccess($this->id, ''); $this->initializeLanguages(); $this->initializeClipboard(); $pageTitleParamForAltDoc = '&recTitle=' . rawurlencode(BackendUtility::getRecordTitle('pages', BackendUtility::getRecordWSOL('pages', $id), true)); /** @var PageRenderer $pageRenderer */ $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/DragDrop'); $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal'); $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/Paste'); $pageActionsCallback = null; if ($this->isPageEditable()) { $languageOverlayId = 0; $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, (int)$this->tt_contentConfig['sys_language_uid']); if (is_array($pageLocalizationRecord)) { $pageLocalizationRecord = reset($pageLocalizationRecord); } if (!empty($pageLocalizationRecord['uid'])) { $languageOverlayId = $pageLocalizationRecord['uid']; } $pageActionsCallback = 'function(PageActions) { PageActions.setPageId(' . (int)$this->id . '); PageActions.setLanguageOverlayId(' . $languageOverlayId . '); }'; } $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/PageActions', $pageActionsCallback); // Get labels for CTypes and tt_content element fields in general: $this->CType_labels = []; foreach ($GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'] as $val) { $this->CType_labels[$val[1]] = $this->getLanguageService()->sL($val[0]); } $this->itemLabels = []; foreach ($GLOBALS['TCA']['tt_content']['columns'] as $name => $val) { $this->itemLabels[$name] = $this->getLanguageService()->sL($val['label']); } $languageColumn = []; $out = ''; // Setting language list: $langList = $this->tt_contentConfig['sys_language_uid']; if ($this->tt_contentConfig['languageMode']) { if ($this->tt_contentConfig['languageColsPointer']) { $langList = '0,' . $this->tt_contentConfig['languageColsPointer']; } else { $langList = implode(',', array_keys($this->tt_contentConfig['languageCols'])); } $languageColumn = []; } $langListArr = GeneralUtility::intExplode(',', $langList); $defaultLanguageElementsByColumn = []; $defLangBinding = []; // For each languages... : // If not languageMode, then we'll only be through this once. foreach ($langListArr as $lP) { $lP = (int)$lP; if (!isset($this->contentElementCache[$lP])) { $this->contentElementCache[$lP] = []; } if (count($langListArr) === 1 || $lP === 0) { $showLanguage = $expressionBuilder->in('sys_language_uid', [$lP, -1]); } else { $showLanguage = $expressionBuilder->eq('sys_language_uid', $lP); } $content = []; $head = []; $backendLayout = $this->getBackendLayoutView()->getSelectedBackendLayout($this->id); $columns = $backendLayout['__colPosList']; // Select content records per column $contentRecordsPerColumn = $this->getContentRecordsPerColumn('table', $id, $columns, $showLanguage); $cList = array_keys($contentRecordsPerColumn); // For each column, render the content into a variable: foreach ($cList as $columnId) { if (!isset($this->contentElementCache[$lP])) { $this->contentElementCache[$lP] = []; } if (!$lP) { $defaultLanguageElementsByColumn[$columnId] = []; } // Start wrapping div $content[$columnId] .= '
'; // Add new content at the top most position $link = ''; if ($this->isContentEditable() && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP)) ) { if ($this->option_newWizard) { $urlParameters = [ 'id' => $id, 'sys_language_uid' => $lP, 'colPos' => $columnId, 'uid_pid' => $id, 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $routeName = BackendUtility::getPagesTSconfig($id)['mod.']['newContentElementWizard.']['override'] ?? 'new_content_element_wizard'; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute($routeName, $urlParameters); } else { $urlParameters = [ 'edit' => [ 'tt_content' => [ $id => 'new' ] ], 'defVals' => [ 'tt_content' => [ 'colPos' => $columnId, 'sys_language_uid' => $lP ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); } $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement')); $link = '' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . ' ' . htmlspecialchars($this->getLanguageService()->getLL('content')) . ''; } if ($this->getBackendUser()->checkLanguageAccess($lP) && $columnId !== 'unused') { $content[$columnId] .= '
' . $link . '
'; } $editUidList = ''; if (!isset($contentRecordsPerColumn[$columnId]) || !is_array($contentRecordsPerColumn[$columnId])) { $message = GeneralUtility::makeInstance( FlashMessage::class, $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:error.invalidBackendLayout'), '', FlashMessage::WARNING ); $service = GeneralUtility::makeInstance(FlashMessageService::class); $queue = $service->getMessageQueueByIdentifier(); $queue->addMessage($message); } else { $rowArr = $contentRecordsPerColumn[$columnId]; $this->generateTtContentDataArray($rowArr); foreach ((array)$rowArr as $rKey => $row) { $this->contentElementCache[$lP][$columnId][$row['uid']] = $row; if ($this->tt_contentConfig['languageMode']) { $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId]; } if (is_array($row) && !VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { $singleElementHTML = '
'; if (!$lP && ($this->defLangBinding || $row['sys_language_uid'] != -1)) { $defaultLanguageElementsByColumn[$columnId][] = ($row['_ORIG_uid'] ?? $row['uid']); } $editUidList .= $row['uid'] . ','; $disableMoveAndNewButtons = $this->defLangBinding && $lP > 0 && $this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP); $singleElementHTML .= $this->tt_content_drawHeader( $row, $this->tt_contentConfig['showInfo'] ? 15 : 5, $disableMoveAndNewButtons, true, $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT) ); $innerContent = '
' . $this->tt_content_drawItem($row) . '
'; $singleElementHTML .= '
' . $innerContent . '
' . $this->tt_content_drawFooter($row); $isDisabled = $this->isDisabled('tt_content', $row); $statusHidden = $isDisabled ? ' t3-page-ce-hidden t3js-hidden-record' : ''; $displayNone = !$this->tt_contentConfig['showHidden'] && $isDisabled ? ' style="display: none;"' : ''; $highlightHeader = ''; if ($this->checkIfTranslationsExistInLanguage([], (int)$row['sys_language_uid']) && (int)$row['l18n_parent'] === 0) { $highlightHeader = ' t3-page-ce-danger'; } elseif ($columnId === 'unused') { $highlightHeader = ' t3-page-ce-warning'; } $singleElementHTML = '
' . $singleElementHTML . '
'; $singleElementHTML .= '
'; $singleElementHTML .= '
'; // Add icon "new content element below" if (!$disableMoveAndNewButtons && $this->isContentEditable($lP) && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP)) && $columnId !== 'unused' ) { // New content element: if ($this->option_newWizard) { $urlParameters = [ 'id' => $row['pid'], 'sys_language_uid' => $row['sys_language_uid'], 'colPos' => $row['colPos'], 'uid_pid' => -$row['uid'], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $routeName = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['newContentElementWizard.']['override'] ?? 'new_content_element_wizard'; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute($routeName, $urlParameters); } else { $urlParameters = [ 'edit' => [ 'tt_content' => [ -$row['uid'] => 'new' ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); } $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement')); $singleElementHTML .= '' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . ' ' . htmlspecialchars($this->getLanguageService()->getLL('content')) . ''; } $singleElementHTML .= '
'; if ($this->defLangBinding && $this->tt_contentConfig['languageMode']) { $defLangBinding[$columnId][$lP][$row[$lP ? 'l18n_parent' : 'uid'] ?: $row['uid']] = $singleElementHTML; } else { $content[$columnId] .= $singleElementHTML; } } else { unset($rowArr[$rKey]); } } $content[$columnId] .= ''; $colTitle = BackendUtility::getProcessedValue('tt_content', 'colPos', $columnId); $tcaItems = GeneralUtility::callUserFunction(\TYPO3\CMS\Backend\View\BackendLayoutView::class . '->getColPosListItemsParsed', $id, $this); foreach ($tcaItems as $item) { if ($item[1] == $columnId) { $colTitle = $this->getLanguageService()->sL($item[0]); } } if ($columnId === 'unused') { if (empty($unusedElementsMessage)) { $unusedElementsMessage = GeneralUtility::makeInstance( FlashMessage::class, $this->getLanguageService()->getLL('staleUnusedElementsWarning'), $this->getLanguageService()->getLL('staleUnusedElementsWarningTitle'), FlashMessage::WARNING ); $service = GeneralUtility::makeInstance(FlashMessageService::class); $queue = $service->getMessageQueueByIdentifier(); $queue->addMessage($unusedElementsMessage); } $colTitle = $this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.unused'); $editParam = ''; } else { $editParam = $this->doEdit && !empty($rowArr) ? '&edit[tt_content][' . $editUidList . ']=edit' . $pageTitleParamForAltDoc : ''; } $head[$columnId] .= $this->tt_content_drawColHeader($colTitle, $editParam); } } // For each column, fit the rendered content into a table cell: $out = ''; if ($this->tt_contentConfig['languageMode']) { // in language mode process the content elements, but only fill $languageColumn. output will be generated later $sortedLanguageColumn = []; foreach ($cList as $columnId) { if (GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnId) || $columnId === 'unused') { $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId]; // We sort $languageColumn again according to $cList as it may contain data already from above. $sortedLanguageColumn[$columnId] = $languageColumn[$columnId]; } } if (!empty($languageColumn['unused'])) { $sortedLanguageColumn['unused'] = $languageColumn['unused']; } $languageColumn = $sortedLanguageColumn; } else { // GRID VIEW: $grid = '
'; // Add colgroups $colCount = (int)$backendLayout['__config']['backend_layout.']['colCount']; $rowCount = (int)$backendLayout['__config']['backend_layout.']['rowCount']; $grid .= ''; for ($i = 0; $i < $colCount; $i++) { $grid .= ''; } $grid .= ''; // Check how to handle restricted columns $hideRestrictedCols = (bool)(BackendUtility::getPagesTSconfig($id)['mod.']['web_layout.']['hideRestrictedCols'] ?? false); // Cycle through rows for ($row = 1; $row <= $rowCount; $row++) { $rowConfig = $backendLayout['__config']['backend_layout.']['rows.'][$row . '.']; if (!isset($rowConfig)) { continue; } $grid .= ''; for ($col = 1; $col <= $colCount; $col++) { $columnConfig = $rowConfig['columns.'][$col . '.']; if (!isset($columnConfig)) { continue; } // Which tt_content colPos should be displayed inside this cell $columnKey = (int)$columnConfig['colPos']; // Render the grid cell $colSpan = (int)$columnConfig['colspan']; $rowSpan = (int)$columnConfig['rowspan']; $grid .= ''; } $grid .= ''; } if (!empty($content['unused'])) { $grid .= ''; // Which tt_content colPos should be displayed inside this cell $columnKey = 'unused'; // Render the grid cell $colSpan = (int)$backendLayout['__config']['backend_layout.']['colCount']; $grid .= ''; } $out .= $grid . '
0 ? ' colspan="' . $colSpan . '"' : '') . ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') . ' data-colpos="' . (int)$columnConfig['colPos'] . '" data-language-uid="' . $lP . '" class="t3js-page-lang-column-' . $lP . ' t3js-page-column t3-grid-cell t3-page-column t3-page-column-' . $columnKey . ((!isset($columnConfig['colPos']) || $columnConfig['colPos'] === '') ? ' t3-grid-cell-unassigned' : '') . ((isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && !$head[$columnKey]) || !GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) ? ($hideRestrictedCols ? ' t3-grid-cell-restricted t3-grid-cell-hidden' : ' t3-grid-cell-restricted') : '') . ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') . ($rowSpan > 0 ? ' t3-gridCell-height' . $rowSpan : '') . '">'; // Draw the pre-generated header with edit and new buttons if a colPos is assigned. // If not, a new header without any buttons will be generated. if (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && $head[$columnKey] && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) ) { $grid .= $head[$columnKey] . $content[$columnKey]; } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) ) { if (!$hideRestrictedCols) { $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('noAccess')); } } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && !GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) ) { if (!$hideRestrictedCols) { $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name']) . ' (' . $this->getLanguageService()->getLL('noAccess') . ')'); } } elseif (isset($columnConfig['name']) && $columnConfig['name'] !== '') { $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name']) . ' (' . $this->getLanguageService()->getLL('notAssigned') . ')'); } else { $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('notAssigned')); } $grid .= '
0 ? ' colspan="' . $colSpan . '"' : '') . ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') . ' data-colpos="unused" data-language-uid="' . $lP . '" class="t3js-page-lang-column-' . $lP . ' t3js-page-column t3-grid-cell t3-page-column t3-page-column-' . $columnKey . ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') . '">'; // Draw the pre-generated header with edit and new buttons if a colPos is assigned. // If not, a new header without any buttons will be generated. $grid .= $head[$columnKey] . $content[$columnKey]; $grid .= '
'; } } $elFromTable = $this->clipboard->elFromTable('tt_content'); if (!empty($elFromTable) && $this->isContentEditable()) { $pasteItem = substr(key($elFromTable), 11); $pasteRecord = BackendUtility::getRecord('tt_content', (int)$pasteItem); $pasteTitle = $pasteRecord['header'] ? $pasteRecord['header'] : $pasteItem; $copyMode = $this->clipboard->clipData['normal']['mode'] ? '-' . $this->clipboard->clipData['normal']['mode'] : ''; $addExtOnReadyCode = ' top.pasteIntoLinkTemplate = ' . $this->tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-into', 'pasteIntoColumn') . '; top.pasteAfterLinkTemplate = ' . $this->tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-after', 'pasteAfterRecord') . ';'; } else { $addExtOnReadyCode = ' top.pasteIntoLinkTemplate = \'\'; top.pasteAfterLinkTemplate = \'\';'; } $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); $pageRenderer->addJsInlineCode('pasteLinkTemplates', $addExtOnReadyCode); // If language mode, then make another presentation: // Notice that THIS presentation will override the value of $out! // But it needs the code above to execute since $languageColumn is filled with content we need! if ($this->tt_contentConfig['languageMode']) { // Get language selector: $languageSelector = $this->languageSelector($id); // Reset out - we will make new content here: $out = ''; // Traverse languages found on the page and build up the table displaying them side by side: $cCont = []; $sCont = []; foreach ($langListArr as $lP) { $languageMode = ''; $labelClass = 'info'; // Header: $lP = (int)$lP; // Determine language mode if ($lP > 0 && isset($this->languageHasTranslationsCache[$lP]['mode'])) { switch ($this->languageHasTranslationsCache[$lP]['mode']) { case 'mixed': $languageMode = $this->getLanguageService()->getLL('languageModeMixed'); $labelClass = 'danger'; break; case 'connected': $languageMode = $this->getLanguageService()->getLL('languageModeConnected'); break; case 'free': $languageMode = $this->getLanguageService()->getLL('languageModeFree'); break; default: // we'll let opcode optimize this intentionally empty case } } $cCont[$lP] = '

' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '

' . ($languageMode !== '' ? '' . $languageMode . '' : '') . ' '; // "View page" icon is added: $viewLink = ''; if (!VersionState::cast($this->getPageLayoutController()->pageinfo['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { $onClick = BackendUtility::viewOnClick( $this->id, '', BackendUtility::BEgetRootLine($this->id), '', '', '&L=' . $lP ); $viewLink = '' . $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render() . ''; } // Language overlay page header: if ($lP) { $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $id, $lP); if (is_array($pageLocalizationRecord)) { $pageLocalizationRecord = reset($pageLocalizationRecord); } BackendUtility::workspaceOL('pages', $pageLocalizationRecord); $recordIcon = BackendUtility::wrapClickMenuOnIcon( $this->iconFactory->getIconForRecord('pages', $pageLocalizationRecord, Icon::SIZE_SMALL)->render(), 'pages', $pageLocalizationRecord['uid'] ); $urlParameters = [ 'edit' => [ 'pages' => [ $pageLocalizationRecord['uid'] => 'edit' ] ], // Disallow manual adjustment of the language field for pages 'overrideVals' => [ 'pages' => [ 'sys_language_uid' => $lP ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); $editLink = ( $this->getBackendUser()->check('tables_modify', 'pages') ? '' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '' : '' ); $defaultLanguageElements = []; array_walk($defaultLanguageElementsByColumn, function (array $columnContent) use (&$defaultLanguageElements) { $defaultLanguageElements = array_merge($defaultLanguageElements, $columnContent); }); $localizationButtons = []; $localizationButtons[] = $this->newLanguageButton( $this->getNonTranslatedTTcontentUids($defaultLanguageElements, $id, $lP), $lP ); $lPLabel = '
' . $viewLink . $editLink . (!empty($localizationButtons) ? implode(LF, $localizationButtons) : '') . '
' . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($pageLocalizationRecord['title'], 20)) ; } else { $editLink = ''; $recordIcon = ''; if ($this->getBackendUser()->checkLanguageAccess(0)) { $recordIcon = BackendUtility::wrapClickMenuOnIcon( $this->iconFactory->getIconForRecord('pages', $this->pageRecord, Icon::SIZE_SMALL)->render(), 'pages', $this->id ); $urlParameters = [ 'edit' => [ 'pages' => [ $this->id => 'edit' ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); $editLink = ( $this->getBackendUser()->check('tables_modify', 'pages') ? '' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '' : '' ); } $lPLabel = '
' . $viewLink . $editLink . '
' . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($this->pageRecord['title'], 20)); } $sCont[$lP] = ' ' . $lPLabel . ''; } // Add headers: $out .= '' . implode('', $cCont) . ''; $out .= '' . implode('', $sCont) . ''; unset($cCont, $sCont); // Traverse previously built content for the columns: foreach ($languageColumn as $cKey => $cCont) { $out .= ''; foreach ($cCont as $languageId => $columnContent) { $out .= '' . $columnContent . ''; } $out .= ''; if ($this->defLangBinding && !empty($defLangBinding[$cKey])) { $maxItemsCount = max(array_map('count', $defLangBinding[$cKey])); for ($i = 0; $i < $maxItemsCount; $i++) { $defUid = $defaultLanguageElementsByColumn[$cKey][$i] ?? 0; $cCont = []; foreach ($langListArr as $lP) { if ($lP > 0 && is_array($defLangBinding[$cKey][$lP]) && !$this->checkIfTranslationsExistInLanguage($defaultLanguageElementsByColumn[$cKey], $lP) && count($defLangBinding[$cKey][$lP]) > $i ) { $slice = array_slice($defLangBinding[$cKey][$lP], $i, 1); $element = $slice[0] ?? ''; } else { $element = $defLangBinding[$cKey][$lP][$defUid] ?? ''; } $cCont[] = $element; } $out .= ' ' . implode(' ', $cCont) . ' '; } } } // Finally, wrap it all in a table and add the language selector on top of it: $out = $languageSelector . '
' . $out . '
'; } return $out; } /********************************** * * Generic listing of items * **********************************/ /** * Creates a standard list of elements from a table. * * @param string $table Table name * @param int $id Page id. * @param string $fList Comma list of fields to display * @param bool $icon If TRUE, icon is shown * @param string $addWhere Additional WHERE-clauses. * @return string HTML table */ public function makeOrdinaryList($table, $id, $fList, $icon = false, $addWhere = '') { // Initialize $addWhere = empty($addWhere) ? [] : [QueryHelper::stripLogicalOperatorPrefix($addWhere)]; $queryBuilder = $this->getQueryBuilder($table, $id, $addWhere); $this->setTotalItems($table, $id, $addWhere); $dbCount = 0; $result = false; // Make query for records if there were any records found in the count operation if ($this->totalItems) { $result = $queryBuilder->execute(); // Will return FALSE, if $result is invalid $dbCount = $queryBuilder->count('uid')->execute()->fetchColumn(0); } // If records were found, render the list if (!$dbCount) { return ''; } // Set fields $out = ''; $this->fieldArray = GeneralUtility::trimExplode(',', '__cmds__,' . $fList . ',__editIconLink__', true); $theData = []; $theData = $this->headerFields($this->fieldArray, $table, $theData); // Title row $localizedTableTitle = htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title'])); $out .= '' . '' . $localizedTableTitle . ' (' . $dbCount . ')' . '' . ''; // Column's titles if ($this->doEdit) { $urlParameters = [ 'edit' => [ $table => [ $this->id => 'new' ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); $title = htmlspecialchars($this->getLanguageService()->getLL('new')); $theData['__cmds__'] = '' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . ''; } $out .= $this->addElement(1, '', $theData, ' class="c-headLine"', 15, '', 'th'); // Render Items $this->eCounter = $this->firstElementNumber; while ($row = $result->fetch()) { BackendUtility::workspaceOL($table, $row); if (is_array($row)) { list($flag, $code) = $this->fwd_rwd_nav(); $out .= $code; if ($flag) { $Nrow = []; // Setting icons links if ($icon) { $Nrow['__cmds__'] = $this->getIcon($table, $row); } // Get values: $Nrow = $this->dataFields($this->fieldArray, $table, $row, $Nrow); // Attach edit icon if ($this->doEdit) { $urlParameters = [ 'edit' => [ $table => [ $row['uid'] => 'edit' ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); $Nrow['__editIconLink__'] = '' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . ''; } else { $Nrow['__editIconLink__'] = $this->noEditIcon(); } $out .= $this->addElement(1, '', $Nrow); } $this->eCounter++; } } // Wrap it all in a table: $out = '
' . $out . '
'; return $out; } /** * Adds content to all data fields in $out array * * Each field name in $fieldArr has a special feature which is that the field name can be specified as more field names. * Eg. "field1,field2;field3". * Field 2 and 3 will be shown in the same cell of the table separated by
while field1 will have its own cell. * * @param array $fieldArr Array of fields to display * @param string $table Table name * @param array $row Record array * @param array $out Array to which the data is added * @return array $out array returned after processing. * @see makeOrdinaryList() */ public function dataFields($fieldArr, $table, $row, $out = []) { // Check table validity if (!isset($GLOBALS['TCA'][$table])) { return $out; } $thumbsCol = $GLOBALS['TCA'][$table]['ctrl']['thumbnail']; // Traverse fields foreach ($fieldArr as $fieldName) { if ($GLOBALS['TCA'][$table]['columns'][$fieldName]) { // Each field has its own cell (if configured in TCA) // If the column is a thumbnail column: if ($fieldName == $thumbsCol) { $out[$fieldName] = $this->thumbCode($row, $table, $fieldName); } else { // ... otherwise just render the output: $out[$fieldName] = nl2br(htmlspecialchars(trim(GeneralUtility::fixed_lgd_cs( BackendUtility::getProcessedValue($table, $fieldName, $row[$fieldName], 0, 0, 0, $row['uid']), 250 )))); } } else { // Each field is separated by
and shown in the same cell (If not a TCA field, then explode // the field name with ";" and check each value there as a TCA configured field) $theFields = explode(';', $fieldName); // Traverse fields, separated by ";" (displayed in a single cell). foreach ($theFields as $fName2) { if ($GLOBALS['TCA'][$table]['columns'][$fName2]) { $out[$fieldName] .= '' . htmlspecialchars($this->getLanguageService()->sL( $GLOBALS['TCA'][$table]['columns'][$fName2]['label'] )) . '' . '  ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs( BackendUtility::getProcessedValue($table, $fName2, $row[$fName2], 0, 0, 0, $row['uid']), 25 )) . '
'; } } } // If no value, add a nbsp. if (!$out[$fieldName]) { $out[$fieldName] = ' '; } // Wrap in dimmed-span tags if record is "disabled" if ($this->isDisabled($table, $row)) { $out[$fieldName] = '' . $out[$fieldName] . ''; } } return $out; } /** * Header fields made for the listing of records * * @param array $fieldArr Field names * @param string $table The table name * @param array $out Array to which the headers are added. * @return array $out returned after addition of the header fields. * @see makeOrdinaryList() */ public function headerFields($fieldArr, $table, $out = []) { foreach ($fieldArr as $fieldName) { $ll = htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])); $out[$fieldName] = $ll ? $ll : ' '; } return $out; } /** * Gets content records per column. * This is required for correct workspace overlays. * * @param string $table UNUSED (will always be queried from tt_content) * @param int $id Page Id to be used (not used at all, but part of the API, see $this->pidSelect) * @param array $columns colPos values to be considered to be shown * @param string $additionalWhereClause Additional where clause for database select * @return array Associative array for each column (colPos) */ protected function getContentRecordsPerColumn($table, $id, array $columns, $additionalWhereClause = '') { $contentRecordsPerColumn = array_fill_keys($columns, []); $columns = array_flip($columns); $queryBuilder = $this->getQueryBuilder( 'tt_content', $id, [ $additionalWhereClause ] ); // Traverse any selected elements and render their display code: $results = $this->getResult($queryBuilder->execute()); $unused = []; $hookArray = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used'] ?? []; foreach ($results as $record) { $used = isset($columns[$record['colPos']]); foreach ($hookArray as $_funcRef) { $_params = ['columns' => $columns, 'record' => $record, 'used' => $used]; $used = GeneralUtility::callUserFunction($_funcRef, $_params, $this); } if ($used) { $columnValue = (string)$record['colPos']; $contentRecordsPerColumn[$columnValue][] = $record; } else { $unused[] = $record; } } if (!empty($unused)) { $contentRecordsPerColumn['unused'] = $unused; } return $contentRecordsPerColumn; } /********************************** * * Additional functions; Pages * **********************************/ /** * Adds pages-rows to an array, selecting recursively in the page tree. * * @param int $pid Starting page id to select from * @param string $iconPrefix Prefix for icon code. * @param int $depth Depth (decreasing) * @param array $rows Array which will accumulate page rows * @return array $rows with added rows. */ protected function getPageRecordsRecursive(int $pid, int $depth, string $iconPrefix = '', array $rows = []): array { $depth--; $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $queryBuilder ->select('*') ->from('pages') ->where( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)), $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)), $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) ); if (!empty($GLOBALS['TCA']['pages']['ctrl']['sortby'])) { $queryBuilder->orderBy($GLOBALS['TCA']['pages']['ctrl']['sortby']); } if ($depth >= 0) { $result = $queryBuilder->execute(); $rowCount = $queryBuilder->count('uid')->execute()->fetchColumn(0); $count = 0; while ($row = $result->fetch()) { BackendUtility::workspaceOL('pages', $row); if (is_array($row)) { $count++; $row['treeIcons'] = $iconPrefix . ''; $rows[] = $row; // Get the branch $spaceOutIcons = ''; $rows = $this->getPageRecordsRecursive( $row['uid'], $row['php_tree_stop'] ? 0 : $depth, $iconPrefix . $spaceOutIcons, $rows ); } } } return $rows; } /** * Adds a list item for the pages-rendering * * @param array $row Record array * @param array $fieldArr Field list * @return string HTML for the item */ public function pages_drawItem($row, $fieldArr) { $userTsConfig = $this->getBackendUser()->getTSConfig(); // Initialization $theIcon = $this->getIcon('pages', $row); // Preparing and getting the data-array $theData = []; foreach ($fieldArr as $field) { switch ($field) { case 'title': $showPageId = !empty($userTsConfig['options.']['pageTree.']['showPageIdWithTitle']); $pTitle = htmlspecialchars(BackendUtility::getProcessedValue('pages', $field, $row[$field], 20)); $theData[$field] = $row['treeIcons'] . $theIcon . ($showPageId ? '[' . $row['uid'] . '] ' : '') . $pTitle; break; case 'php_tree_stop': // Intended fall through case 'TSconfig': $theData[$field] = $row[$field] ? 'x' : ' '; break; case 'uid': if ($this->getBackendUser()->doesUserHaveAccess($row, 2) && $row['uid'] > 0) { $urlParameters = [ 'edit' => [ 'pages' => [ $row['uid'] => 'edit' ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); $onClick = BackendUtility::viewOnClick($row['uid'], '', BackendUtility::BEgetRootLine($row['uid'])); $eI = '' . $this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)->render() . ''; $eI .= '' . $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render() . ''; } else { $eI = ''; } $theData[$field] = '
' . $eI . '
'; break; case 'shortcut': case 'shortcut_mode': if ((int)$row['doktype'] === \TYPO3\CMS\Frontend\Page\PageRepository::DOKTYPE_SHORTCUT) { $theData[$field] = $this->getPagesTableFieldValue($field, $row); } break; default: if (strpos($field, 'table_') === 0) { $f2 = substr($field, 6); if ($GLOBALS['TCA'][$f2]) { $c = $this->numberOfRecords($f2, $row['uid']); $theData[$field] = ($c ? $c : ''); } } else { $theData[$field] = $this->getPagesTableFieldValue($field, $row); } } } $this->addElement_tdParams['title'] = $row['_CSSCLASS'] ? ' class="' . $row['_CSSCLASS'] . '"' : ''; return $this->addElement(1, '', $theData); } /** * Returns the HTML code for rendering a field in the pages table. * The row value is processed to a human readable form and the result is parsed through htmlspecialchars(). * * @param string $field The name of the field of which the value should be rendered. * @param array $row The pages table row as an associative array. * @return string The rendered table field value. */ protected function getPagesTableFieldValue($field, array $row) { return htmlspecialchars(BackendUtility::getProcessedValue('pages', $field, $row[$field])); } /********************************** * * Additional functions; Content Elements * **********************************/ /** * Draw header for a content element column: * * @param string $colName Column name * @param string $editParams Edit params (Syntax: &edit[...] for FormEngine) * @return string HTML table */ public function tt_content_drawColHeader($colName, $editParams = '') { $iconsArr = []; // Create command links: if ($this->tt_contentConfig['showCommands']) { // Edit whole of column: if ($editParams && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT) && $this->getBackendUser()->checkLanguageAccess(0)) { $iconsArr['edit'] = '' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . ''; } } $icons = ''; if (!empty($iconsArr)) { $icons = '
' . implode('', $iconsArr) . '
'; } // Create header row: $out = '
' . $icons . '
' . htmlspecialchars($colName) . '
'; return $out; } /** * Draw a paste icon either for pasting into a column or for pasting after a record * * @param int $pasteItem ID of the item in the clipboard * @param string $pasteTitle Title for the JS modal * @param string $copyMode copy or cut * @param string $cssClass CSS class to determine if pasting is done into column or after record * @param string $title title attribute of the generated link * * @return string Generated HTML code with link and icon */ protected function tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, $cssClass, $title) { $pasteIcon = json_encode( ' ' . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() . '' ); return $pasteIcon; } /** * Draw the footer for a single tt_content element * * @param array $row Record array * @return string HTML of the footer * @throws \UnexpectedValueException */ protected function tt_content_drawFooter(array $row) { $content = ''; // Get processed values: $info = []; $this->getProcessedValue('tt_content', 'starttime,endtime,fe_group,space_before_class,space_after_class', $row, $info); // Content element annotation if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) { $info[] = htmlspecialchars($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]); } // Call drawFooter hooks foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) { $hookObject = GeneralUtility::makeInstance($className); if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) { throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1404378171); } $hookObject->preProcess($this, $info, $row); } // Display info from records fields: if (!empty($info)) { $content = '
' . implode('
', $info) . '
'; } // Wrap it if (!empty($content)) { $content = ''; } return $content; } /** * Draw the header for a single tt_content element * * @param array $row Record array * @param int $space Amount of pixel space above the header. UNUSED * @param bool $disableMoveAndNewButtons If set the buttons for creating new elements and moving up and down are not shown. * @param bool $langMode If set, we are in language mode and flags will be shown for languages * @param bool $dragDropEnabled If set the move button must be hidden * @return string HTML table with the record header. */ public function tt_content_drawHeader($row, $space = 0, $disableMoveAndNewButtons = false, $langMode = false, $dragDropEnabled = false) { $backendUser = $this->getBackendUser(); $out = ''; // If show info is set...; if ($this->tt_contentConfig['showInfo'] && $backendUser->recordEditAccessInternals('tt_content', $row)) { // Render control panel for the element: if ($this->tt_contentConfig['showCommands'] && $this->isContentEditable($row['sys_language_uid'])) { // Edit content element: $urlParameters = [ 'edit' => [ 'tt_content' => [ $this->tt_contentData['nextThree'][$row['uid']] => 'edit' ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid'], ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-tt_content-' . $row['uid']; $out .= '' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . ''; // Hide element: $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled']; if ($hiddenField && $GLOBALS['TCA']['tt_content']['columns'][$hiddenField] && (!$GLOBALS['TCA']['tt_content']['columns'][$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', 'tt_content:' . $hiddenField)) ) { if ($row[$hiddenField]) { $value = 0; $label = 'unHide'; } else { $value = 1; $label = 'hide'; } $params = '&data[tt_content][' . ($row['_ORIG_uid'] ? $row['_ORIG_uid'] : $row['uid']) . '][' . $hiddenField . ']=' . $value; $out .= '' . $this->iconFactory->getIcon('actions-edit-' . strtolower($label), Icon::SIZE_SMALL)->render() . ''; } // Delete $disableDelete = (bool)\trim( $backendUser->getTSConfig()['options.']['disableDelete.']['tt_content'] ?? $backendUser->getTSConfig()['options.']['disableDelete'] ?? '0' ); if (!$disableDelete) { $params = '&cmd[tt_content][' . $row['uid'] . '][delete]=1'; $refCountMsg = BackendUtility::referenceCount( 'tt_content', $row['uid'], ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'), $this->getReferenceCount('tt_content', $row['uid']) ) . BackendUtility::translationCount( 'tt_content', $row['uid'], ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord') ); $confirm = $this->getLanguageService()->getLL('deleteWarning') . $refCountMsg; $out .= '' . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . ''; if ($out && $backendUser->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)) { $out = '
' . $out . '
'; } else { $out = ''; } } if (!$disableMoveAndNewButtons) { $moveButtonContent = ''; $displayMoveButtons = false; // Move element up: if ($this->tt_contentData['prev'][$row['uid']]) { $params = '&cmd[tt_content][' . $row['uid'] . '][move]=' . $this->tt_contentData['prev'][$row['uid']]; $moveButtonContent .= '' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . ''; if (!$dragDropEnabled) { $displayMoveButtons = true; } } else { $moveButtonContent .= '' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . ''; } // Move element down: if ($this->tt_contentData['next'][$row['uid']]) { $params = '&cmd[tt_content][' . $row['uid'] . '][move]= ' . $this->tt_contentData['next'][$row['uid']]; $moveButtonContent .= '' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . ''; if (!$dragDropEnabled) { $displayMoveButtons = true; } } else { $moveButtonContent .= '' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . ''; } if ($displayMoveButtons) { $out .= '
' . $moveButtonContent . '
'; } } } } $allowDragAndDrop = $this->isDragAndDropAllowed($row); $additionalIcons = []; $additionalIcons[] = $this->getIcon('tt_content', $row) . ' '; if ($langMode && isset($this->siteLanguages[(int)$row['sys_language_uid']])) { $additionalIcons[] = $this->renderLanguageFlag($this->siteLanguages[(int)$row['sys_language_uid']]); } // Get record locking status: if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) { $additionalIcons[] = '' . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . ''; } // Call stats information hook $_params = ['tt_content', $row['uid'], &$row]; foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [] as $_funcRef) { $additionalIcons[] = GeneralUtility::callUserFunction($_funcRef, $_params, $this); } // Wrap the whole header // NOTE: end-tag for
is in getTable_tt_content() return '
' . implode('', $additionalIcons) . '
' . ($out ? '
' . $out . '
' : '') . '
'; } /** * Gets the number of records referencing the record with the UID $uid in * the table $tableName. * * @param string $tableName * @param int $uid * @return int The number of references to record $uid in table */ protected function getReferenceCount(string $tableName, int $uid): int { if (!isset($this->referenceCount[$tableName][$uid])) { $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class); $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid); $this->referenceCount[$tableName][$uid] = $numberOfReferences; } return $this->referenceCount[$tableName][$uid]; } /** * Determine whether Drag & Drop should be allowed * * @param array $row * @return bool */ protected function isDragAndDropAllowed(array $row) { if ((int)$row['l18n_parent'] === 0 && ( $this->getBackendUser()->isAdmin() || ((int)$row['editlock'] === 0 && (int)$this->pageinfo['editlock'] === 0) && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT) && $this->getBackendUser()->checkAuthMode('tt_content', 'CType', $row['CType'], $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode']) ) ) { return true; } return false; } /** * Draws the preview content for a content element * * @param array $row Content element * @return string HTML * @throws \UnexpectedValueException */ public function tt_content_drawItem($row) { $out = ''; $outHeader = ''; // Make header: if ($row['header']) { $hiddenHeaderNote = ''; // If header layout is set to 'hidden', display an accordant note: if ($row['header_layout'] == 100) { $hiddenHeaderNote = ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']'; } $outHeader = $row['date'] ? htmlspecialchars($this->itemLabels['date'] . ' ' . BackendUtility::date($row['date'])) . '
' : ''; $outHeader .= '' . $this->linkEditContent($this->renderText($row['header']), $row) . $hiddenHeaderNote . '
'; } // Make content: $drawItem = true; // Hook: Render an own preview of a record foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) { $hookObject = GeneralUtility::makeInstance($className); if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) { throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1218547409); } $hookObject->preProcess($this, $drawItem, $outHeader, $out, $row); } // If the previous hook did not render something, // then check if a Fluid-based preview template was defined for this CType // and render it via Fluid. Possible option: // mod.web_layout.tt_content.preview.media = EXT:site_mysite/Resources/Private/Templates/Preview/Media.html if ($drawItem) { $tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? []; $fluidTemplateFile = ''; if ($row['CType'] === 'list' && !empty($row['list_type']) && !empty($tsConfig['list.'][$row['list_type']]) ) { $fluidTemplateFile = $tsConfig['list.'][$row['list_type']]; } elseif (!empty($tsConfig[$row['CType']])) { $fluidTemplateFile = $tsConfig[$row['CType']]; } if ($fluidTemplateFile) { $fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile); if ($fluidTemplateFile) { try { $view = GeneralUtility::makeInstance(StandaloneView::class); $view->setTemplatePathAndFilename($fluidTemplateFile); $view->assignMultiple($row); if (!empty($row['pi_flexform'])) { $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform'])); } $out = $view->render(); $drawItem = false; } catch (\Exception $e) { $this->logger->warning(sprintf( 'The backend preview for content element %d can not be rendered using the Fluid template file "%s": %s', $row['uid'], $fluidTemplateFile, $e->getMessage() )); } } } } // Draw preview of the item depending on its CType (if not disabled by previous hook): if ($drawItem) { switch ($row['CType']) { case 'header': if ($row['subheader']) { $out .= $this->linkEditContent($this->renderText($row['subheader']), $row) . '
'; } break; case 'bullets': case 'table': if ($row['bodytext']) { $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '
'; } break; case 'uploads': if ($row['media']) { $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'media'), $row) . '
'; } break; case 'menu': $contentType = $this->CType_labels[$row['CType']]; $out .= $this->linkEditContent('' . htmlspecialchars($contentType) . '', $row) . '
'; // Add Menu Type $menuTypeLabel = $this->getLanguageService()->sL( BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'menu_type', $row['menu_type']) ); $menuTypeLabel = $menuTypeLabel ?: 'invalid menu type'; $out .= $this->linkEditContent(htmlspecialchars($menuTypeLabel), $row); if ($row['menu_type'] !== '2' && ($row['pages'] || $row['selected_categories'])) { // Show pages if menu type is not "Sitemap" $out .= ':' . $this->linkEditContent($this->generateListForCTypeMenu($row), $row) . '
'; } break; case 'shortcut': if (!empty($row['records'])) { $shortcutContent = []; $recordList = explode(',', $row['records']); foreach ($recordList as $recordIdentifier) { $split = BackendUtility::splitTable_Uid($recordIdentifier); $tableName = empty($split[0]) ? 'tt_content' : $split[0]; $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]); if (is_array($shortcutRecord)) { $icon = $this->iconFactory->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render(); $icon = BackendUtility::wrapClickMenuOnIcon( $icon, $tableName, $shortcutRecord['uid'] ); $shortcutContent[] = $icon . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord)); } } $out .= implode('
', $shortcutContent) . '
'; } break; case 'list': $hookOut = ''; $_params = ['pObj' => &$this, 'row' => $row, 'infoArr' => []]; foreach ( $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$row['list_type']] ?? $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ?? [] as $_funcRef ) { $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $this); } if ((string)$hookOut !== '') { $out .= $hookOut; } elseif (!empty($row['list_type'])) { $label = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'list_type', $row['list_type']); if (!empty($label)) { $out .= $this->linkEditContent('' . htmlspecialchars($this->getLanguageService()->sL($label)) . '', $row) . '
'; } else { $message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['list_type']); $out .= '' . htmlspecialchars($message) . ''; } } else { $out .= '' . $this->getLanguageService()->getLL('noPluginSelected') . ''; } $out .= htmlspecialchars($this->getLanguageService()->sL( BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages']) )) . '
'; break; default: $contentType = $this->CType_labels[$row['CType']]; if (!isset($contentType)) { $contentType = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'CType', $row['CType']); } if ($contentType) { $out .= $this->linkEditContent('' . htmlspecialchars($contentType) . '', $row) . '
'; if ($row['bodytext']) { $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '
'; } if ($row['image']) { $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'image'), $row) . '
'; } } else { $message = sprintf( $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['CType'] ); $out .= '' . htmlspecialchars($message) . ''; } } } // Wrap span-tags: $out = ' ' . $out . ''; // Add header: $out = $outHeader . $out; // Return values: if ($this->isDisabled('tt_content', $row)) { return '' . $out . ''; } return $out; } /** * Generates a list of selected pages or categories for the CType menu * * @param array $row row from pages * @return string */ protected function generateListForCTypeMenu(array $row) { $table = 'pages'; $field = 'pages'; // get categories instead of pages if (strpos($row['menu_type'], 'categorized_') !== false) { $table = 'sys_category'; $field = 'selected_categories'; } if (trim($row[$field]) === '') { return ''; } $content = ''; $uidList = explode(',', $row[$field]); foreach ($uidList as $uid) { $uid = (int)$uid; $record = BackendUtility::getRecord($table, $uid, 'title'); $content .= '
' . htmlspecialchars($record['title']) . ' (' . $uid . ')'; } return $content; } /** * Filters out all tt_content uids which are already translated so only non-translated uids is left. * Selects across columns, but within in the same PID. Columns are expect to be the same * for translations and original but this may be a conceptual error (?) * * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language * @param int $id Page pid * @param int $lP Sys language UID * @return array Modified $defLanguageCount */ public function getNonTranslatedTTcontentUids($defaultLanguageUids, $id, $lP) { if ($lP && !empty($defaultLanguageUids)) { // Select all translations here: $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, null, false)); $queryBuilder ->select('*') ->from('tt_content') ->where( $queryBuilder->expr()->eq( 'sys_language_uid', $queryBuilder->createNamedParameter($lP, \PDO::PARAM_INT) ), $queryBuilder->expr()->in( 'l18n_parent', $queryBuilder->createNamedParameter($defaultLanguageUids, Connection::PARAM_INT_ARRAY) ) ); $result = $queryBuilder->execute(); // Flip uids: $defaultLanguageUids = array_flip($defaultLanguageUids); // Traverse any selected elements and unset original UID if any: while ($row = $result->fetch()) { BackendUtility::workspaceOL('tt_content', $row); unset($defaultLanguageUids[$row['l18n_parent']]); } // Flip again: $defaultLanguageUids = array_keys($defaultLanguageUids); } return $defaultLanguageUids; } /** * Creates button which is used to create copies of records.. * * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language * @param int $lP Sys language UID * @return string "Copy languages" button, if available. */ public function newLanguageButton($defaultLanguageUids, $lP) { $lP = (int)$lP; if (!$this->doEdit || !$lP) { return ''; } $theNewButton = ''; $localizationTsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['localization.'] ?? []; $allowCopy = (bool)($localizationTsConfig['enableCopy'] ?? true); $allowTranslate = (bool)($localizationTsConfig['enableTranslate'] ?? true); if (!empty($this->languageHasTranslationsCache[$lP])) { if (isset($this->languageHasTranslationsCache[$lP]['hasStandAloneContent'])) { $allowTranslate = false; } if (isset($this->languageHasTranslationsCache[$lP]['hasTranslations'])) { $allowCopy = $allowCopy && !$this->languageHasTranslationsCache[$lP]['hasTranslations']; } } if (isset($this->contentElementCache[$lP]) && is_array($this->contentElementCache[$lP])) { foreach ($this->contentElementCache[$lP] as $column => $records) { foreach ($records as $record) { $key = array_search($record['l10n_source'], $defaultLanguageUids); if ($key !== false) { unset($defaultLanguageUids[$key]); } } } } if (!empty($defaultLanguageUids)) { $theNewButton = '' . $this->iconFactory->getIcon('actions-localize', Icon::SIZE_SMALL)->render() . ' ' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . ''; } return $theNewButton; } /** * Creates onclick-attribute content for a new content element * * @param int $id Page id where to create the element. * @param int $colPos Preset: Column position value * @param int $sys_language Preset: Sys language value * @return string String for onclick attribute. * @see getTable_tt_content() */ public function newContentElementOnClick($id, $colPos, $sys_language) { if ($this->option_newWizard) { $routeName = BackendUtility::getPagesTSconfig($id)['mod.']['newContentElementWizard.']['override'] ?? 'new_content_element_wizard'; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = $uriBuilder->buildUriFromRoute($routeName, [ 'id' => $id, 'colPos' => $colPos, 'sys_language_uid' => $sys_language, 'uid_pid' => $id, 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') ]); $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$url) . ';'; } else { $onClick = BackendUtility::editOnClick('&edit[tt_content][' . $id . ']=new&defVals[tt_content][colPos]=' . $colPos . '&defVals[tt_content][sys_language_uid]=' . $sys_language); } return $onClick; } /** * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE. * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button * * @param string $str String to link. Must be prepared for HTML output. * @param array $row The row. * @return string If the whole thing was editable ($this->doEdit) $str is return with link around. Otherwise just $str. * @see getTable_tt_content() */ public function linkEditContent($str, $row) { if ($this->doEdit && $this->getBackendUser()->recordEditAccessInternals('tt_content', $row)) { $urlParameters = [ 'edit' => [ 'tt_content' => [ $row['uid'] => 'edit' ] ], 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid'] ]; $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); // Return link return '' . $str . ''; } return $str; } /** * Make selector box for creating new translation in a language * Displays only languages which are not yet present for the current page and * that are not disabled with page TS. * * @param int $id Page id for which to create a new translation record of pages * @return string ' . $output . '
'; } return ''; } /** * Traverse the result pointer given, adding each record to array and setting some internal values at the same time. * * @param Statement $result DBAL Statement * @param string $table Table name defaulting to tt_content * @return array The selected rows returned in this array. */ public function getResult(Statement $result, string $table = 'tt_content'): array { $output = []; // Traverse the result: while ($row = $result->fetch()) { BackendUtility::workspaceOL($table, $row, -99, true); if ($row) { // Add the row to the array: $output[] = $row; } } $this->generateTtContentDataArray($output); // Return selected records return $output; } /******************************** * * Various helper functions * ********************************/ /** * Initializes the clipboard for generating paste links * * * @see \TYPO3\CMS\Backend\Controller\ContextMenuController::clipboardAction() * @see \TYPO3\CMS\Filelist\Controller\FileListController::indexAction() */ protected function initializeClipboard() { // Start clipboard $this->clipboard = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Clipboard\Clipboard::class); // Initialize - reads the clipboard content from the user session $this->clipboard->initializeClipboard(); // This locks the clipboard to the Normal for this request. $this->clipboard->lockToNormal(); // Clean up pad $this->clipboard->cleanCurrent(); // Save the clipboard content $this->clipboard->endClipboard(); } /** * Generates the data for previous and next elements which is needed for movements. * * @param array $rowArray */ protected function generateTtContentDataArray(array $rowArray) { if (empty($this->tt_contentData)) { $this->tt_contentData = [ 'nextThree' => [], 'next' => [], 'prev' => [], ]; } foreach ($rowArray as $key => $value) { // Create the list of the next three ids (for editing links...) for ($i = 0; $i < $this->nextThree; $i++) { if (isset($rowArray[$key - $i]) && !GeneralUtility::inList($this->tt_contentData['nextThree'][$rowArray[$key - $i]['uid']], $value['uid']) ) { $this->tt_contentData['nextThree'][$rowArray[$key - $i]['uid']] .= $value['uid'] . ','; } } // Create information for next and previous content elements if (isset($rowArray[$key - 1])) { if (isset($rowArray[$key - 2])) { $this->tt_contentData['prev'][$value['uid']] = -$rowArray[$key - 2]['uid']; } else { $this->tt_contentData['prev'][$value['uid']] = $value['pid']; } $this->tt_contentData['next'][$rowArray[$key - 1]['uid']] = -$value['uid']; } } } /** * Counts and returns the number of records on the page with $pid * * @param string $table Table name * @param int $pid Page id * @return int Number of records. */ public function numberOfRecords($table, $pid) { $count = 0; if ($GLOBALS['TCA'][$table]) { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($table); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $count = (int)$queryBuilder->count('uid') ->from($table) ->where( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)) ) ->execute() ->fetchColumn(); } return $count; } /** * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc. * * @param string $input Input string * @return string Output string */ public function renderText($input) { $input = strip_tags($input); $input = GeneralUtility::fixed_lgd_cs($input, 1500); return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false)); } /** * Creates the icon image tag for record from table and wraps it in a link which will trigger the click menu. * * @param string $table Table name * @param array $row Record array * @param string $enabledClickMenuItems Passthrough to wrapClickMenuOnIcon * @return string HTML for the icon */ public function getIcon($table, $row, $enabledClickMenuItems = '') { // Initialization $toolTip = BackendUtility::getRecordToolTip($row, 'tt_content'); $icon = '' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . ''; $this->counter++; // The icon with link if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) { $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']); } return $icon; } /** * Creates processed values for all field names in $fieldList based on values from $row array. * The result is 'returned' through $info which is passed as a reference * * @param string $table Table name * @param string $fieldList Comma separated list of fields. * @param array $row Record from which to take values for processing. * @param array $info Array to which the processed values are added. */ public function getProcessedValue($table, $fieldList, array $row, array &$info) { // Splitting values from $fieldList $fieldArr = explode(',', $fieldList); // Traverse fields from $fieldList foreach ($fieldArr as $field) { if ($row[$field]) { $info[] = '' . htmlspecialchars($this->itemLabels[$field]) . ' ' . htmlspecialchars(BackendUtility::getProcessedValue($table, $field, $row[$field])); } } } /** * Returns TRUE, if the record given as parameters is NOT visible based on hidden/starttime/endtime (if available) * * @param string $table Tablename of table to test * @param array $row Record row. * @return bool Returns TRUE, if disabled. */ public function isDisabled($table, $row) { $enableCols = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']; return $enableCols['disabled'] && $row[$enableCols['disabled']] || $enableCols['starttime'] && $row[$enableCols['starttime']] > $GLOBALS['EXEC_TIME'] || $enableCols['endtime'] && $row[$enableCols['endtime']] && $row[$enableCols['endtime']] < $GLOBALS['EXEC_TIME']; } /** * Returns icon for "no-edit" of a record. * Basically, the point is to signal that this record could have had an edit link if * the circumstances were right. A placeholder for the regular edit icon... * * @param string $label Label key from LOCAL_LANG * @return string IMG tag for icon. */ public function noEditIcon($label = 'noEditItems') { $title = htmlspecialchars($this->getLanguageService()->getLL($label)); return '' . $this->iconFactory->getIcon('status-edit-read-only', Icon::SIZE_SMALL)->render() . ''; } /***************************************** * * External renderings * *****************************************/ /** * Creates a menu of the tables that can be listed by this function * Only tables which has records on the page will be included. * Notice: The function also fills in the internal variable $this->activeTables with icon/titles. * * @param int $id Page id from which we are listing records (the function will look up if there are records on the page) * @return string HTML output. */ public function getTableMenu($id) { // Initialize: $this->activeTables = []; $theTables = ['tt_content']; // External tables: if (is_array($this->externalTables)) { $theTables = array_unique(array_merge($theTables, array_keys($this->externalTables))); } $out = ''; // Traverse tables to check: foreach ($theTables as $tName) { // Check access and whether the proper extensions are loaded: if ($this->getBackendUser()->check('tables_select', $tName) && ( isset($this->externalTables[$tName]) || $tName === 'fe_users' || $tName === 'tt_content' || \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded($tName) ) ) { // Make query to count records from page: $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($tName); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $count = $queryBuilder->count('uid') ->from($tName) ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))) ->execute() ->fetchColumn(); // If records were found (or if "tt_content" is the table...): if ($count || $tName === 'tt_content') { // Add row to menu: $out .= ' ' . $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render() . ''; // ... and to the internal array, activeTables we also add table icon and title (for use elsewhere) $title = htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title'])) . ': ' . $count . ' ' . htmlspecialchars($this->getLanguageService()->getLL('records')); $this->activeTables[$tName] = '' . $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render() . '' . ' ' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title'])); } } } // Wrap cells in table tags: $out = ' ' . $out . '
'; // Return the content: return $out; } /** * Create thumbnail code for record/field but not linked * * @param mixed[] $row Record array * @param string $table Table (record is from) * @param string $field Field name for which thumbnail are to be rendered. * @return string HTML for thumbnails, if any. */ public function getThumbCodeUnlinked($row, $table, $field) { return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false); } /** * Checks whether translated Content Elements exist in the desired language * If so, deny creating new ones via the UI * * @param array $contentElements * @param int $language * @return bool */ protected function checkIfTranslationsExistInLanguage(array $contentElements, int $language) { // If in default language, you may always create new entries // Also, you may override this strict behavior via user TS Config // If you do so, you're on your own and cannot rely on any support by the TYPO3 core // We jump out here since we don't need to do the expensive loop operations $allowInconsistentLanguageHandling = (bool)(BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['allowInconsistentLanguageHandling'] ?? false); if ($language === 0 || $allowInconsistentLanguageHandling) { return false; } /** * Build up caches */ if (!isset($this->languageHasTranslationsCache[$language])) { foreach ($contentElements as $columns) { foreach ($columns as $contentElement) { if ((int)$contentElement['l18n_parent'] === 0) { $this->languageHasTranslationsCache[$language]['hasStandAloneContent'] = true; $this->languageHasTranslationsCache[$language]['mode'] = 'free'; } if ((int)$contentElement['l18n_parent'] > 0) { $this->languageHasTranslationsCache[$language]['hasTranslations'] = true; $this->languageHasTranslationsCache[$language]['mode'] = 'connected'; } } } if (!isset($this->languageHasTranslationsCache[$language])) { $this->languageHasTranslationsCache[$language]['hasTranslations'] = false; } // Check whether we have a mix of both if (isset($this->languageHasTranslationsCache[$language]['hasStandAloneContent']) && $this->languageHasTranslationsCache[$language]['hasTranslations'] ) { $this->languageHasTranslationsCache[$language]['mode'] = 'mixed'; $siteLanguage = $this->siteLanguages[$language]; $message = GeneralUtility::makeInstance( FlashMessage::class, sprintf($this->getLanguageService()->getLL('staleTranslationWarning'), $siteLanguage->getTitle()), sprintf($this->getLanguageService()->getLL('staleTranslationWarningTitle'), $siteLanguage->getTitle()), FlashMessage::WARNING ); $service = GeneralUtility::makeInstance(FlashMessageService::class); $queue = $service->getMessageQueueByIdentifier(); $queue->addMessage($message); } } return $this->languageHasTranslationsCache[$language]['hasTranslations']; } /** * @return BackendLayoutView */ protected function getBackendLayoutView() { return GeneralUtility::makeInstance(BackendLayoutView::class); } /** * @return BackendUserAuthentication */ protected function getBackendUser() { return $GLOBALS['BE_USER']; } /** * @return PageLayoutController */ protected function getPageLayoutController() { return $GLOBALS['SOBE']; } /** * Initializes the list generation * * @param int $id Page id for which the list is rendered. Must be >= 0 * @param string $table Tablename - if extended mode where only one table is listed at a time. * @param int $pointer Browsing pointer. * @param string $search Search word, if any * @param int $levels Number of levels to search down the page tree * @param int $showLimit Limit of records to be listed. * @throws SiteNotFoundException */ public function start($id, $table, $pointer, $search = '', $levels = 0, $showLimit = 0) { $this->resolveSiteLanguages((int)$id); $backendUser = $this->getBackendUser(); // Setting internal variables: // sets the parent id $this->id = (int)$id; if ($GLOBALS['TCA'][$table]) { // Setting single table mode, if table exists: $this->table = $table; } $this->firstElementNumber = $pointer; $this->searchString = trim($search); $this->searchLevels = (int)$levels; $this->showLimit = MathUtility::forceIntegerInRange($showLimit, 0, 10000); // Setting GPvars: $this->csvOutput = (bool)GeneralUtility::_GP('csv'); $this->sortField = GeneralUtility::_GP('sortField'); $this->sortRev = GeneralUtility::_GP('sortRev'); $this->displayFields = GeneralUtility::_GP('displayFields'); $this->duplicateField = GeneralUtility::_GP('duplicateField'); if (GeneralUtility::_GP('justLocalized')) { $this->localizationRedirect(GeneralUtility::_GP('justLocalized')); } // Init dynamic vars: $this->counter = 0; $this->JScode = ''; $this->HTMLcode = ''; // Limits if (isset($this->modTSconfig['properties']['itemsLimitPerTable'])) { $this->itemsLimitPerTable = MathUtility::forceIntegerInRange( (int)$this->modTSconfig['properties']['itemsLimitPerTable'], 1, 10000 ); } if (isset($this->modTSconfig['properties']['itemsLimitSingleTable'])) { $this->itemsLimitSingleTable = MathUtility::forceIntegerInRange( (int)$this->modTSconfig['properties']['itemsLimitSingleTable'], 1, 10000 ); } // $table might be NULL at this point in the code. As the expressionBuilder // is used to limit returned records based on the page permissions and the // uid field of the pages it can hardcoded to work on the pages table. $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('pages') ->expr(); $permsClause = $expressionBuilder->andX($backendUser->getPagePermsClause(Permission::PAGE_SHOW)); // This will hide records from display - it has nothing to do with user rights!! $pidList = GeneralUtility::intExplode(',', $backendUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? '', true); if (!empty($pidList)) { $permsClause->add($expressionBuilder->notIn('pages.uid', $pidList)); } $this->perms_clause = (string)$permsClause; // Get configuration of collapsed tables from user uc and merge with sanitized GP vars $this->tablesCollapsed = is_array($backendUser->uc['moduleData']['list']) ? $backendUser->uc['moduleData']['list'] : []; $collapseOverride = GeneralUtility::_GP('collapse'); if (is_array($collapseOverride)) { foreach ($collapseOverride as $collapseTable => $collapseValue) { if (is_array($GLOBALS['TCA'][$collapseTable]) && ($collapseValue == 0 || $collapseValue == 1)) { $this->tablesCollapsed[$collapseTable] = $collapseValue; } } // Save modified user uc $backendUser->uc['moduleData']['list'] = $this->tablesCollapsed; $backendUser->writeUC($backendUser->uc); $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl')); if ($returnUrl !== '') { HttpUtility::redirect($returnUrl); } } $this->initializeLanguages(); } /** * Traverses the table(s) to be listed and renders the output code for each: * The HTML is accumulated in $this->HTMLcode * Finishes off with a stopper-gif */ public function generateList() { // Set page record in header $this->pageRecord = BackendUtility::getRecordWSOL('pages', $this->id); $hideTablesArray = GeneralUtility::trimExplode(',', $this->hideTables); $backendUser = $this->getBackendUser(); // pre-process tables and add sorting instructions $tableNames = array_flip(array_keys($GLOBALS['TCA'])); foreach ($tableNames as $tableName => &$config) { $hideTable = false; // Checking if the table should be rendered: // Checks that we see only permitted/requested tables: if ($this->table && $tableName !== $this->table || $this->tableList && !GeneralUtility::inList($this->tableList, $tableName) || !$backendUser->check('tables_select', $tableName) ) { $hideTable = true; } if (!$hideTable) { // Don't show table if hidden by TCA ctrl section // Don't show table if hidden by pageTSconfig mod.web_list.hideTables $hideTable = $hideTable || !empty($GLOBALS['TCA'][$tableName]['ctrl']['hideTable']) || in_array($tableName, $hideTablesArray, true) || in_array('*', $hideTablesArray, true); // Override previous selection if table is enabled or hidden by TSconfig TCA override mod.web_list.table if (isset($this->tableTSconfigOverTCA[$tableName . '.']['hideTable'])) { $hideTable = (bool)$this->tableTSconfigOverTCA[$tableName . '.']['hideTable']; } } if ($hideTable) { unset($tableNames[$tableName]); } else { if (isset($this->tableDisplayOrder[$tableName])) { // Copy display order information $tableNames[$tableName] = $this->tableDisplayOrder[$tableName]; } else { $tableNames[$tableName] = []; } } } unset($config); $orderedTableNames = GeneralUtility::makeInstance(DependencyOrderingService::class) ->orderByDependencies($tableNames); foreach ($orderedTableNames as $tableName => $_) { // check if we are in single- or multi-table mode if ($this->table) { $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems']) ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems'] : $this->itemsLimitSingleTable; } else { // if there are no records in table continue current foreach $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($tableName); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $queryBuilder = $this->addPageIdConstraint($tableName, $queryBuilder); $firstRow = $queryBuilder->select('uid') ->from($tableName) ->execute() ->fetch(); if (!is_array($firstRow)) { continue; } $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxDBListItems']) ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxDBListItems'] : $this->itemsLimitPerTable; } if ($this->showLimit) { $this->iLimit = $this->showLimit; } // Setting fields to select: if ($this->allFields) { $fields = $this->makeFieldList($tableName); $fields[] = 'tstamp'; $fields[] = 'crdate'; $fields[] = '_PATH_'; $fields[] = '_CONTROL_'; if (is_array($this->setFields[$tableName])) { $fields = array_intersect($fields, $this->setFields[$tableName]); } else { $fields = []; } } else { $fields = []; } // Finally, render the list: $this->HTMLcode .= $this->getTable($tableName, $this->id, implode(',', $fields)); } } /** * Creates the search box * * @param bool $formFields If TRUE, the search box is wrapped in its own form-tags * @return string HTML for the search box */ public function getSearchBox($formFields = true) { $lang = $this->getLanguageService(); // Setting form-elements, if applicable: $formElements = ['', '']; if ($formFields) { $formElements = [ '
', '
' ]; } // Make level selector: $opt = []; // "New" generation of search levels ... based on TS config $config = BackendUtility::getPagesTSconfig($this->id); $searchLevelsFromTSconfig = $config['mod.']['web_list.']['searchLevel.']['items.']; $searchLevelItems = []; // get translated labels for search levels from pagets foreach ($searchLevelsFromTSconfig as $keySearchLevel => $labelConfigured) { $label = $lang->sL('LLL:' . $labelConfigured); if ($label === '') { $label = $labelConfigured; } $searchLevelItems[$keySearchLevel] = $label; } foreach ($searchLevelItems as $kv => $label) { $opt[] = ''; } $lMenu = ''; // Table with the search box: $content = ''; return $content; } /** * Setting the field names to display in extended list. * Sets the internal variable $this->setFields */ public function setDispFields() { $backendUser = $this->getBackendUser(); // Getting from session: $dispFields = $backendUser->getModuleData('list/displayFields'); // If fields has been inputted, then set those as the value and push it to session variable: if (is_array($this->displayFields)) { reset($this->displayFields); $tKey = key($this->displayFields); $dispFields[$tKey] = $this->displayFields[$tKey]; $backendUser->pushModuleData('list/displayFields', $dispFields); } // Setting result: $this->setFields = $dispFields; } /** * Create thumbnail code for record/field * * @param mixed[] $row Record array * @param string $table Table (record is from) * @param string $field Field name for which thumbnail are to be rendered. * @return string HTML for thumbnails, if any. */ public function thumbCode($row, $table, $field) { return BackendUtility::thumbCode($row, $table, $field); } /** * Returns a QueryBuilder configured to select $fields from $table where the pid is restricted * depending on the current searchlevel setting. * * @param string $table Table name * @param int $pageId Page id Only used to build the search constraints, getPageIdConstraint() used for restrictions * @param string[] $additionalConstraints Additional part for where clause * @param string[] $fields Field list to select, * for all * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder */ public function getQueryBuilder( string $table, int $pageId, array $additionalConstraints = [], array $fields = ['*'] ): QueryBuilder { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($table); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $queryBuilder ->select(...$fields) ->from($table); if (!empty($additionalConstraints)) { $queryBuilder->andWhere(...$additionalConstraints); } $queryBuilder = $this->prepareQueryBuilder($table, $pageId, $fields, $additionalConstraints, $queryBuilder); return $queryBuilder; } /** * Return the modified QueryBuilder object ($queryBuilder) which will be * used to select the records from a table $table with pid = $this->pidList * * @param string $table Table name * @param int $pageId Page id Only used to build the search constraints, $this->pidList is used for restrictions * @param string[] $fieldList List of fields to select from the table * @param string[] $additionalConstraints Additional part for where clause * @param QueryBuilder $queryBuilder * @param bool $addSorting * @return QueryBuilder */ protected function prepareQueryBuilder( string $table, int $pageId, array $fieldList = ['*'], array $additionalConstraints = [], QueryBuilder $queryBuilder, bool $addSorting = true ): QueryBuilder { $parameters = [ 'table' => $table, 'fields' => $fieldList, 'groupBy' => null, 'orderBy' => null, 'firstResult' => $this->firstElementNumber ?: null, 'maxResults' => $this->iLimit ?: null ]; if ($this->iLimit > 0) { $queryBuilder->setMaxResults($this->iLimit); } if ($addSorting) { if ($this->sortField && in_array($this->sortField, $this->makeFieldList($table, 1))) { $queryBuilder->orderBy($this->sortField, $this->sortRev ? 'DESC' : 'ASC'); } else { $orderBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?: $GLOBALS['TCA'][$table]['ctrl']['default_sortby']; $orderBys = QueryHelper::parseOrderBy((string)$orderBy); foreach ($orderBys as $orderBy) { $queryBuilder->addOrderBy($orderBy[0], $orderBy[1]); } } } // Build the query constraints $queryBuilder = $this->addPageIdConstraint($table, $queryBuilder); $searchWhere = $this->makeSearchString($table, $pageId); if (!empty($searchWhere)) { $queryBuilder->andWhere($searchWhere); } // Filtering on displayable pages (permissions): if ($table === 'pages' && $this->perms_clause) { $queryBuilder->andWhere($this->perms_clause); } // Filter out records that are translated, if TSconfig mod.web_list.hideTranslations is set if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) && (GeneralUtility::inList($this->hideTranslations, $table) || $this->hideTranslations === '*') ) { $queryBuilder->andWhere( $queryBuilder->expr()->eq( $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 0 ) ); } $hookName = DatabaseRecordList::class; foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['buildQueryParameters'] ?? [] as $className) { // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, the modifyQuery hook should be used instead. trigger_error('The hook ($GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][' . $hookName . '][\'buildQueryParameters\']) will be removed in TYPO3 v10.0, the modifyQuery hook should be used instead.', E_USER_DEPRECATED); $hookObject = GeneralUtility::makeInstance($className); if (method_exists($hookObject, 'buildQueryParametersPostProcess')) { $hookObject->buildQueryParametersPostProcess( $parameters, $table, $pageId, $additionalConstraints, $fieldList, $this, $queryBuilder ); } } foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery'] ?? [] as $className) { $hookObject = GeneralUtility::makeInstance($className); if (method_exists($hookObject, 'modifyQuery')) { $hookObject->modifyQuery( $parameters, $table, $pageId, $additionalConstraints, $fieldList, $queryBuilder ); } } // array_unique / array_filter used to eliminate empty and duplicate constraints // the array keys are eliminated by this as well to facilitate argument unpacking // when used with the querybuilder. // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 if (!empty($parameters['where'])) { $parameters['where'] = array_unique(array_filter(array_values($parameters['where']))); } if (!empty($parameters['where'])) { $this->logDeprecation('where'); $queryBuilder->where(...$parameters['where']); } if (!empty($parameters['orderBy'])) { $this->logDeprecation('orderBy'); foreach ($parameters['orderBy'] as $fieldNameAndSorting) { list($fieldName, $sorting) = $fieldNameAndSorting; $queryBuilder->addOrderBy($fieldName, $sorting); } } if (!empty($parameters['firstResult'])) { $this->logDeprecation('firstResult'); $queryBuilder->setFirstResult((int)$parameters['firstResult']); } if (!empty($parameters['maxResults']) && $parameters['maxResults'] !== $this->iLimit) { $this->logDeprecation('maxResults'); $queryBuilder->setMaxResults((int)$parameters['maxResults']); } if (!empty($parameters['groupBy'])) { $this->logDeprecation('groupBy'); $queryBuilder->groupBy($parameters['groupBy']); } return $queryBuilder; } /** * Executed a query to set $this->totalItems to the number of total * items, eg. for pagination * * @param string $table Table name * @param int $pageId Only used to build the search constraints, $this->pidList is used for restrictions * @param array $constraints Additional constraints for where clause */ public function setTotalItems(string $table, int $pageId, array $constraints) { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($table); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $queryBuilder ->from($table); if (!empty($constraints)) { $queryBuilder->andWhere(...$constraints); } $queryBuilder = $this->prepareQueryBuilder($table, $pageId, ['*'], $constraints, $queryBuilder, false); // Reset limit and offset for full count query $queryBuilder->setFirstResult(0); $queryBuilder->setMaxResults(1); $this->totalItems = (int)$queryBuilder->count('*') ->execute() ->fetchColumn(); } /** * Creates part of query for searching after a word ($this->searchString) * fields in input table. * * @param string $table Table, in which the fields are being searched. * @param int $currentPid Page id for the possible search limit. -1 only if called from an old XCLASS. * @return string Returns part of WHERE-clause for searching, if applicable. */ public function makeSearchString($table, $currentPid = -1) { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $expressionBuilder = $queryBuilder->expr(); $constraints = []; $currentPid = (int)$currentPid; $tablePidField = $table === 'pages' ? 'uid' : 'pid'; // Make query only if table is valid and a search string is actually defined if (empty($this->searchString)) { return ''; } $searchableFields = $this->getSearchFields($table); if (MathUtility::canBeInterpretedAsInteger($this->searchString)) { $constraints[] = $expressionBuilder->eq('uid', (int)$this->searchString); foreach ($searchableFields as $fieldName) { if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) { continue; } $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']; $fieldType = $fieldConfig['type']; $evalRules = $fieldConfig['eval'] ?: ''; if ($fieldType === 'input' && $evalRules && GeneralUtility::inList($evalRules, 'int')) { if (!isset($fieldConfig['search']['pidonly']) || ($fieldConfig['search']['pidonly'] && $currentPid > 0) ) { $constraints[] = $expressionBuilder->andX( $expressionBuilder->eq($fieldName, (int)$this->searchString), $expressionBuilder->eq($tablePidField, (int)$currentPid) ); } } elseif ($fieldType === 'text' || $fieldType === 'flex' || ($fieldType === 'input' && (!$evalRules || !preg_match('/\b(?:date|time|int)\b/', $evalRules))) ) { $constraints[] = $expressionBuilder->like( $fieldName, $queryBuilder->quote('%' . (int)$this->searchString . '%') ); } } } elseif (!empty($searchableFields)) { $like = $queryBuilder->quote('%' . $queryBuilder->escapeLikeWildcards($this->searchString) . '%'); foreach ($searchableFields as $fieldName) { if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) { continue; } $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']; $fieldType = $fieldConfig['type']; $evalRules = $fieldConfig['eval'] ?: ''; $searchConstraint = $expressionBuilder->andX( $expressionBuilder->comparison( 'LOWER(' . $queryBuilder->quoteIdentifier($fieldName) . ')', 'LIKE', 'LOWER(' . $like . ')' ) ); if (is_array($fieldConfig['search'])) { $searchConfig = $fieldConfig['search']; if (in_array('case', $searchConfig)) { // Replace case insensitive default constraint $searchConstraint = $expressionBuilder->andX($expressionBuilder->like($fieldName, $like)); } if (in_array('pidonly', $searchConfig) && $currentPid > 0) { $searchConstraint->add($expressionBuilder->eq($tablePidField, (int)$currentPid)); } if ($searchConfig['andWhere']) { $searchConstraint->add( QueryHelper::stripLogicalOperatorPrefix($fieldConfig['search']['andWhere']) ); } } if ($fieldType === 'text' || $fieldType === 'flex' || $fieldType === 'input' && (!$evalRules || !preg_match('/\b(?:date|time|int)\b/', $evalRules)) ) { if ($searchConstraint->count() !== 0) { $constraints[] = $searchConstraint; } } } } // If no search field conditions have been built ensure no results are returned if (empty($constraints)) { return '0=1'; } return $expressionBuilder->orX(...$constraints); } /** * Fetches a list of fields to use in the Backend search for the given table. * * @param string $tableName * @return string[] */ protected function getSearchFields($tableName) { $fieldArray = []; $fieldListWasSet = false; // Get fields from ctrl section of TCA first if (isset($GLOBALS['TCA'][$tableName]['ctrl']['searchFields'])) { $fieldArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$tableName]['ctrl']['searchFields'], true); $fieldListWasSet = true; } // Call hook to add or change the list foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'] ?? [] as $hookFunction) { $hookParameters = [ 'tableHasSearchConfiguration' => $fieldListWasSet, 'tableName' => $tableName, 'searchFields' => &$fieldArray, 'searchString' => $this->searchString ]; GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this); } return $fieldArray; } /** * Returns the title (based on $code) of a table ($table) with the proper link around. For headers over tables. * The link will cause the display of all extended mode or not for the table. * * @param string $table Table name * @param string $code Table label * @return string The linked table label */ public function linkWrapTable($table, $code) { if ($this->table !== $table) { return '' . $code . ''; } return '' . $code . ''; } /** * 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...) * * @param string $table Table name * @param int $uid Item uid * @param string $code Item title (not htmlspecialchars()'ed yet) * @param mixed[] $row Item row * @return string The item title. Ready for HTML output (is htmlspecialchars()'ed) */ public function linkWrapItems($table, $uid, $code, $row) { $lang = $this->getLanguageService(); $origCode = $code; // If the title is blank, make a "no title" label: if ((string)$code === '') { $code = '[' . htmlspecialchars( $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') ) . '] - ' . htmlspecialchars(BackendUtility::getRecordTitle($table, $row)); } else { $code = htmlspecialchars($code, ENT_QUOTES, 'UTF-8', false); if ($code != htmlspecialchars($origCode)) { $code = '' . $code . ''; } } switch ((string)$this->clickTitleMode) { case 'edit': // If the listed table is 'pages' we have to request the permission settings for each page: if ($table === 'pages') { $localCalcPerms = $this->getBackendUser()->calcPerms( BackendUtility::getRecord('pages', $row['uid']) ); $permsEdit = $localCalcPerms & Permission::PAGE_EDIT; } else { $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT; } // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id) if ($permsEdit) { $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit'; $code = '' . $code . ''; } break; case 'show': // "Show" link (only pages and tt_content elements) if ($table === 'pages' || $table === 'tt_content') { $code = '' . $code . ''; } break; case 'info': // "Info": (All records) $code = '' . $code . ''; break; default: // Output the label now: if ($table === 'pages') { $code = '' . $code . ''; } else { $code = $this->linkUrlMail($code, $origCode); } } return $code; } /** * Wrapping input code in link to URL or email if $testString is either. * * @param string $code code to wrap * @param string $testString String which is tested for being a URL or email and which will be used for the link if so. * @return string Link-Wrapped $code value, if $testString was URL or email. */ public function linkUrlMail($code, $testString) { // Check for URL: $scheme = parse_url($testString, PHP_URL_SCHEME); if ($scheme === 'http' || $scheme === 'https' || $scheme === 'ftp') { return '' . $code . ''; } // Check for email: if (GeneralUtility::validEmail($testString)) { return '' . $code . ''; } // Return if nothing else... return $code; } /** * Creates the URL to this script, including all relevant GPvars * Fixed GPvars are id, table, imagemode, returnUrl, search_field, search_levels and showLimit * The GPvars "sortField" and "sortRev" are also included UNLESS they are found in the $exclList variable. * * @param string $altId Alternative id value. Enter blank string for the current id ($this->id) * @param string $table Table name to display. Enter "-1" for the current table. * @param string $exclList Comma separated list of fields NOT to include ("sortField", "sortRev" or "firstElementNumber") * @return string URL */ public function listURL($altId = '', $table = '-1', $exclList = '') { $urlParameters = []; if ((string)$altId !== '') { $urlParameters['id'] = $altId; } else { $urlParameters['id'] = $this->id; } if ($table === '-1') { $urlParameters['table'] = $this->table; } else { $urlParameters['table'] = $table; } if ($this->thumbs) { $urlParameters['imagemode'] = $this->thumbs; } if ($this->returnUrl) { $urlParameters['returnUrl'] = $this->returnUrl; } if ((!$exclList || !GeneralUtility::inList($exclList, 'search_field')) && $this->searchString) { $urlParameters['search_field'] = $this->searchString; } if ($this->searchLevels) { $urlParameters['search_levels'] = $this->searchLevels; } if ($this->showLimit) { $urlParameters['showLimit'] = $this->showLimit; } if ((!$exclList || !GeneralUtility::inList($exclList, 'firstElementNumber')) && $this->firstElementNumber) { $urlParameters['pointer'] = $this->firstElementNumber; } if ((!$exclList || !GeneralUtility::inList($exclList, 'sortField')) && $this->sortField) { $urlParameters['sortField'] = $this->sortField; } if ((!$exclList || !GeneralUtility::inList($exclList, 'sortRev')) && $this->sortRev) { $urlParameters['sortRev'] = $this->sortRev; } $urlParameters = array_merge_recursive($urlParameters, $this->overrideUrlParameters); if ($routePath = GeneralUtility::_GP('route')) { $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters); } else { $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?'); } return $url; } /** * Returns "requestUri" - which is basically listURL * @return string Content of ->listURL() */ public function requestUri() { return $this->listURL(); } /** * Makes the list of fields to select for a table * * @param string $table Table name * @param bool $dontCheckUser If set, users access to the field (non-exclude-fields) is NOT checked. * @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) * @return string[] Array, where values are fieldnames to include in query */ public function makeFieldList($table, $dontCheckUser = false, $addDateFields = false) { $backendUser = $this->getBackendUser(); // Init fieldlist array: $fieldListArr = []; // Check table: if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['columns']) && is_array( $GLOBALS['TCA'][$table]['columns'] )) { if (isset($GLOBALS['TCA'][$table]['columns']) && is_array($GLOBALS['TCA'][$table]['columns'])) { // Traverse configured columns and add them to field array, if available for user. foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fieldValue) { if ($dontCheckUser || (!$fieldValue['exclude'] || $backendUser->check( 'non_exclude_fields', $table . ':' . $fN )) && $fieldValue['config']['type'] !== 'passthrough') { $fieldListArr[] = $fN; } } $fieldListArr[] = 'uid'; $fieldListArr[] = 'pid'; // Add date fields if ($dontCheckUser || $backendUser->isAdmin() || $addDateFields) { if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) { $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['tstamp']; } if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) { $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['crdate']; } } // Add more special fields: if ($dontCheckUser || $backendUser->isAdmin()) { if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) { $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['cruser_id']; } if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; } if (ExtensionManagementUtility::isLoaded('workspaces') && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { $fieldListArr[] = 't3ver_id'; $fieldListArr[] = 't3ver_state'; $fieldListArr[] = 't3ver_wsid'; } } } else { $this->logger->error('TCA is broken for the table "' . $table . '": no required "columns" entry in TCA.'); } } return $fieldListArr; } /** * Redirects to FormEngine if a record is just localized. * * @param string $justLocalized String with table, orig uid and language separated by ": */ public function localizationRedirect($justLocalized) { list($table, $orig_uid, $language) = explode(':', $justLocalized); if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ) { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $localizedRecordUid = $queryBuilder->select('uid') ->from($table) ->where( $queryBuilder->expr()->eq( $GLOBALS['TCA'][$table]['ctrl']['languageField'], $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT) ), $queryBuilder->expr()->eq( $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], $queryBuilder->createNamedParameter($orig_uid, \PDO::PARAM_INT) ) ) ->setMaxResults(1) ->execute() ->fetchColumn(); if ($localizedRecordUid !== false) { // Create parameters and finally run the classic page module for creating a new page translation $url = $this->listURL(); $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $editUserAccountUrl = (string)$uriBuilder->buildUriFromRoute( 'record_edit', [ 'edit[' . $table . '][' . $localizedRecordUid . ']' => 'edit', 'returnUrl' => $url ] ); HttpUtility::redirect($editUserAccountUrl); } } } /** * Set URL parameters to override or add in the listUrl() method. * * @param string[] $urlParameters */ public function setOverrideUrlParameters(array $urlParameters) { $this->overrideUrlParameters = $urlParameters; } /** * Set table display order information * * Structure of $orderInformation: * 'tableName' => [ * 'before' => // comma-separated string list or array of table names * 'after' => // comma-separated string list or array of table names * ] * * @param array $orderInformation * @throws \UnexpectedValueException */ public function setTableDisplayOrder(array $orderInformation) { foreach ($orderInformation as $tableName => &$configuration) { if (isset($configuration['before'])) { if (is_string($configuration['before'])) { $configuration['before'] = GeneralUtility::trimExplode(',', $configuration['before'], true); } elseif (!is_array($configuration['before'])) { throw new \UnexpectedValueException( 'The specified "before" order configuration for table "' . $tableName . '" is invalid.', 1504870805 ); } } if (isset($configuration['after'])) { if (is_string($configuration['after'])) { $configuration['after'] = GeneralUtility::trimExplode(',', $configuration['after'], true); } elseif (!is_array($configuration['after'])) { throw new \UnexpectedValueException( 'The specified "after" order configuration for table "' . $tableName . '" is invalid.', 1504870806 ); } } } $this->tableDisplayOrder = $orderInformation; } /** * @return array */ public function getOverridePageIdList(): array { return $this->overridePageIdList; } /** * @param int[]|array $overridePageIdList */ public function setOverridePageIdList(array $overridePageIdList) { $this->overridePageIdList = array_map('intval', $overridePageIdList); } /** * Get all allowed mount pages to be searched in. * * @param int $id Page id * @param int $depth Depth to go down * @param string $perms_clause select clause * @return int[] */ protected function getSearchableWebmounts($id, $depth, $perms_clause) { $backendUser = $this->getBackendUser(); /** @var PageTreeView $tree */ $tree = GeneralUtility::makeInstance(PageTreeView::class); $tree->init('AND ' . $perms_clause); $tree->makeHTML = 0; $tree->fieldArray = ['uid', 'php_tree_stop']; $idList = []; $allowedMounts = !$backendUser->isAdmin() && $id === 0 ? $backendUser->returnWebmounts() : [$id]; foreach ($allowedMounts as $allowedMount) { $idList[] = $allowedMount; if ($depth) { $tree->getTree($allowedMount, $depth, ''); } $idList = array_merge($idList, $tree->ids); } return $idList; } /** * Add conditions to the QueryBuilder object ($queryBuilder) to limit a * query to a list of page IDs based on the current search level setting. * * @param string $tableName * @param QueryBuilder $queryBuilder * @return QueryBuilder Modified QueryBuilder object */ protected function addPageIdConstraint(string $tableName, QueryBuilder $queryBuilder): QueryBuilder { // Set search levels: $searchLevels = $this->searchLevels; // Set search levels to 999 instead of -1 as the following methods // do not support -1 as valid value for infinite search. if ($searchLevels === -1) { $searchLevels = 999; } if ($searchLevels === 0) { $queryBuilder->andWhere( $queryBuilder->expr()->eq( $tableName . '.pid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT) ) ); } elseif ($searchLevels > 0) { $allowedMounts = $this->getSearchableWebmounts($this->id, $searchLevels, $this->perms_clause); $queryBuilder->andWhere( $queryBuilder->expr()->in( $tableName . '.pid', $queryBuilder->createNamedParameter($allowedMounts, Connection::PARAM_INT_ARRAY) ) ); } if (!empty($this->getOverridePageIdList())) { $queryBuilder->andWhere( $queryBuilder->expr()->in( $tableName . '.pid', $queryBuilder->createNamedParameter($this->getOverridePageIdList(), Connection::PARAM_INT_ARRAY) ) ); } return $queryBuilder; } /** * Method used to log deprecated usage of old buildQueryParametersPostProcess hook arguments * * @param string $index * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ protected function logDeprecation(string $index) { trigger_error( '[index: ' . $index . '] $parameters in "buildQueryParameters"-Hook will be removed in TYPO3 v10.0, use $queryBuilder instead', E_USER_DEPRECATED ); } /** * Sets the script url depending on being a module or script request */ protected function determineScriptUrl() { if ($routePath = GeneralUtility::_GP('route')) { $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath); } else { $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME'); } } /** * Returns a table-row with the content from the fields in the input data array. * OBS: $this->fieldArray MUST be set! (represents the list of fields to display) * * @param int $h Is an integer >=0 and denotes how tall an element is. Set to '0' makes a halv line, -1 = full line, set to 1 makes a 'join' and above makes 'line' * @param string $icon Is the + of the record. If not supplied the first 'join'-icon will be a 'line' instead * @param array $data Is the dataarray, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in -tags * @param string $rowParams Is insert in the -tags. Must carry a ' ' as first character * @param string $_ OBSOLETE - NOT USED ANYMORE. $lMargin is the leftMargin (int) * @param string $_2 OBSOLETE - NOT USED ANYMORE. Is the HTML -tag for an alternative 'gfx/ol/line.gif'-icon (used in the top) * @param string $colType Defines the tag being used for the columns. Default is td. * * @return string HTML content for the table row */ public function addElement($h, $icon, $data, $rowParams = '', $_ = '', $_2 = '', $colType = 'td') { $colType = ($colType === 'th') ? 'th' : 'td'; $noWrap = $this->no_noWrap ? '' : ' nowrap'; // Start up: $l10nParent = isset($data['_l10nparent_']) ? (int)$data['_l10nparent_'] : 0; $out = ' '; // Show icon and lines if ($this->showIcon) { $out .= ' <' . $colType . ' class="col-icon nowrap">'; if (!$h) { $out .= ' '; } else { for ($a = 0; $a < $h; $a++) { if (!$a) { if ($icon) { $out .= $icon; } } } } $out .= ' '; } // Init rendering. $colsp = ''; $lastKey = ''; $c = 0; $ccount = 0; // __label is used as the label key to circumvent problems with uid used as label (see #67756) // as it was introduced later on, check if it really exists before using it $fields = $this->fieldArray; if ($colType === 'td' && array_key_exists('__label', $data)) { $fields[0] = '__label'; } // Traverse field array which contains the data to present: foreach ($fields as $vKey) { if (isset($data[$vKey])) { if ($lastKey) { $cssClass = $this->addElement_tdCssClass[$lastKey]; if ($this->oddColumnsCssClass && $ccount % 2 == 0) { $cssClass = implode(' ', [$this->addElement_tdCssClass[$lastKey], $this->oddColumnsCssClass]); } $out .= ' <' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . $this->addElement_tdParams[$lastKey] . '>' . $data[$lastKey] . ''; } $lastKey = $vKey; $c = 1; $ccount++; } else { if (!$lastKey) { $lastKey = $vKey; } $c++; } if ($c > 1) { $colsp = ' colspan="' . $c . '"'; } else { $colsp = ''; } } if ($lastKey) { $cssClass = $this->addElement_tdCssClass[$lastKey]; if ($this->oddColumnsCssClass) { $cssClass = implode(' ', [$this->addElement_tdCssClass[$lastKey], $this->oddColumnsCssClass]); } $out .= ' <' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . $this->addElement_tdParams[$lastKey] . '>' . $data[$lastKey] . ''; } // End row $out .= ' '; // Return row. return $out; } /** * Dummy function, used to write the top of a table listing. */ public function writeTop() { } /** * Creates a forward/reverse button based on the status of ->eCounter, ->firstElementNumber, ->iLimit * * @param string $table Table name * @return array array([boolean], [HTML]) where [boolean] is 1 for reverse element, [HTML] is the table-row code for the element */ public function fwd_rwd_nav($table = '') { $code = ''; if ($this->eCounter >= $this->firstElementNumber && $this->eCounter < $this->firstElementNumber + $this->iLimit) { if ($this->firstElementNumber && $this->eCounter == $this->firstElementNumber) { // Reverse $theData = []; $titleCol = $this->fieldArray[0]; $theData[$titleCol] = $this->fwd_rwd_HTML('fwd', $this->eCounter, $table); $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"'); } return [1, $code]; } if ($this->eCounter == $this->firstElementNumber + $this->iLimit) { // Forward $theData = []; $titleCol = $this->fieldArray[0]; $theData[$titleCol] = $this->fwd_rwd_HTML('rwd', $this->eCounter, $table); $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"'); } return [0, $code]; } /** * Creates the button with link to either forward or reverse * * @param string $type Type: "fwd" or "rwd * @param int $pointer Pointer * @param string $table Table name * @return string * @internal */ public function fwd_rwd_HTML($type, $pointer, $table = '') { $content = ''; $tParam = $table ? '&table=' . rawurlencode($table) : ''; switch ($type) { case 'fwd': $href = $this->listURL() . '&pointer=' . ($pointer - $this->iLimit) . $tParam; $content = '' . $this->iconFactory->getIcon( 'actions-move-up', Icon::SIZE_SMALL )->render() . ' [' . (max(0, $pointer - $this->iLimit) + 1) . ' - ' . $pointer . ']'; break; case 'rwd': $href = $this->listURL() . '&pointer=' . $pointer . $tParam; $content = '' . $this->iconFactory->getIcon( 'actions-move-down', Icon::SIZE_SMALL )->render() . ' [' . ($pointer + 1) . ' - ' . $this->totalItems . ']'; break; } return $content; } /** * @return string */ protected function getThisScript() { return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&'; } /** * Returning JavaScript for ClipBoard functionality. * * @return string */ public function CBfunctions() { return ' // checkOffCB() function checkOffCB(listOfCBnames, link) { // var checkBoxes, flag, i; var checkBoxes = listOfCBnames.split(","); if (link.rel === "") { link.rel = "allChecked"; flag = true; } else { link.rel = ""; flag = false; } for (i = 0; i < checkBoxes.length; i++) { setcbValue(checkBoxes[i], flag); } } // cbValue() function cbValue(CBname) { // var CBfullName = "CBC["+CBname+"]"; return (document.dblistForm[CBfullName] && document.dblistForm[CBfullName].checked ? 1 : 0); } // setcbValue() function setcbValue(CBname,flag) { // CBfullName = "CBC["+CBname+"]"; if(document.dblistForm[CBfullName]) { document.dblistForm[CBfullName].checked = flag ? "on" : 0; } } '; } /** * Initializes page languages */ public function initializeLanguages() { // Look up page overlays: $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('pages'); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $result = $queryBuilder ->select('*') ->from('pages') ->where( $queryBuilder->expr()->andX( $queryBuilder->expr()->eq( $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT) ), $queryBuilder->expr()->gt( $GLOBALS['TCA']['pages']['ctrl']['languageField'], $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) ) ) ) ->execute(); $this->pageOverlays = []; while ($row = $result->fetch()) { $this->pageOverlays[$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]] = $row; } // @deprecated $this->languageIconTitles can be removed in TYPO3 v10.0. foreach ($this->siteLanguages as $language) { $this->languageIconTitles[$language->getLanguageId()] = [ 'title' => $language->getTitle(), 'flagIcon' => $language->getFlagIdentifier() ]; } } /** * Return the icon for the language * * @param int $sys_language_uid Sys language uid * @param bool $addAsAdditionalText If set to true, only the flag is returned * @return string Language icon * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0. Use Site Languages instead. */ public function languageFlag($sys_language_uid, $addAsAdditionalText = true) { trigger_error('This method will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); $out = ''; $title = htmlspecialchars($this->languageIconTitles[$sys_language_uid]['title']); if ($this->languageIconTitles[$sys_language_uid]['flagIcon']) { $out .= '' . $this->iconFactory->getIcon( $this->languageIconTitles[$sys_language_uid]['flagIcon'], Icon::SIZE_SMALL )->render() . ''; if (!$addAsAdditionalText) { return $out; } $out .= ' '; } $out .= $title; return $out; } /** * Renders the language flag and language title, but only if a icon is given, otherwise just the language * * @param SiteLanguage $language * @return string */ protected function renderLanguageFlag(SiteLanguage $language) { $title = htmlspecialchars($language->getTitle()); if ($language->getFlagIdentifier()) { $icon = $this->iconFactory->getIcon( $language->getFlagIdentifier(), Icon::SIZE_SMALL )->render(); return '' . $icon . ' ' . $title; } return $title; } /** * Fetch the site language objects for the given $pageId and store it in $this->siteLanguages * * @param int $pageId * @throws SiteNotFoundException */ protected function resolveSiteLanguages(int $pageId) { $site = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId($pageId); $this->siteLanguages = $site->getAvailableLanguages($this->getBackendUser(), true, $pageId); } /** * Generates HTML code for a Reference tooltip out of * sys_refindex records you hand over * * @param int $references number of records from sys_refindex table * @param string $launchViewParameter JavaScript String, which will be passed as parameters to top.TYPO3.InfoWindow.showItem * @return string */ protected function generateReferenceToolTip($references, $launchViewParameter = '') { if (!$references) { $htmlCode = '-'; } else { $htmlCode = 'getLanguageService()->sL( 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references' ) . ' (' . $references . ')' ) . '">'; $htmlCode .= $references; $htmlCode .= ''; } return $htmlCode; } /** * @return string $title */ protected function getLocalizedPageTitle(): string { if (($this->tt_contentConfig['sys_language_uid'] ?? 0) > 0) { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('pages'); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); $localizedPage = $queryBuilder ->select('*') ->from('pages') ->where( $queryBuilder->expr()->eq( $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT) ), $queryBuilder->expr()->eq( $GLOBALS['TCA']['pages']['ctrl']['languageField'], $queryBuilder->createNamedParameter($this->tt_contentConfig['sys_language_uid'], \PDO::PARAM_INT) ) ) ->setMaxResults(1) ->execute() ->fetch(); BackendUtility::workspaceOL('pages', $localizedPage); return $localizedPage['title']; } return $this->pageinfo['title']; } /** * Check if page can be edited by current user * * @return bool */ protected function isPageEditable() { if ($this->getBackendUser()->isAdmin()) { return true; } return !$this->pageinfo['editlock'] && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::PAGE_EDIT); } /** * Check if content can be edited by current user * * @param int|null $languageId * @return bool */ protected function isContentEditable(?int $languageId = null) { if ($this->getBackendUser()->isAdmin()) { return true; } return !$this->pageinfo['editlock'] && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT) && ($languageId === null || $this->getBackendUser()->checkLanguageAccess($languageId)); } /** * Returns the language service * @return LanguageService */ protected function getLanguageService() { return $GLOBALS['LANG']; } }