Files
gutachten_t3/typo3/sysext/backend/Classes/View/PageLayoutView.php
T
2026-10-19 08:18:39 +02:00

4516 lines
193 KiB
PHP

<?php
namespace TYPO3\CMS\Backend\View;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Doctrine\DBAL\Driver\Statement;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Backend\Controller\Page\LocalizationController;
use TYPO3\CMS\Backend\Controller\PageLayoutController;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Tree\View\PageTreeView;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Routing\SiteMatcher;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
/**
* Child class for the Web > 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 <td> 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 = '<a class="btn btn-default" href="' . htmlspecialchars($url)
. '" title="' . htmlspecialchars($iTitle) . '">'
. $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
} else {
$eI = '';
}
switch ($field) {
case 'title':
$theData[$field] = $eI . '&nbsp;<strong>'
. $lang->sL($GLOBALS['TCA']['pages']['columns'][$field]['label'])
. '</strong>';
break;
case 'uid':
$theData[$field] = '';
break;
default:
if (strpos($field, 'table_') === 0) {
$f2 = substr($field, 6);
if ($GLOBALS['TCA'][$f2]) {
$theData[$field] = '&nbsp;' .
'<span title="' .
htmlspecialchars($lang->sL($GLOBALS['TCA'][$f2]['ctrl']['title'])) .
'">' .
$this->iconFactory->getIconForRecord($f2, [], Icon::SIZE_SMALL)->render() .
'</span>';
}
} else {
$theData[$field] = $eI . '&nbsp;<strong>'
. htmlspecialchars($lang->sL($GLOBALS['TCA']['pages']['columns'][$field]['label']))
. '</strong>';
}
}
}
$out = '<div class="table-fit">'
. '<table class="table table-striped table-hover typo3-page-pages">'
. '<thead>'
. $this->addElement(1, '', $theData)
. '</thead>'
. '<tbody>'
. $out
. '</tbody>'
. '</table>'
. '</div>';
}
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] .= '<div data-colpos="' . $columnId . '" data-language-uid="' . $lP . '" class="t3js-sortable t3js-sortable-lang t3js-sortable-lang-' . $lP . ' t3-page-ce-wrapper';
if (empty($contentRecordsPerColumn[$columnId])) {
$content[$columnId] .= ' t3-page-ce-empty';
}
$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 = '<a href="' . htmlspecialchars($url) . '" '
. 'title="' . $title . '"'
. 'data-title="' . $title . '"'
. 'class="btn btn-default btn-sm ' . ($this->option_newWizard ? 't3js-toggle-new-content-element-wizard disabled' : '') . '">'
. $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render()
. ' '
. htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>';
}
if ($this->getBackendUser()->checkLanguageAccess($lP) && $columnId !== 'unused') {
$content[$columnId] .= '
<div class="t3-page-ce t3js-page-ce" data-page="' . (int)$id . '" id="' . StringUtility::getUniqueId() . '">
<div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $columnId . '-' . 'page-' . $id . '-' . StringUtility::getUniqueId() . '">'
. $link
. '</div>
<div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div>
</div>
';
}
$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 = '<div class="t3-page-ce-dragitem" id="' . StringUtility::getUniqueId() . '">';
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 = '<div ' . ($row['_ORIG_uid'] ? ' class="ver-element"' : '') . '>'
. $this->tt_content_drawItem($row) . '</div>';
$singleElementHTML .= '<div class="t3-page-ce-body-inner">' . $innerContent . '</div></div>'
. $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 = '<div class="t3-page-ce' . $highlightHeader . ' t3js-page-ce t3js-page-ce-sortable ' . $statusHidden . '" id="element-tt_content-'
. $row['uid'] . '" data-table="tt_content" data-uid="' . $row['uid'] . '"' . $displayNone . '>' . $singleElementHTML . '</div>';
$singleElementHTML .= '<div class="t3-page-ce" data-colpos="' . $columnId . '">';
$singleElementHTML .= '<div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $columnId . '-' . 'page-' . $id .
'-' . StringUtility::getUniqueId() . '">';
// 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 .= '<a href="' . htmlspecialchars($url) . '" '
. 'title="' . $title . '"'
. 'data-title="' . $title . '"'
. 'class="btn btn-default btn-sm ' . ($this->option_newWizard ? 't3js-toggle-new-content-element-wizard disabled' : '') . '">'
. $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render()
. ' '
. htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>';
}
$singleElementHTML .= '</div></div><div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div></div>';
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] .= '</div>';
$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 = '<div class="t3-grid-container"><table border="0" cellspacing="0" cellpadding="0" width="100%" class="t3-page-columns t3-grid-table t3js-page-columns">';
// Add colgroups
$colCount = (int)$backendLayout['__config']['backend_layout.']['colCount'];
$rowCount = (int)$backendLayout['__config']['backend_layout.']['rowCount'];
$grid .= '<colgroup>';
for ($i = 0; $i < $colCount; $i++) {
$grid .= '<col />';
}
$grid .= '</colgroup>';
// 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 .= '<tr>';
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 .= '<td valign="top"' .
($colSpan > 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 .= '</td>';
}
$grid .= '</tr>';
}
if (!empty($content['unused'])) {
$grid .= '<tr>';
// Which tt_content colPos should be displayed inside this cell
$columnKey = 'unused';
// Render the grid cell
$colSpan = (int)$backendLayout['__config']['backend_layout.']['colCount'];
$grid .= '<td valign="top"' .
($colSpan > 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 .= '</td></tr>';
}
$out .= $grid . '</table></div>';
}
}
$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] = '
<td valign="top" class="t3-page-column t3-page-column-lang-name" data-language-uid="' . $lP . '">
<h2>' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '</h2>
' . ($languageMode !== '' ? '<span class="label label-' . $labelClass . '">' . $languageMode . '</span>' : '') . '
</td>';
// "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 = '<a href="#" class="btn btn-default btn-sm" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">' . $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render() . '</a>';
}
// 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')
? '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"'
. ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
. $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'
: ''
);
$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 =
'<div class="btn-group">'
. $viewLink
. $editLink
. (!empty($localizationButtons) ? implode(LF, $localizationButtons) : '')
. '</div>'
. ' ' . $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')
? '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"'
. ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
. $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'
: ''
);
}
$lPLabel =
'<div class="btn-group">'
. $viewLink
. $editLink
. '</div>'
. ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($this->pageRecord['title'], 20));
}
$sCont[$lP] = '
<td class="t3-page-column t3-page-lang-label nowrap">' . $lPLabel . '</td>';
}
// Add headers:
$out .= '<tr>' . implode('', $cCont) . '</tr>';
$out .= '<tr>' . implode('', $sCont) . '</tr>';
unset($cCont, $sCont);
// Traverse previously built content for the columns:
foreach ($languageColumn as $cKey => $cCont) {
$out .= '<tr>';
foreach ($cCont as $languageId => $columnContent) {
$out .= '<td valign="top" data-colpos="' . $cKey . '" class="t3-grid-cell t3-page-column t3js-page-column t3js-page-lang-column t3js-page-lang-column-' . $languageId . '">' . $columnContent . '</td>';
}
$out .= '</tr>';
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 .= '
<tr>
<td valign="top" class="t3-grid-cell" data-colpos="' . $cKey . '">' . implode('</td>
<td valign="top" class="t3-grid-cell" data-colpos="' . $cKey . '">', $cCont) . '</td>
</tr>';
}
}
}
// Finally, wrap it all in a table and add the language selector on top of it:
$out = $languageSelector . '
<div class="t3-grid-container">
<table cellpadding="0" cellspacing="0" class="t3-page-columns t3-grid-table t3js-page-columns">
' . $out . '
</table>
</div>';
}
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 .= '<tr><th class="col-icon"></th>'
. '<th colspan="' . (count($theData) - 2) . '"><span class="c-table">'
. $localizedTableTitle . '</span> (' . $dbCount . ')</td>' . '<td class="col-icon"></td>'
. '</tr>';
// 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__'] = '<a href="' . htmlspecialchars($url) . '" class="' . ($this->option_newWizard ? 't3js-toggle-new-content-element-wizard disabled' : '') . '" '
. 'title="' . $title . '"'
. 'data-title="' . $title . '">'
. $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . '</a>';
}
$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__'] = '<a class="btn btn-default" href="' . htmlspecialchars($url)
. '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
. $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
} else {
$Nrow['__editIconLink__'] = $this->noEditIcon();
}
$out .= $this->addElement(1, '', $Nrow);
}
$this->eCounter++;
}
}
// Wrap it all in a table:
$out = '
<!--
Standard list of table "' . $table . '"
-->
<div class="table-fit"><table class="table table-hover table-striped">
' . $out . '
</table></div>';
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 <br /> 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 <br /> 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] .= '<strong>' . htmlspecialchars($this->getLanguageService()->sL(
$GLOBALS['TCA'][$table]['columns'][$fName2]['label']
)) . '</strong>' . '&nbsp;&nbsp;' . htmlspecialchars(GeneralUtility::fixed_lgd_cs(
BackendUtility::getProcessedValue($table, $fName2, $row[$fName2], 0, 0, 0, $row['uid']),
25
)) . '<br />';
}
}
}
// If no value, add a nbsp.
if (!$out[$fieldName]) {
$out[$fieldName] = '&nbsp;';
}
// Wrap in dimmed-span tags if record is "disabled"
if ($this->isDisabled($table, $row)) {
$out[$fieldName] = '<span class="text-muted">' . $out[$fieldName] . '</span>';
}
}
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 : '&nbsp;';
}
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
. '<span class="treeline-icon treeline-icon-join'
. ($rowCount === $count ? 'bottom' : '')
. '"></span>';
$rows[] = $row;
// Get the branch
$spaceOutIcons = '<span class="treeline-icon treeline-icon-'
. ($rowCount === $count ? 'clear' : 'line')
. '"></span>';
$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] ? '<strong>x</strong>' : '&nbsp;';
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 =
'<a href="#" onclick="' . htmlspecialchars($onClick) . '" class="btn btn-default" title="' .
$this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewPage') . '">' .
$this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)->render() .
'</a>';
$eI .=
'<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' .
htmlspecialchars($this->getLanguageService()->getLL('editThisPage')) . '">' .
$this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render() .
'</a>';
} else {
$eI = '';
}
$theData[$field] = '<div class="btn-group" role="group">' . $eI . '</div>';
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'] = '<a href="#" onclick="'
. htmlspecialchars(BackendUtility::editOnClick($editParams)) . '" title="'
. htmlspecialchars($this->getLanguageService()->getLL('editColumn')) . '">'
. $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
}
}
$icons = '';
if (!empty($iconsArr)) {
$icons = '<div class="t3-page-column-header-icons">' . implode('', $iconsArr) . '</div>';
}
// Create header row:
$out = '<div class="t3-page-column-header">
' . $icons . '
<div class="t3-page-column-header-label">' . htmlspecialchars($colName) . '</div>
</div>';
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(
' <a data-content="' . htmlspecialchars($pasteItem) . '"'
. ' data-title="' . htmlspecialchars($pasteTitle) . '"'
. ' data-severity="warning"'
. ' class="t3js-paste t3js-paste' . htmlspecialchars($copyMode) . ' ' . htmlspecialchars($cssClass) . ' btn btn-default btn-sm"'
. ' title="' . htmlspecialchars($this->getLanguageService()->getLL($title)) . '">'
. $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
. '</a>'
);
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 = '<div class="t3-page-ce-info">
' . implode('<br>', $info) . '
</div>';
}
// Wrap it
if (!empty($content)) {
$content = '<div class="t3-page-ce-footer">' . $content . '</div>';
}
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 .= '<a class="btn btn-default" href="' . htmlspecialchars($url)
. '" title="' . htmlspecialchars($this->nextThree > 1
? sprintf($this->getLanguageService()->getLL('nextThree'), $this->nextThree)
: $this->getLanguageService()->getLL('edit'))
. '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
// 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 .= '<a class="btn btn-default" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
. '#element-tt_content-' . $row['uid'] . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($label)) . '">'
. $this->iconFactory->getIcon('actions-edit-' . strtolower($label), Icon::SIZE_SMALL)->render() . '</a>';
}
// 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 .= '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) . '"'
. ' data-severity="warning"'
. ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')) . '"'
. ' data-content="' . htmlspecialchars($confirm) . '" '
. ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
. ' title="' . htmlspecialchars($this->getLanguageService()->getLL('deleteItem')) . '">'
. $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
if ($out && $backendUser->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)) {
$out = '<div class="btn-group btn-group-sm" role="group">' . $out . '</div>';
} 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 .= '<a class="btn btn-default" href="'
. htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
. '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">'
. $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>';
if (!$dragDropEnabled) {
$displayMoveButtons = true;
}
} else {
$moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
}
// Move element down:
if ($this->tt_contentData['next'][$row['uid']]) {
$params = '&cmd[tt_content][' . $row['uid'] . '][move]= ' . $this->tt_contentData['next'][$row['uid']];
$moveButtonContent .= '<a class="btn btn-default" href="'
. htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
. '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">'
. $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>';
if (!$dragDropEnabled) {
$displayMoveButtons = true;
}
} else {
$moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
}
if ($displayMoveButtons) {
$out .= '<div class="btn-group btn-group-sm" role="group">' . $moveButtonContent . '</div>';
}
}
}
}
$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[] = '<a href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">'
. $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</a>';
}
// 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 <div class="t3-page-ce-body"> is in getTable_tt_content()
return '<div class="t3-page-ce-header ' . ($allowDragAndDrop ? 't3-page-ce-header-draggable t3js-page-ce-draghandle' : '') . '">
<div class="t3-page-ce-header-icons-left">' . implode('', $additionalIcons) . '</div>
<div class="t3-page-ce-header-icons-right">' . ($out ? '<div class="btn-toolbar">' . $out . '</div>' : '') . '</div>
</div>
<div class="t3-page-ce-body">';
}
/**
* 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 = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']</em>';
}
$outHeader = $row['date']
? htmlspecialchars($this->itemLabels['date'] . ' ' . BackendUtility::date($row['date'])) . '<br />'
: '';
$outHeader .= '<strong>' . $this->linkEditContent($this->renderText($row['header']), $row)
. $hiddenHeaderNote . '</strong><br />';
}
// 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) . '<br />';
}
break;
case 'bullets':
case 'table':
if ($row['bodytext']) {
$out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
}
break;
case 'uploads':
if ($row['media']) {
$out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'media'), $row) . '<br />';
}
break;
case 'menu':
$contentType = $this->CType_labels[$row['CType']];
$out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />';
// 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) . '<br />';
}
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('<br />', $shortcutContent) . '<br />';
}
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('<strong>' . htmlspecialchars($this->getLanguageService()->sL($label)) . '</strong>', $row) . '<br />';
} else {
$message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['list_type']);
$out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
}
} else {
$out .= '<strong>' . $this->getLanguageService()->getLL('noPluginSelected') . '</strong>';
}
$out .= htmlspecialchars($this->getLanguageService()->sL(
BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages'])
)) . '<br />';
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('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />';
if ($row['bodytext']) {
$out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
}
if ($row['image']) {
$out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'image'), $row) . '<br />';
}
} else {
$message = sprintf(
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
$row['CType']
);
$out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
}
}
}
// Wrap span-tags:
$out = '
<span class="exampleContent">' . $out . '</span>';
// Add header:
$out = $outHeader . $out;
// Return values:
if ($this->isDisabled('tt_content', $row)) {
return '<span class="text-muted">' . $out . '</span>';
}
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 .= '<br>' . 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 =
'<a'
. ' href="#"'
. ' class="btn btn-default btn-sm t3js-localize disabled"'
. ' title="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . '"'
. ' data-page="' . htmlspecialchars($this->getLocalizedPageTitle()) . '"'
. ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP]) . '"'
. ' data-allow-copy="' . (int)$allowCopy . '"'
. ' data-allow-translate="' . (int)$allowTranslate . '"'
. ' data-table="tt_content"'
. ' data-page-id="' . (int)GeneralUtility::_GP('id') . '"'
. ' data-language-id="' . $lP . '"'
. ' data-language-name="' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '"'
. '>'
. $this->iconFactory->getIcon('actions-localize', Icon::SIZE_SMALL)->render()
. ' ' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate'))
. '</a>';
}
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 '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $str . '</a>';
}
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 <select> HTML element (if there were items for the box anyways...)
* @see getTable_tt_content()
*/
public function languageSelector($id)
{
if (!$this->getBackendUser()->check('tables_modify', 'pages')) {
return '';
}
$id = (int)$id;
// First, select all languages that are available for the current user
$availableTranslations = [];
foreach ($this->siteLanguages as $language) {
if ($language->getLanguageId() <= 0) {
continue;
}
$availableTranslations[$language->getLanguageId()] = $language->getTitle();
}
// Then, subtract the languages which are already on the page:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
$queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
->from('pages')
->where(
$queryBuilder->expr()->eq(
$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
$queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
)
);
$statement = $queryBuilder->execute();
while ($row = $statement->fetch()) {
unset($availableTranslations[(int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]]);
}
// If any languages are left, make selector:
if (!empty($availableTranslations)) {
$output = '<option value="">' . htmlspecialchars($this->getLanguageService()->getLL('new_language')) . '</option>';
foreach ($availableTranslations as $languageUid => $languageTitle) {
// Build localize command URL to DataHandler (tce_db)
// which redirects to FormEngine (record_edit)
// which, when finished editing should return back to the current page (returnUrl)
$parameters = [
'justLocalized' => 'pages:' . $id . ':' . $languageUid,
'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
];
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', $parameters);
$targetUrl = BackendUtility::getLinkToDataHandlerAction(
'&cmd[pages][' . $id . '][localize]=' . $languageUid,
$redirectUrl
);
$output .= '<option value="' . htmlspecialchars($targetUrl) . '">' . htmlspecialchars($languageTitle) . '</option>';
}
return '<div class="form-inline form-inline-spaced">'
. '<div class="form-group">'
. '<select class="form-control input-sm" name="createNewLanguage" onchange="window.location.href=this.options[this.selectedIndex].value">'
. $output
. '</select></div></div>';
}
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 = '<span ' . $toolTip . '>' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
$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[] = '<strong>' . htmlspecialchars($this->itemLabels[$field]) . '</strong> '
. 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 '<span title="' . $title . '">' . $this->iconFactory->getIcon('status-edit-read-only', Icon::SIZE_SMALL)->render() . '</span>';
}
/*****************************************
*
* 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 .= '
<td><a href="#' . $tName . '" title="' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title'])) . '"></a>'
. $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render()
. '</td>';
// ... 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] = '<span title="' . $title . '">'
. $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render()
. '</span>'
. '&nbsp;' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title']));
}
}
}
// Wrap cells in table tags:
$out = '
<!--
Menu of tables on the page (table menu)
-->
<table border="0" cellpadding="0" cellspacing="0" id="typo3-page-tblMenu">
<tr>' . $out . '
</tr>
</table>';
// 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 = [
'<form action="' . htmlspecialchars(
$this->listURL('', '-1', 'firstElementNumber,search_field')
) . '" method="post">',
'</form>'
];
}
// 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[] = '<option value="' . $kv . '"' . ($kv === $this->searchLevels ? ' selected="selected"' : '') . '>' . htmlspecialchars(
$label
) . '</option>';
}
$lMenu = '<select class="form-control" name="search_levels" title="' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.search_levels')
) . '" id="search_levels">' . implode('', $opt) . '</select>';
// Table with the search box:
$content = '<div class="db_list-searchbox-form db_list-searchbox-toolbar module-docheader-bar module-docheader-bar-search t3js-module-docheader-bar t3js-module-docheader-bar-search" id="db_list-searchbox-toolbar" style="display: ' . ($this->searchString == '' ? 'none' : 'block') . ';">
' . $formElements[0] . '
<div id="typo3-dblist-search">
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="form-group col-xs-12">
<label for="search_field">' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.searchString')
) . ': </label>
<input class="form-control" type="search" placeholder="' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enterSearchString')
) . '" title="' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.searchString')
) . '" name="search_field" id="search_field" value="' . htmlspecialchars($this->searchString) . '" />
</div>
<div class="form-group col-xs-12 col-sm-6">
<label for="search_levels">' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.search_levels')
) . ': </label>
' . $lMenu . '
</div>
<div class="form-group col-xs-12 col-sm-6">
<label for="showLimit">' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.limit')
) . ': </label>
<input class="form-control" type="number" min="0" max="10000" placeholder="10" title="' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.limit')
) . '" name="showLimit" id="showLimit" value="' . htmlspecialchars(
($this->showLimit ? $this->showLimit : '')
) . '" />
</div>
<div class="form-group col-xs-12">
<div class="form-control-wrap">
<button type="submit" class="btn btn-default" name="search" title="' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.search')
) . '">
' . $this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render(
) . ' ' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.search')
) . '
</button>
</div>
</div>
</div>
</div>
</div>
</div>
' . $formElements[1] . '</div>';
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 '<a href="' . htmlspecialchars(
$this->listURL('', $table, 'firstElementNumber')
) . '">' . $code . '</a>';
}
return '<a href="' . htmlspecialchars(
$this->listURL('', '', 'sortField,sortRev,table,firstElementNumber')
) . '">' . $code . '</a>';
}
/**
* 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 = '<i>[' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')
) . ']</i> - '
. htmlspecialchars(BackendUtility::getRecordTitle($table, $row));
} else {
$code = htmlspecialchars($code, ENT_QUOTES, 'UTF-8', false);
if ($code != htmlspecialchars($origCode)) {
$code = '<span title="' . htmlspecialchars(
$origCode,
ENT_QUOTES,
'UTF-8',
false
) . '">' . $code . '</span>';
}
}
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 = '<a href="#" onclick="' . htmlspecialchars(
BackendUtility::editOnClick($params, '', -1)
) . '" title="' . htmlspecialchars($lang->getLL('edit')) . '">' . $code . '</a>';
}
break;
case 'show':
// "Show" link (only pages and tt_content elements)
if ($table === 'pages' || $table === 'tt_content') {
$code = '<a href="#" onclick="' . htmlspecialchars(
BackendUtility::viewOnClick(
($table === 'tt_content' ? $this->id . '#' . $row['uid'] : $row['uid'])
)
) . '" title="' . htmlspecialchars(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')
) . '">' . $code . '</a>';
}
break;
case 'info':
// "Info": (All records)
$code = '<a href="#" onclick="' . htmlspecialchars(
'top.TYPO3.InfoWindow.showItem(\'' . $table . '\', \'' . $row['uid'] . '\'); return false;'
) . '" title="' . htmlspecialchars($lang->getLL('showInfo')) . '">' . $code . '</a>';
break;
default:
// Output the label now:
if ($table === 'pages') {
$code = '<a href="' . htmlspecialchars(
$this->listURL($uid, '', 'firstElementNumber')
) . '" onclick="setHighlight(' . $uid . ')">' . $code . '</a>';
} 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 '<a href="' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
}
// Check for email:
if (GeneralUtility::validEmail($testString)) {
return '<a href="mailto:' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
}
// 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 <img>+<a> 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 <td>-tags
* @param string $rowParams Is insert in the <tr>-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 <img>-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 = '
<!-- Element, begin: -->
<tr ' . $rowParams . ' data-uid="' . (int)$data['uid'] . '" data-l10nparent="' . $l10nParent . '">';
// Show icon and lines
if ($this->showIcon) {
$out .= '
<' . $colType . ' class="col-icon nowrap">';
if (!$h) {
$out .= '&nbsp;';
} else {
for ($a = 0; $a < $h; $a++) {
if (!$a) {
if ($icon) {
$out .= $icon;
}
}
}
}
$out .= '</' . $colType . '>
';
}
// 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] . '</' . $colType . '>';
}
$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] . '</' . $colType . '>';
}
// End row
$out .= '
</tr>';
// 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 = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
'actions-move-up',
Icon::SIZE_SMALL
)->render() . '</a> <i>[' . (max(0, $pointer - $this->iLimit) + 1) . ' - ' . $pointer . ']</i>';
break;
case 'rwd':
$href = $this->listURL() . '&pointer=' . $pointer . $tParam;
$content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
'actions-move-down',
Icon::SIZE_SMALL
)->render() . '</a> <i>[' . ($pointer + 1) . ' - ' . $this->totalItems . ']</i>';
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 .= '<span title="' . $title . '">' . $this->iconFactory->getIcon(
$this->languageIconTitles[$sys_language_uid]['flagIcon'],
Icon::SIZE_SMALL
)->render() . '</span>';
if (!$addAsAdditionalText) {
return $out;
}
$out .= '&nbsp;';
}
$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 '<span title="' . $title . '">' . $icon . '</span>&nbsp;' . $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 = '<a href="#"';
if ($launchViewParameter !== '') {
$htmlCode .= ' onclick="' . htmlspecialchars(
'top.TYPO3.InfoWindow.showItem(' . $launchViewParameter . '); return false;'
) . '"';
}
$htmlCode .= ' title="' . htmlspecialchars(
$this->getLanguageService()->sL(
'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references'
) . ' (' . $references . ')'
) . '">';
$htmlCode .= $references;
$htmlCode .= '</a>';
}
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'];
}
}