diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon index 261390aad511676babde87f529ac66c938e1f990..8f93168d8a907dffd21378b310290dcbff0dd29f 100644 --- a/Build/phpstan/phpstan-baseline.neon +++ b/Build/phpstan/phpstan-baseline.neon @@ -3073,52 +3073,32 @@ parameters: - message: "#^Binary operation \"\\>\\>\" between string and 1 results in an error\\.$#" count: 2 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php + path: ../../typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php - message: "#^Binary operation \"\\>\\>\" between string and 5 results in an error\\.$#" count: 2 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php - - - - message: "#^Call to function is_array\\(\\) with float\\|int\\|string\\|null will always evaluate to false\\.$#" - count: 1 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php + path: ../../typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php - message: "#^Call to function is_array\\(\\) with string will always evaluate to false\\.$#" count: 1 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php + path: ../../typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php - message: "#^Cannot assign offset 'type' to string\\.$#" count: 2 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php - - - - message: "#^Offset 'queryConfig' on array on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php - - - - message: "#^Offset 'queryLimit' on non\\-empty\\-array on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php + path: ../../typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php - message: "#^Offset 'userFunc' on 'user' in isset\\(\\) does not exist\\.$#" count: 1 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php - - - - message: "#^Offset 0 on array\\{\\} in isset\\(\\) does not exist\\.$#" - count: 1 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php + path: ../../typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php - - message: "#^Result of \\|\\| is always true\\.$#" + message: "#^Offset 0 on array\\{\\} in empty\\(\\) does not exist\\.$#" count: 1 - path: ../../typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php + path: ../../typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php - message: "#^Negated boolean expression is always false\\.$#" diff --git a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php index e9c36902a18eadd462bc9a03df5aba1d7d51caf5..23631098c7e08cedbb3e7ce3a490d305e526452a 100644 --- a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php +++ b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php @@ -17,7 +17,10 @@ declare(strict_types=1); namespace TYPO3\CMS\Lowlevel\Controller; +use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Types\Types; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Routing\Route; @@ -26,17 +29,29 @@ use TYPO3\CMS\Backend\Template\Components\ButtonBar; use TYPO3\CMS\Backend\Template\ModuleTemplate; use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\QueryHelper; +use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; use TYPO3\CMS\Core\Database\ReferenceIndex; 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\FlashMessageRendererResolver; +use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Type\Bitmask\Permission; +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; +use TYPO3\CMS\Core\Utility\CsvUtility; +use TYPO3\CMS\Core\Utility\DebugUtility; 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\PathUtility; -use TYPO3\CMS\Lowlevel\Database\QueryGenerator; +use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\CMS\Lowlevel\Integrity\DatabaseIntegrityCheck; /** @@ -60,6 +75,150 @@ class DatabaseIntegrityController protected UriBuilder $uriBuilder; protected ModuleTemplateFactory $moduleTemplateFactory; + protected string $formName = ''; + protected string $moduleName = ''; + + /** + * If the current user is an admin and $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] + * is set to true, the names of fields and tables are displayed. + */ + protected bool $showFieldAndTableNames = false; + protected array $hookArray = []; + protected string $table = ''; + protected bool $enablePrefix = false; + protected int $noDownloadB = 0; + protected array $tableArray = []; + protected array $queryConfig = []; + protected array $extFieldLists = []; + protected array $fields = []; + protected string $storeList = 'search_query_smallparts,search_result_labels,labels_noprefix,show_deleted,queryConfig,queryTable,queryFields,queryLimit,queryOrder,queryOrderDesc,queryOrder2,queryOrder2Desc,queryGroup,search_query_makeQuery'; + protected bool $enableQueryParts = false; + protected array $lang = [ + 'OR' => 'or', + 'AND' => 'and', + 'comparison' => [ + // Type = text offset = 0 + '0_' => 'contains', + '1_' => 'does not contain', + '2_' => 'starts with', + '3_' => 'does not start with', + '4_' => 'ends with', + '5_' => 'does not end with', + '6_' => 'equals', + '7_' => 'does not equal', + // Type = number , offset = 32 + '32_' => 'equals', + '33_' => 'does not equal', + '34_' => 'is greater than', + '35_' => 'is less than', + '36_' => 'is between', + '37_' => 'is not between', + '38_' => 'is in list', + '39_' => 'is not in list', + '40_' => 'binary AND equals', + '41_' => 'binary AND does not equal', + '42_' => 'binary OR equals', + '43_' => 'binary OR does not equal', + // Type = multiple, relation, offset = 64 + '64_' => 'equals', + '65_' => 'does not equal', + '66_' => 'contains', + '67_' => 'does not contain', + '68_' => 'is in list', + '69_' => 'is not in list', + '70_' => 'binary AND equals', + '71_' => 'binary AND does not equal', + '72_' => 'binary OR equals', + '73_' => 'binary OR does not equal', + // Type = date,time offset = 96 + '96_' => 'equals', + '97_' => 'does not equal', + '98_' => 'is greater than', + '99_' => 'is less than', + '100_' => 'is between', + '101_' => 'is not between', + '102_' => 'binary AND equals', + '103_' => 'binary AND does not equal', + '104_' => 'binary OR equals', + '105_' => 'binary OR does not equal', + // Type = boolean, offset = 128 + '128_' => 'is True', + '129_' => 'is False', + // Type = binary , offset = 160 + '160_' => 'equals', + '161_' => 'does not equal', + '162_' => 'contains', + '163_' => 'does not contain', + ], + ]; + protected string $fieldName = ''; + protected string $name = ''; + protected string $fieldList; + protected array $comp_offsets = [ + 'text' => 0, + 'number' => 1, + 'multiple' => 2, + 'relation' => 2, + 'date' => 3, + 'time' => 3, + 'boolean' => 4, + 'binary' => 5, + ]; + protected array $compSQL = [ + // Type = text offset = 0 + '0' => '#FIELD# LIKE \'%#VALUE#%\'', + '1' => '#FIELD# NOT LIKE \'%#VALUE#%\'', + '2' => '#FIELD# LIKE \'#VALUE#%\'', + '3' => '#FIELD# NOT LIKE \'#VALUE#%\'', + '4' => '#FIELD# LIKE \'%#VALUE#\'', + '5' => '#FIELD# NOT LIKE \'%#VALUE#\'', + '6' => '#FIELD# = \'#VALUE#\'', + '7' => '#FIELD# != \'#VALUE#\'', + // Type = number, offset = 32 + '32' => '#FIELD# = \'#VALUE#\'', + '33' => '#FIELD# != \'#VALUE#\'', + '34' => '#FIELD# > #VALUE#', + '35' => '#FIELD# < #VALUE#', + '36' => '#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#', + '37' => 'NOT (#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#)', + '38' => '#FIELD# IN (#VALUE#)', + '39' => '#FIELD# NOT IN (#VALUE#)', + '40' => '(#FIELD# & #VALUE#)=#VALUE#', + '41' => '(#FIELD# & #VALUE#)!=#VALUE#', + '42' => '(#FIELD# | #VALUE#)=#VALUE#', + '43' => '(#FIELD# | #VALUE#)!=#VALUE#', + // Type = multiple, relation, offset = 64 + '64' => '#FIELD# = \'#VALUE#\'', + '65' => '#FIELD# != \'#VALUE#\'', + '66' => '#FIELD# LIKE \'%#VALUE#%\' AND #FIELD# LIKE \'%#VALUE1#%\'', + '67' => '(#FIELD# NOT LIKE \'%#VALUE#%\' OR #FIELD# NOT LIKE \'%#VALUE1#%\')', + '68' => '#FIELD# IN (#VALUE#)', + '69' => '#FIELD# NOT IN (#VALUE#)', + '70' => '(#FIELD# & #VALUE#)=#VALUE#', + '71' => '(#FIELD# & #VALUE#)!=#VALUE#', + '72' => '(#FIELD# | #VALUE#)=#VALUE#', + '73' => '(#FIELD# | #VALUE#)!=#VALUE#', + // Type = date, offset = 32 + '96' => '#FIELD# = \'#VALUE#\'', + '97' => '#FIELD# != \'#VALUE#\'', + '98' => '#FIELD# > #VALUE#', + '99' => '#FIELD# < #VALUE#', + '100' => '#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#', + '101' => 'NOT (#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#)', + '102' => '(#FIELD# & #VALUE#)=#VALUE#', + '103' => '(#FIELD# & #VALUE#)!=#VALUE#', + '104' => '(#FIELD# | #VALUE#)=#VALUE#', + '105' => '(#FIELD# | #VALUE#)!=#VALUE#', + // Type = boolean, offset = 128 + '128' => '#FIELD# = \'1\'', + '129' => '#FIELD# != \'1\'', + // Type = binary = 160 + '160' => '#FIELD# = \'#VALUE#\'', + '161' => '#FIELD# != \'#VALUE#\'', + '162' => '(#FIELD# & #VALUE#)=#VALUE#', + '163' => '(#FIELD# & #VALUE#)=0', + ]; + public function __construct( IconFactory $iconFactory, UriBuilder $uriBuilder, @@ -68,6 +227,7 @@ class DatabaseIntegrityController $this->iconFactory = $iconFactory; $this->uriBuilder = $uriBuilder; $this->moduleTemplateFactory = $moduleTemplateFactory; + $this->moduleName = 'system_dbint'; } public function handleRequest(ServerRequestInterface $request): ResponseInterface @@ -183,7 +343,7 @@ class DatabaseIntegrityController } } if ($setLimitToStart) { - $currentLimit = explode(',', $this->MOD_SETTINGS['queryLimit']); + $currentLimit = explode(',', $this->MOD_SETTINGS['queryLimit'] ?? ''); if (!empty($currentLimit[1] ?? 0)) { $this->MOD_SETTINGS['queryLimit'] = '0,' . $currentLimit[1]; } else { @@ -263,9 +423,10 @@ class DatabaseIntegrityController protected function searchAction(ModuleTemplate $view, ServerRequestInterface $request): ResponseInterface { $lang = $this->getLanguageService(); + $lang->includeLLFile('EXT:core/Resources/Private/Language/locallang_t3lib_fullsearch.xlf'); + $this->showFieldAndTableNames = $this->getBackendUserAuthentication()->shallDisplayDebugInformation(); $searchMode = $this->MOD_SETTINGS['search']; - $fullSearch = GeneralUtility::makeInstance(QueryGenerator::class, $this->MOD_SETTINGS, $this->MOD_MENU, 'system_dbint'); - $fullSearch->setFormName('queryform'); + $this->setFormName('queryform'); $submenu = '<div class="row row-cols-auto align-items-end g-3 mb-3">'; $submenu .= '<div class="col">' . self::getDropdownMenu(0, 'SET[search]', $searchMode, $this->MOD_MENU['search'], $request) . '</div>'; if ($this->MOD_SETTINGS['search'] === 'query') { @@ -283,16 +444,2171 @@ class DatabaseIntegrityController $view->assign('searchMode', $searchMode); switch ($searchMode) { case 'query': - $view->assign('queryMaker', $fullSearch->queryMaker($request)); + $view->assign('queryMaker', $this->queryMaker($request)); break; case 'raw': default: - $view->assign('searchOptions', $fullSearch->form()); - $view->assign('results', $fullSearch->search()); + $view->assign('searchOptions', $this->form()); + $view->assign('results', $this->search()); } return $view->renderResponse('CustomSearch'); } + protected function setFormName(string $formName): void + { + $this->formName = trim($formName); + } + + protected function queryMaker(ServerRequestInterface $request): string + { + $output = ''; + $this->hookArray = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3lib_fullsearch'] ?? []; + $msg = $this->procesStoreControl(); + $userTsConfig = $this->getBackendUserAuthentication()->getTSConfig(); + if (!($userTsConfig['mod.']['dbint.']['disableStoreControl'] ?? false)) { + $output .= '<h2>Load/Save Query</h2>'; + $output .= '<div>' . $this->makeStoreControl() . '</div>'; + $output .= $msg; + } + // Query Maker: + $this->init('queryConfig', $this->MOD_SETTINGS['queryTable'] ?? '', '', $this->MOD_SETTINGS); + if ($this->formName) { + $this->setFormName($this->formName); + } + $tmpCode = $this->makeSelectorTable($this->MOD_SETTINGS, $request); + $output .= '<div id="query"></div><h2>Make query</h2><div>' . $tmpCode . '</div>'; + $mQ = $this->MOD_SETTINGS['search_query_makeQuery'] ?? ''; + // Make form elements: + if ($this->table && is_array($GLOBALS['TCA'][$this->table])) { + if ($mQ) { + // Show query + $this->enablePrefix = true; + $queryString = $this->getQuery($this->queryConfig); + $selectQueryString = $this->getSelectQuery($queryString); + $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table); + + $isConnectionMysql = str_starts_with($connection->getServerVersion(), 'MySQL'); + $fullQueryString = ''; + try { + if ($mQ === 'explain' && $isConnectionMysql) { + // EXPLAIN is no ANSI SQL, for now this is only executed on mysql + // @todo: Move away from getSelectQuery() or model differently + $fullQueryString = 'EXPLAIN ' . $selectQueryString; + $dataRows = $connection->executeQuery('EXPLAIN ' . $selectQueryString)->fetchAllAssociative(); + } elseif ($mQ === 'count') { + $queryBuilder = $connection->createQueryBuilder(); + $queryBuilder->getRestrictions()->removeAll(); + if (empty($this->MOD_SETTINGS['show_deleted'])) { + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + } + $queryBuilder->count('*') + ->from($this->table) + ->where(QueryHelper::stripLogicalOperatorPrefix($queryString)); + $fullQueryString = $queryBuilder->getSQL(); + $dataRows = [$queryBuilder->executeQuery()->fetchOne()]; + } else { + $fullQueryString = $selectQueryString; + $dataRows = $connection->executeQuery($selectQueryString)->fetchAllAssociative(); + } + if (!($userTsConfig['mod.']['dbint.']['disableShowSQLQuery'] ?? false)) { + $output .= '<h2>SQL query</h2><div><code>' . htmlspecialchars($fullQueryString) . '</code></div>'; + } + $cPR = $this->getQueryResultCode($mQ, $dataRows, $this->table); + $output .= '<h2>' . ($cPR['header'] ?? '') . '</h2><div>' . $cPR['content'] . '</div>'; + } catch (DBALException $e) { + if (!($userTsConfig['mod.']['dbint.']['disableShowSQLQuery'] ?? false)) { + $output .= '<h2>SQL query</h2><div><code>' . htmlspecialchars($fullQueryString) . '</code></div>'; + } + $out = '<p><strong>Error: <span class="text-danger">' + . htmlspecialchars($e->getMessage()) + . '</span></strong></p>'; + $output .= '<h2>SQL error</h2><div>' . $out . '</div>'; + } + } + } + return '<div class="database-query-builder">' . $output . '</div>'; + } + + protected function getSelectQuery(string $qString = ''): string + { + $backendUserAuthentication = $this->getBackendUserAuthentication(); + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table); + $queryBuilder->getRestrictions()->removeAll(); + if (empty($this->MOD_SETTINGS['show_deleted'])) { + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + } + $deleteField = $GLOBALS['TCA'][$this->table]['ctrl']['delete'] ?? ''; + $fieldList = GeneralUtility::trimExplode( + ',', + $this->extFieldLists['queryFields'] + . ',pid' + . ($deleteField ? ',' . $deleteField : '') + ); + $queryBuilder->select(...$fieldList) + ->from($this->table); + + if ($this->extFieldLists['queryGroup']) { + $queryBuilder->groupBy(...QueryHelper::parseGroupBy($this->extFieldLists['queryGroup'])); + } + if ($this->extFieldLists['queryOrder']) { + foreach (QueryHelper::parseOrderBy($this->extFieldLists['queryOrder_SQL']) as $orderPair) { + [$fieldName, $order] = $orderPair; + $queryBuilder->addOrderBy($fieldName, $order); + } + } + $queryLimit = (string)($this->extFieldLists['queryLimit'] ?? ''); + if ($queryLimit) { + // Explode queryLimit to fetch the limit and a possible offset + $parts = GeneralUtility::intExplode(',', $queryLimit); + if ($parts[1] ?? null) { + // Offset and limit are given + $queryBuilder->setFirstResult($parts[0]); + $queryBuilder->setMaxResults($parts[1]); + } else { + // Only the limit is given + $queryBuilder->setMaxResults($parts[0]); + } + } + + if (!$backendUserAuthentication->isAdmin()) { + $webMounts = $backendUserAuthentication->returnWebmounts(); + $perms_clause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); + $webMountPageTree = ''; + $webMountPageTreePrefix = ''; + foreach ($webMounts as $webMount) { + if ($webMountPageTree) { + $webMountPageTreePrefix = ','; + } + $webMountPageTree .= $webMountPageTreePrefix + . $this->getTreeList($webMount, 999, 0, $perms_clause); + } + // createNamedParameter() is not used here because the SQL fragment will only include + // the :dcValueX placeholder when the query is returned as a string. The value for the + // placeholder would be lost in the process. + if ($this->table === 'pages') { + $queryBuilder->where( + QueryHelper::stripLogicalOperatorPrefix($perms_clause), + $queryBuilder->expr()->in( + 'uid', + GeneralUtility::intExplode(',', $webMountPageTree) + ) + ); + } else { + $queryBuilder->where( + $queryBuilder->expr()->in( + 'pid', + GeneralUtility::intExplode(',', $webMountPageTree) + ) + ); + } + } + if (!$qString) { + $qString = $this->getQuery($this->queryConfig); + } + $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($qString)); + + return $queryBuilder->getSQL(); + } + + /** + * Recursively fetch all descendants of a given page + * + * @return string comma separated list of descendant pages + */ + protected function getTreeList(int $id, int $depth, int $begin = 0, string $permsClause = ''): string + { + if ($id < 0) { + $id = abs($id); + } + if ($begin == 0) { + $theList = (string)$id; + } else { + $theList = ''; + } + if ($id && $depth > 0) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $statement = $queryBuilder->select('uid') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)), + $queryBuilder->expr()->eq('sys_language_uid', 0) + ) + ->orderBy('uid'); + if ($permsClause !== '') { + $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($permsClause)); + } + $statement = $queryBuilder->executeQuery(); + while ($row = $statement->fetchAssociative()) { + if ($begin <= 0) { + $theList .= ',' . $row['uid']; + } + if ($depth > 1) { + $theSubList = $this->getTreeList($row['uid'], $depth - 1, $begin - 1, $permsClause); + if (!empty($theList) && !empty($theSubList) && ($theSubList[0] !== ',')) { + $theList .= ','; + } + $theList .= $theSubList; + } + } + } + return $theList; + } + + /** + * @return array HTML-code for "header" and "content" + * @throws \TYPO3\CMS\Core\Exception + */ + protected function getQueryResultCode(string $type, array $dataRows, string $table): array + { + $out = ''; + $cPR = []; + switch ($type) { + case 'count': + $cPR['header'] = 'Count'; + $cPR['content'] = '<br><strong>' . (int)$dataRows[0] . '</strong> records selected.'; + break; + case 'all': + $rowArr = []; + $dataRow = null; + foreach ($dataRows as $dataRow) { + $rowArr[] = $this->resultRowDisplay($dataRow, $GLOBALS['TCA'][$table], $table); + } + if (is_array($this->hookArray['beforeResultTable'] ?? false)) { + foreach ($this->hookArray['beforeResultTable'] as $_funcRef) { + $out .= GeneralUtility::callUserFunction($_funcRef, $this->MOD_SETTINGS); + } + } + if (!empty($rowArr)) { + $cPR['header'] = 'Result'; + $out .= '<table class="table table-striped table-hover">' + . $this->resultRowTitles((array)$dataRow, $GLOBALS['TCA'][$table]) . implode(LF, $rowArr) + . '</table>'; + } else { + $this->renderNoResultsFoundMessage(); + } + + $cPR['content'] = $out; + break; + case 'csv': + $rowArr = []; + $first = 1; + foreach ($dataRows as $dataRow) { + if ($first) { + $rowArr[] = $this->csvValues(array_keys($dataRow)); + $first = 0; + } + $rowArr[] = $this->csvValues($dataRow, ',', '"', $GLOBALS['TCA'][$table], $table); + } + if (!empty($rowArr)) { + $cPR['header'] = 'Result'; + $out .= '<textarea name="whatever" rows="20" class="text-monospace" style="width:100%">' + . htmlspecialchars(implode(LF, $rowArr)) + . '</textarea>'; + if (!$this->noDownloadB) { + $out .= '<br><input class="btn btn-default" type="submit" name="download_file" ' + . 'value="Click to download file">'; + } + // Downloads file: + // @todo: args. routing anyone? + if (GeneralUtility::_GP('download_file')) { + $filename = 'TYPO3_' . $table . '_export_' . date('dmy-Hi') . '.csv'; + $mimeType = 'application/octet-stream'; + header('Content-Type: ' . $mimeType); + header('Content-Disposition: attachment; filename=' . $filename); + echo implode(CRLF, $rowArr); + die; + } + } else { + $this->renderNoResultsFoundMessage(); + } + $cPR['content'] = $out; + break; + case 'explain': + default: + foreach ($dataRows as $dataRow) { + $out .= '<br />' . DebugUtility::viewArray($dataRow); + } + $cPR['header'] = 'Explain SQL query'; + $cPR['content'] = $out; + } + return $cPR; + } + + protected function csvValues(array $row, string $delim = ',', string $quote = '"', array $conf = [], string $table = ''): string + { + $valueArray = $row; + if (($this->MOD_SETTINGS['search_result_labels'] ?? false) && $table) { + foreach ($valueArray as $key => $val) { + $valueArray[$key] = $this->getProcessedValueExtra($table, $key, $val, $conf, ';'); + } + } + return CsvUtility::csvValues($valueArray, $delim, $quote); + } + + /** + * @param array|null $row Table columns + */ + protected function resultRowTitles($row, array $conf): string + { + $languageService = $this->getLanguageService(); + $tableHeader = []; + // Start header row + $tableHeader[] = '<thead><tr>'; + // Iterate over given columns + foreach ($row ?? [] as $fieldName => $fieldValue) { + if (GeneralUtility::inList($this->MOD_SETTINGS['queryFields'] ?? '', $fieldName) + || !($this->MOD_SETTINGS['queryFields'] ?? false) + && $fieldName !== 'pid' + && $fieldName !== 'deleted' + ) { + if ($this->MOD_SETTINGS['search_result_labels'] ?? false) { + $title = $languageService->sL(($conf['columns'][$fieldName]['label'] ?? false) ?: $fieldName); + } else { + $title = $languageService->sL($fieldName); + } + $tableHeader[] = '<th>' . htmlspecialchars($title) . '</th>'; + } + } + // Add empty icon column + $tableHeader[] = '<th></th>'; + // Close header row + $tableHeader[] = '</tr></thead>'; + return implode(LF, $tableHeader); + } + + protected function resultRowDisplay(array $row, array $conf, string $table): string + { + $languageService = $this->getLanguageService(); + $out = '<tr>'; + foreach ($row as $fieldName => $fieldValue) { + if (GeneralUtility::inList($this->MOD_SETTINGS['queryFields'] ?? '', $fieldName) + || !($this->MOD_SETTINGS['queryFields'] ?? false) + && $fieldName !== 'pid' + && $fieldName !== 'deleted' + ) { + if ($this->MOD_SETTINGS['search_result_labels'] ?? false) { + $fVnew = $this->getProcessedValueExtra($table, $fieldName, (string)$fieldValue, $conf, '<br />'); + } else { + $fVnew = htmlspecialchars((string)$fieldValue); + } + $out .= '<td>' . $fVnew . '</td>'; + } + } + $out .= '<td>'; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + + if (!($row['deleted'] ?? false)) { + $out .= '<div class="btn-group" role="group">'; + $url = (string)$uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => [ + $table => [ + $row['uid'] => 'edit', + ], + ], + 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri() + . HttpUtility::buildQueryString(['SET' => (array)GeneralUtility::_POST('SET')], '&'), + ]); + $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url) . '">' + . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'; + $out .= '</div><div class="btn-group" role="group">'; + $out .= sprintf( + '<a class="btn btn-default" href="#" data-dispatch-action="%s" data-dispatch-args-list="%s">%s</a>', + 'TYPO3.InfoWindow.showItem', + htmlspecialchars($table . ',' . $row['uid']), + $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() + ); + $out .= '</div>'; + } else { + $out .= '<div class="btn-group" role="group">'; + $out .= '<a class="btn btn-default" href="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('tce_db', [ + 'cmd' => [ + $table => [ + $row['uid'] => [ + 'undelete' => 1, + ], + ], + ], + 'redirect' => GeneralUtility::linkThisScript(), + ])) . '" title="' . htmlspecialchars($languageService->getLL('undelete_only')) . '">'; + $out .= $this->iconFactory->getIcon('actions-edit-restore', Icon::SIZE_SMALL)->render() . '</a>'; + $formEngineParameters = [ + 'edit' => [ + $table => [ + $row['uid'] => 'edit', + ], + ], + 'returnUrl' => GeneralUtility::linkThisScript(), + ]; + $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', $formEngineParameters); + $out .= '<a class="btn btn-default" href="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('tce_db', [ + 'cmd' => [ + $table => [ + $row['uid'] => [ + 'undelete' => 1, + ], + ], + ], + 'redirect' => $redirectUrl, + ])) . '" title="' . htmlspecialchars($languageService->getLL('undelete_and_edit')) . '">'; + $out .= $this->iconFactory->getIcon('actions-delete-edit', Icon::SIZE_SMALL)->render() . '</a>'; + $out .= '</div>'; + } + $_params = [$table => $row]; + if (is_array($this->hookArray['additionalButtons'] ?? false)) { + foreach ($this->hookArray['additionalButtons'] as $_funcRef) { + $out .= GeneralUtility::callUserFunction($_funcRef, $_params); + } + } + $out .= '</td></tr>'; + return $out; + } + + protected function getProcessedValueExtra(string $table, string $fieldName, string $fieldValue, array $conf, string $splitString): string + { + $out = ''; + $fields = []; + // Analysing the fields in the table. + if (is_array($GLOBALS['TCA'][$table] ?? null)) { + $fC = $GLOBALS['TCA'][$table]['columns'][$fieldName] ?? null; + $fields = $fC['config'] ?? []; + $fields['exclude'] = $fC['exclude'] ?? ''; + if (is_array($fC) && $fC['label']) { + $fields['label'] = preg_replace('/:$/', '', trim($this->getLanguageService()->sL($fC['label']))); + switch ($fields['type']) { + case 'input': + if (GeneralUtility::inList($fields['eval'] ?? '', 'year')) { + $fields['type'] = 'number'; + } else { + $fields['type'] = 'text'; + } + break; + case 'number': + // Empty on purpose, we have to keep the type "number". + // Falling back to the "default" case would set the type to "text" + break; + case 'datetime': + if (!in_array($fields['dbType'] ?? '', QueryHelper::getDateTimeTypes(), true)) { + $fields['type'] = 'number'; + } elseif ($fields['dbType'] === 'time') { + $fields['type'] = 'time'; + } else { + $fields['type'] = 'date'; + } + break; + case 'check': + if (!($fields['items'] ?? false)) { + $fields['type'] = 'boolean'; + } else { + $fields['type'] = 'binary'; + } + break; + case 'radio': + $fields['type'] = 'multiple'; + break; + case 'select': + case 'category': + $fields['type'] = 'multiple'; + if ($fields['foreign_table'] ?? false) { + $fields['type'] = 'relation'; + } + if ($fields['special'] ?? false) { + $fields['type'] = 'text'; + } + break; + case 'group': + $fields['type'] = 'relation'; + break; + case 'user': + case 'flex': + case 'passthrough': + case 'none': + case 'text': + case 'email': + case 'link': + case 'password': + case 'color': + default: + $fields['type'] = 'text'; + } + } else { + $fields['label'] = '[FIELD: ' . $fieldName . ']'; + switch ($fieldName) { + case 'pid': + $fields['type'] = 'relation'; + $fields['allowed'] = 'pages'; + break; + case 'tstamp': + case 'crdate': + $fields['type'] = 'time'; + break; + default: + $fields['type'] = 'number'; + } + } + } + switch ($fields['type']) { + case 'date': + if ($fieldValue != -1) { + // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. + $out = (string)@strftime('%d-%m-%Y', (int)$fieldValue); + } + break; + case 'time': + if ($fieldValue != -1) { + if ($splitString === '<br />') { + // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. + $out = (string)@strftime('%H:%M' . $splitString . '%d-%m-%Y', (int)$fieldValue); + } else { + // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. + $out = (string)@strftime('%H:%M %d-%m-%Y', (int)$fieldValue); + } + } + break; + case 'multiple': + case 'binary': + case 'relation': + $out = $this->makeValueList($fieldName, $fieldValue, $fields, $table, $splitString); + break; + case 'boolean': + $out = $fieldValue ? 'True' : 'False'; + break; + default: + $out = htmlspecialchars($fieldValue); + } + return $out; + } + + protected function makeValueList(string $fieldName, string $fieldValue, array $conf, string $table, string $splitString): string + { + $backendUserAuthentication = $this->getBackendUserAuthentication(); + $languageService = $this->getLanguageService(); + $from_table_Arr = []; + $fieldSetup = $conf; + $out = ''; + if ($fieldSetup['type'] === 'multiple') { + foreach (($fieldSetup['items'] ?? []) as $val) { + $value = $languageService->sL($val[0]); + if (GeneralUtility::inList($fieldValue, $val[1]) || $fieldValue == $val[1]) { + if ($out !== '') { + $out .= $splitString; + } + $out .= htmlspecialchars($value); + } + } + } + if ($fieldSetup['type'] === 'binary') { + foreach ($fieldSetup['items'] as $val) { + $value = $languageService->sL($val[0]); + if ($out !== '') { + $out .= $splitString; + } + $out .= htmlspecialchars($value); + } + } + if ($fieldSetup['type'] === 'relation') { + $dontPrefixFirstTable = 0; + $useTablePrefix = 0; + foreach (($fieldSetup['items'] ?? []) as $val) { + if (str_starts_with($val[0], 'LLL:')) { + $value = $languageService->sL($val[0]); + } else { + $value = $val[0]; + } + if (GeneralUtility::inList($fieldValue, $value) || $fieldValue == $value) { + if ($out !== '') { + $out .= $splitString; + } + $out .= htmlspecialchars($value); + } + } + if (str_contains($fieldSetup['allowed'] ?? '', ',')) { + $from_table_Arr = explode(',', $fieldSetup['allowed']); + $useTablePrefix = 1; + if (!$fieldSetup['prepend_tname']) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $statement = $queryBuilder->select($fieldName)->from($table)->executeQuery(); + while ($row = $statement->fetchAssociative()) { + if (str_contains($row[$fieldName], ',')) { + $checkContent = explode(',', $row[$fieldName]); + foreach ($checkContent as $singleValue) { + if (!str_contains($singleValue, '_')) { + $dontPrefixFirstTable = 1; + } + } + } else { + $singleValue = $row[$fieldName]; + if ($singleValue !== '' && !str_contains($singleValue, '_')) { + $dontPrefixFirstTable = 1; + } + } + } + } + } else { + $from_table_Arr[0] = $fieldSetup['allowed'] ?? null; + } + if (!empty($fieldSetup['prepend_tname'])) { + $useTablePrefix = 1; + } + if (!empty($fieldSetup['foreign_table'])) { + $from_table_Arr[0] = $fieldSetup['foreign_table']; + } + $counter = 0; + $useSelectLabels = 0; + $useAltSelectLabels = 0; + $tablePrefix = ''; + $labelFieldSelect = []; + foreach ($from_table_Arr as $from_table) { + if ($useTablePrefix && !$dontPrefixFirstTable && $counter != 1 || $counter == 1) { + $tablePrefix = $from_table . '_'; + } + $counter = 1; + if (is_array($GLOBALS['TCA'][$from_table] ?? null)) { + $labelField = $GLOBALS['TCA'][$from_table]['ctrl']['label'] ?? ''; + $altLabelField = $GLOBALS['TCA'][$from_table]['ctrl']['label_alt'] ?? ''; + if (is_array($GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items'] ?? false)) { + $items = $GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items']; + foreach ($items as $labelArray) { + $labelFieldSelect[$labelArray[1]] = $languageService->sL($labelArray[0]); + } + $useSelectLabels = 1; + } + $altLabelFieldSelect = []; + if (is_array($GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items'] ?? false)) { + $items = $GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items']; + foreach ($items as $altLabelArray) { + $altLabelFieldSelect[$altLabelArray[1]] = $languageService->sL($altLabelArray[0]); + } + $useAltSelectLabels = 1; + } + + if (empty($this->tableArray[$from_table])) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($from_table); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $selectFields = ['uid', $labelField]; + if ($altLabelField) { + $selectFields = array_merge($selectFields, GeneralUtility::trimExplode(',', $altLabelField, true)); + } + $queryBuilder->select(...$selectFields) + ->from($from_table) + ->orderBy('uid'); + if (!$backendUserAuthentication->isAdmin()) { + $webMounts = $backendUserAuthentication->returnWebmounts(); + $perms_clause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); + $webMountPageTree = ''; + $webMountPageTreePrefix = ''; + foreach ($webMounts as $webMount) { + if ($webMountPageTree) { + $webMountPageTreePrefix = ','; + } + $webMountPageTree .= $webMountPageTreePrefix + . $this->getTreeList($webMount, 999, 0, $perms_clause); + } + if ($from_table === 'pages') { + $queryBuilder->where( + QueryHelper::stripLogicalOperatorPrefix($perms_clause), + $queryBuilder->expr()->in( + 'uid', + $queryBuilder->createNamedParameter( + GeneralUtility::intExplode(',', $webMountPageTree), + Connection::PARAM_INT_ARRAY + ) + ) + ); + } else { + $queryBuilder->where( + $queryBuilder->expr()->in( + 'pid', + $queryBuilder->createNamedParameter( + GeneralUtility::intExplode(',', $webMountPageTree), + Connection::PARAM_INT_ARRAY + ) + ) + ); + } + } + $statement = $queryBuilder->executeQuery(); + $this->tableArray[$from_table] = []; + while ($row = $statement->fetchAssociative()) { + $this->tableArray[$from_table][] = $row; + } + } + + foreach ($this->tableArray[$from_table] as $key => $val) { + $this->MOD_SETTINGS['labels_noprefix'] = + ($this->MOD_SETTINGS['labels_noprefix'] ?? '') == 1 + ? 'on' + : $this->MOD_SETTINGS['labels_noprefix']; + $prefixString = + $this->MOD_SETTINGS['labels_noprefix'] === 'on' + ? '' + : ' [' . $tablePrefix . $val['uid'] . '] '; + if ($out !== '') { + $out .= $splitString; + } + if (GeneralUtility::inList($fieldValue, $tablePrefix . $val['uid']) + || $fieldValue == $tablePrefix . $val['uid']) { + if ($useSelectLabels) { + $out .= htmlspecialchars($prefixString . $labelFieldSelect[$val[$labelField]]); + } elseif ($val[$labelField]) { + $out .= htmlspecialchars($prefixString . $val[$labelField]); + } elseif ($useAltSelectLabels) { + $out .= htmlspecialchars($prefixString . $altLabelFieldSelect[$val[$altLabelField]]); + } else { + $out .= htmlspecialchars($prefixString . $val[$altLabelField]); + } + } + } + } + } + } + return $out; + } + + /** + * @throws \InvalidArgumentException + * @throws \TYPO3\CMS\Core\Exception + */ + private function renderNoResultsFoundMessage(): void + { + $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, 'No rows selected!', '', ContextualFeedbackSeverity::INFO); + $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); + $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + + protected function getQuery(array $queryConfig, string $pad = ''): string + { + $qs = ''; + // Since we don't traverse the array using numeric keys in the upcoming whileloop make sure it's fresh and clean + ksort($queryConfig); + $first = true; + foreach ($queryConfig as $key => $conf) { + $conf = $this->convertIso8601DatetimeStringToUnixTimestamp($conf); + switch ($conf['type']) { + case 'newlevel': + $qs .= LF . $pad . trim($conf['operator']) . ' (' . $this->getQuery( + $queryConfig[$key]['nl'], + $pad . ' ' + ) . LF . $pad . ')'; + break; + default: + $qs .= LF . $pad . $this->getQuerySingle($conf, $first); + } + $first = false; + } + return $qs; + } + + protected function getQuerySingle(array $conf, bool $first): string + { + $comparison = (int)($conf['comparison'] ?? 0); + $qs = ''; + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table); + $prefix = $this->enablePrefix ? $this->table . '.' : ''; + if (!$first) { + // Is it OK to insert the AND operator if none is set? + $operator = strtoupper(trim($conf['operator'] ?? '')); + if (!in_array($operator, ['AND', 'OR'], true)) { + $operator = 'AND'; + } + $qs .= $operator . ' '; + } + $qsTmp = str_replace('#FIELD#', $prefix . trim(substr($conf['type'], 6)), $this->compSQL[$comparison] ?? ''); + $inputVal = $this->cleanInputVal($conf); + if ($comparison === 68 || $comparison === 69) { + $inputVal = explode(',', (string)$inputVal); + foreach ($inputVal as $key => $fileName) { + $inputVal[$key] = $queryBuilder->quote($fileName); + } + $inputVal = implode(',', $inputVal); + $qsTmp = str_replace('#VALUE#', $inputVal, $qsTmp); + } elseif ($comparison === 162 || $comparison === 163) { + $inputValArray = explode(',', (string)$inputVal); + $inputVal = 0; + foreach ($inputValArray as $fileName) { + $inputVal += (int)$fileName; + } + $qsTmp = str_replace('#VALUE#', (string)$inputVal, $qsTmp); + } else { + if (is_array($inputVal)) { + $inputVal = $inputVal[0]; + } + // @todo This is weired, as it seems that it quotes the value as string and remove + // quotings using the trim() method. Should be investagated/refactored. + $qsTmp = str_replace('#VALUE#', trim($queryBuilder->quote((string)$inputVal), '\''), $qsTmp); + } + if ($comparison === 37 || $comparison === 36 || $comparison === 66 || $comparison === 67 || $comparison === 100 || $comparison === 101) { + // between: + $inputVal = $this->cleanInputVal($conf, '1'); + // @todo This is weired, as it seems that it quotes the value as string and remove + // quotings using the trim() method. Should be investagated/refactored. + $qsTmp = str_replace('#VALUE1#', trim($queryBuilder->quote((string)$inputVal), '\''), $qsTmp); + } + $qs .= trim((string)$qsTmp); + return $qs; + } + + /** + * @return mixed + */ + protected function cleanInputVal(array $conf, string $suffix = '') + { + $comparison = (int)($conf['comparison'] ?? 0); + $var = $conf['inputValue' . $suffix] ?? ''; + if ($comparison >> 5 === 0 || ($comparison === 32 || $comparison === 33 || $comparison === 64 || $comparison === 65 || $comparison === 66 || $comparison === 67 || $comparison === 96 || $comparison === 97)) { + $inputVal = $var ?? null; + } elseif ($comparison === 39 || $comparison === 38) { + // in list: + $inputVal = implode(',', GeneralUtility::intExplode(',', (string)($var ?? ''))); + } elseif ($comparison === 68 || $comparison === 69 || $comparison === 162 || $comparison === 163) { + // in list: + if (is_array($var ?? false)) { + $inputVal = implode(',', $var); + } elseif ($var ?? false) { + $inputVal = $var; + } else { + $inputVal = 0; + } + } elseif (!is_array($var) && strtotime((string)$var)) { + $inputVal = $var; + } elseif (!is_array($var) && MathUtility::canBeInterpretedAsInteger($var)) { + $inputVal = (int)$var; + } else { + // TODO: Six eyes looked at this code and nobody understood completely what is going on here and why we + // fallback to float casting, the whole class smells like it needs a refactoring. + $inputVal = (float)($var ?? 0.0); + } + return $inputVal; + } + + protected function convertIso8601DatetimeStringToUnixTimestamp(array $conf): array + { + if ($this->isDateOfIso8601Format($conf['inputValue'] ?? '')) { + $conf['inputValue'] = strtotime($conf['inputValue']); + if ($this->isDateOfIso8601Format($conf['inputValue1'] ?? '')) { + $conf['inputValue1'] = strtotime($conf['inputValue1']); + } + } + + return $conf; + } + + /** + * Checks if the given value is of the ISO 8601 format. + * + * @param mixed $date + */ + protected function isDateOfIso8601Format($date): bool + { + if (!is_int($date) && !is_string($date)) { + return false; + } + $format = 'Y-m-d\\TH:i:s\\Z'; + $formattedDate = \DateTime::createFromFormat($format, (string)$date); + return $formattedDate && $formattedDate->format($format) === $date; + } + + protected function makeSelectorTable(array $modSettings, ServerRequestInterface $request, string $enableList = 'table,fields,query,group,order,limit'): string + { + $out = []; + $enableArr = explode(',', $enableList); + $userTsConfig = $this->getBackendUserAuthentication()->getTSConfig(); + + // Make output + if (in_array('table', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableSelectATable'] ?? false)) { + $out[] = '<div class="form-group">'; + $out[] = '<label for="SET[queryTable]">Select a table:</label>'; + $out[] = '<div class="row row-cols-auto">'; + $out[] = '<div class="col">'; + $out[] = $this->mkTableSelect('SET[queryTable]', $this->table); + $out[] = '</div>'; + $out[] = '</div>'; + $out[] = '</div>'; + } + if ($this->table) { + // Init fields: + $this->setAndCleanUpExternalLists('queryFields', $modSettings['queryFields'] ?? '', 'uid,' . $this->getLabelCol()); + $this->setAndCleanUpExternalLists('queryGroup', $modSettings['queryGroup'] ?? ''); + $this->setAndCleanUpExternalLists('queryOrder', ($modSettings['queryOrder'] ?? '') . ',' . ($modSettings['queryOrder2'] ?? '')); + // Limit: + $this->extFieldLists['queryLimit'] = $modSettings['queryLimit'] ?? ''; + if (!$this->extFieldLists['queryLimit']) { + $this->extFieldLists['queryLimit'] = 100; + } + $parts = GeneralUtility::intExplode(',', (string)$this->extFieldLists['queryLimit']); + $limitBegin = 0; + $limitLength = (int)($this->extFieldLists['queryLimit']); + if ($parts[1] ?? null) { + $limitBegin = (int)$parts[0]; + $limitLength = (int)$parts[1]; + } + $this->extFieldLists['queryLimit'] = implode(',', array_slice($parts, 0, 2)); + // Insert Descending parts + if ($this->extFieldLists['queryOrder']) { + $descParts = explode(',', ($modSettings['queryOrderDesc'] ?? '') . ',' . ($modSettings['queryOrder2Desc'] ?? '')); + $orderParts = explode(',', $this->extFieldLists['queryOrder']); + $reList = []; + foreach ($orderParts as $kk => $vv) { + $reList[] = $vv . ($descParts[$kk] ? ' DESC' : ''); + } + $this->extFieldLists['queryOrder_SQL'] = implode(',', $reList); + } + // Query Generator: + $this->procesData(($modSettings['queryConfig'] ?? '') ? unserialize((string)$modSettings['queryConfig'], ['allowed_classes' => false]) : []); + $this->queryConfig = $this->cleanUpQueryConfig($this->queryConfig); + $this->enableQueryParts = (bool)($modSettings['search_query_smallparts'] ?? false); + $codeArr = $this->getFormElements(); + $queryCode = $this->printCodeArray($codeArr); + if (in_array('fields', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableSelectFields'] ?? false)) { + $out[] = '<div class="form-group form-group-with-button-addon">'; + $out[] = ' <label for="SET[queryFields]">Select fields:</label>'; + $out[] = $this->mkFieldToInputSelect('SET[queryFields]', $this->extFieldLists['queryFields']); + $out[] = '</div>'; + } + if (in_array('query', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableMakeQuery'] ?? false)) { + $out[] = '<div class="form-group">'; + $out[] = ' <label>Make Query:</label>'; + $out[] = $queryCode; + $out[] = '</div>'; + } + if (in_array('group', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableGroupBy'] ?? false)) { + $out[] = '<div class="form-group">'; + $out[] = '<label for="SET[queryGroup]">Group By:</label>'; + $out[] = '<div class="row row-cols-auto">'; + $out[] = '<div class="col">'; + $out[] = $this->mkTypeSelect('SET[queryGroup]', $this->extFieldLists['queryGroup'], ''); + $out[] = '</div>'; + $out[] = '</div>'; + $out[] = '</div>'; + } + if (in_array('order', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableOrderBy'] ?? false)) { + $orderByArr = explode(',', $this->extFieldLists['queryOrder']); + $orderBy = []; + $orderBy[] = '<div class="row row-cols-auto align-items-center">'; + $orderBy[] = '<div class="col">'; + $orderBy[] = $this->mkTypeSelect('SET[queryOrder]', $orderByArr[0], ''); + $orderBy[] = '</div>'; + $orderBy[] = '<div class="col mt-2">'; + $orderBy[] = '<div class="form-check">'; + $orderBy[] = self::getFuncCheck(0, 'SET[queryOrderDesc]', $modSettings['queryOrderDesc'] ?? '', $request, '', '', 'id="checkQueryOrderDesc"'); + $orderBy[] = '<label class="form-check-label" for="checkQueryOrderDesc">Descending</label>'; + $orderBy[] = '</div>'; + $orderBy[] = '</div>'; + $orderBy[] = '</div>'; + + if ($orderByArr[0]) { + $orderBy[] = '<div class="row row-cols-auto align-items-center mt-2">'; + $orderBy[] = '<div class="col">'; + $orderBy[] = '<div class="input-group">'; + $orderBy[] = $this->mkTypeSelect('SET[queryOrder2]', $orderByArr[1] ?? '', ''); + $orderBy[] = '</div>'; + $orderBy[] = '</div>'; + $orderBy[] = '<div class="col mt-2">'; + $orderBy[] = '<div class="form-check">'; + $orderBy[] = self::getFuncCheck(0, 'SET[queryOrder2Desc]', $modSettings['queryOrder2Desc'] ?? false, $request, '', '', 'id="checkQueryOrder2Desc"'); + $orderBy[] = '<label class="form-check-label" for="checkQueryOrder2Desc">Descending</label>'; + $orderBy[] = '</div>'; + $orderBy[] = '</div>'; + $orderBy[] = '</div>'; + } + $out[] = '<div class="form-group">'; + $out[] = ' <label>Order By:</label>'; + $out[] = implode(LF, $orderBy); + $out[] = '</div>'; + } + if (in_array('limit', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableLimit'] ?? false)) { + $limit = []; + $limit[] = '<div class="input-group">'; + $limit[] = ' <span class="input-group-btn">'; + $limit[] = $this->updateIcon(); + $limit[] = ' </span>'; + $limit[] = ' <input type="text" class="form-control" value="' . htmlspecialchars($this->extFieldLists['queryLimit']) . '" name="SET[queryLimit]" id="queryLimit">'; + $limit[] = '</div>'; + + $prevLimit = $limitBegin - $limitLength < 0 ? 0 : $limitBegin - $limitLength; + $prevButton = ''; + $nextButton = ''; + + if ($limitBegin) { + $prevButton = '<input type="button" class="btn btn-default" value="previous ' . htmlspecialchars((string)$limitLength) . '" data-value="' . htmlspecialchars($prevLimit . ',' . $limitLength) . '">'; + } + if (!$limitLength) { + $limitLength = 100; + } + + $nextLimit = $limitBegin + $limitLength; + if ($nextLimit < 0) { + $nextLimit = 0; + } + if ($nextLimit) { + $nextButton = '<input type="button" class="btn btn-default" value="next ' . htmlspecialchars((string)$limitLength) . '" data-value="' . htmlspecialchars($nextLimit . ',' . $limitLength) . '">'; + } + + $out[] = '<div class="form-group">'; + $out[] = ' <label>Limit:</label>'; + $out[] = ' <div class="row row-cols-auto">'; + $out[] = ' <div class="col">'; + $out[] = implode(LF, $limit); + $out[] = ' </div>'; + $out[] = ' <div class="col">'; + $out[] = ' <div class="btn-group t3js-limit-submit">'; + $out[] = $prevButton; + $out[] = $nextButton; + $out[] = ' </div>'; + $out[] = ' </div>'; + $out[] = ' <div class="col">'; + $out[] = ' <div class="btn-group t3js-limit-submit">'; + $out[] = ' <input type="button" class="btn btn-default" data-value="10" value="10">'; + $out[] = ' <input type="button" class="btn btn-default" data-value="20" value="20">'; + $out[] = ' <input type="button" class="btn btn-default" data-value="50" value="50">'; + $out[] = ' <input type="button" class="btn btn-default" data-value="100" value="100">'; + $out[] = ' </div>'; + $out[] = ' </div>'; + $out[] = ' </div>'; + $out[] = '</div>'; + } + } + return implode(LF, $out); + } + + protected function cleanUpQueryConfig(array $queryConfig): array + { + // Since we don't traverse the array using numeric keys in the upcoming while-loop make sure it's fresh and clean before displaying + if (!empty($queryConfig) && is_array($queryConfig)) { + ksort($queryConfig); + } elseif (empty($queryConfig[0]['type'])) { + // Make sure queryConfig is an array + $queryConfig = []; + $queryConfig[0] = ['type' => 'FIELD_']; + } + // Traverse: + foreach ($queryConfig as $key => $conf) { + $fieldName = ''; + if (str_starts_with(($conf['type'] ?? ''), 'FIELD_')) { + $fieldName = substr($conf['type'], 6); + $fieldType = $this->fields[$fieldName]['type'] ?? ''; + } elseif (($conf['type'] ?? '') === 'newlevel') { + $fieldType = $conf['type']; + } else { + $fieldType = 'ignore'; + } + switch ($fieldType) { + case 'newlevel': + if (!$queryConfig[$key]['nl']) { + $queryConfig[$key]['nl'][0]['type'] = 'FIELD_'; + } + $queryConfig[$key]['nl'] = $this->cleanUpQueryConfig($queryConfig[$key]['nl']); + break; + case 'userdef': + break; + case 'ignore': + default: + $verifiedName = $this->verifyType($fieldName); + $queryConfig[$key]['type'] = 'FIELD_' . $this->verifyType($verifiedName); + if ((int)($conf['comparison'] ?? 0) >> 5 !== (int)($this->comp_offsets[$fieldType] ?? 0)) { + $conf['comparison'] = (int)($this->comp_offsets[$fieldType] ?? 0) << 5; + } + $queryConfig[$key]['comparison'] = $this->verifyComparison($conf['comparison'] ?? '' ? (string)$conf['comparison'] : '0', ($conf['negate'] ?? null) ? 1 : 0); + $queryConfig[$key]['inputValue'] = $this->cleanInputVal($queryConfig[$key]); + $queryConfig[$key]['inputValue1'] = $this->cleanInputVal($queryConfig[$key], '1'); + } + } + return $queryConfig; + } + + protected function verifyType(string $fieldName): string + { + $first = ''; + foreach ($this->fields as $key => $value) { + if (!$first) { + $first = $key; + } + if ($key === $fieldName) { + return $key; + } + } + return $first; + } + + /** + * @param string $comparison + */ + protected function verifyComparison($comparison, int $neg): int + { + $compOffSet = $comparison >> 5; + $first = -1; + for ($i = 32 * $compOffSet + $neg; $i < 32 * ($compOffSet + 1); $i += 2) { + if ($first === -1) { + $first = $i; + } + if ($i >> 1 === $comparison >> 1) { + return $i; + } + } + return $first; + } + + /** + * @param string $queryConfig + */ + protected function getFormElements(int $subLevel = 0, $queryConfig = '', string $parent = ''): array + { + $codeArr = []; + if (!is_array($queryConfig)) { + $queryConfig = $this->queryConfig; + } + $c = 0; + $arrCount = 0; + $loopCount = 0; + foreach ($queryConfig as $key => $conf) { + $fieldName = ''; + $subscript = $parent . '[' . $key . ']'; + $lineHTML = []; + $lineHTML[] = $this->mkOperatorSelect($this->name . $subscript, ($conf['operator'] ?? ''), (bool)$c, ($conf['type'] ?? '') !== 'FIELD_'); + if (str_starts_with(($conf['type'] ?? ''), 'FIELD_')) { + $fieldName = substr($conf['type'], 6); + $this->fieldName = $fieldName; + $fieldType = $this->fields[$fieldName]['type'] ?? ''; + if ((int)($conf['comparison'] ?? 0) >> 5 !== (int)($this->comp_offsets[$fieldType] ?? 0)) { + $conf['comparison'] = (int)($this->comp_offsets[$fieldType] ?? 0) << 5; + } + //nasty nasty... + //make sure queryConfig contains _actual_ comparevalue. + //mkCompSelect don't care, but getQuery does. + $queryConfig[$key]['comparison'] += isset($conf['negate']) - $conf['comparison'] % 2; + } elseif (($conf['type'] ?? '') === 'newlevel') { + $fieldType = $conf['type']; + } else { + $fieldType = 'ignore'; + } + $fieldPrefix = htmlspecialchars($this->name . $subscript); + switch ($fieldType) { + case 'ignore': + break; + case 'newlevel': + if (!$queryConfig[$key]['nl']) { + $queryConfig[$key]['nl'][0]['type'] = 'FIELD_'; + } + $lineHTML[] = '<input type="hidden" name="' . $fieldPrefix . '[type]" value="newlevel">'; + $codeArr[$arrCount]['sub'] = $this->getFormElements($subLevel + 1, $queryConfig[$key]['nl'], $subscript . '[nl]'); + break; + case 'userdef': + $lineHTML[] = ''; + break; + case 'date': + $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; + $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); + if ($conf['comparison'] === 100 || $conf['comparison'] === 101) { + // between + $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'date'); + $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue1]', $conf['inputValue1'], 'date'); + } else { + $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'date'); + } + $lineHTML[] = '</div>'; + break; + case 'time': + $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; + $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); + if ($conf['comparison'] === 100 || $conf['comparison'] === 101) { + // between: + $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'datetime'); + $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue1]', $conf['inputValue1'], 'datetime'); + } else { + $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'datetime'); + } + $lineHTML[] = '</div>'; + break; + case 'multiple': + case 'binary': + case 'relation': + $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; + $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); + $lineHTML[] = '<div class="col mb-sm-2">'; + if ($conf['comparison'] === 68 || $conf['comparison'] === 69 || $conf['comparison'] === 162 || $conf['comparison'] === 163) { + $lineHTML[] = '<select class="form-select" name="' . $fieldPrefix . '[inputValue][]" multiple="multiple">'; + } elseif ($conf['comparison'] === 66 || $conf['comparison'] === 67) { + if (is_array($conf['inputValue'])) { + $conf['inputValue'] = implode(',', $conf['inputValue']); + } + $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue'] ?? '') . '" name="' . $fieldPrefix . '[inputValue]">'; + } elseif ($conf['comparison'] === 64) { + if (is_array($conf['inputValue'])) { + $conf['inputValue'] = $conf['inputValue'][0]; + } + $lineHTML[] = '<select class="form-select t3js-submit-change" name="' . $fieldPrefix . '[inputValue]">'; + } else { + $lineHTML[] = '<select class="form-select t3js-submit-change" name="' . $fieldPrefix . '[inputValue]">'; + } + if ($conf['comparison'] != 66 && $conf['comparison'] != 67) { + $lineHTML[] = $this->makeOptionList($fieldName, $conf, $this->table); + $lineHTML[] = '</select>'; + } + $lineHTML[] = '</div>'; + $lineHTML[] = '</div>'; + break; + case 'boolean': + $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; + $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); + $lineHTML[] = '<input type="hidden" value="1" name="' . $fieldPrefix . '[inputValue]">'; + $lineHTML[] = '</div>'; + break; + default: + $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; + $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); + $lineHTML[] = '<div class="col mb-sm-2">'; + if ($conf['comparison'] === 37 || $conf['comparison'] === 36) { + // between: + $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue'] ?? '') . '" name="' . $fieldPrefix . '[inputValue]">'; + $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue1'] ?? '') . '" name="' . $fieldPrefix . '[inputValue1]">'; + } else { + $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue'] ?? '') . '" name="' . $fieldPrefix . '[inputValue]">'; + } + $lineHTML[] = '</div>'; + $lineHTML[] = '</div>'; + } + if ($fieldType !== 'ignore') { + $lineHTML[] = '<div class="row row-cols-auto mb-2">'; + $lineHTML[] = '<div class="btn-group">'; + $lineHTML[] = $this->updateIcon(); + if ($loopCount) { + $lineHTML[] = '<button class="btn btn-default" title="Remove condition" name="qG_del' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-delete', Icon::SIZE_SMALL)->render() . '</button>'; + } + $lineHTML[] = '<button class="btn btn-default" title="Add condition" name="qG_ins' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-plus', Icon::SIZE_SMALL)->render() . '</button>'; + if ($c != 0) { + $lineHTML[] = '<button class="btn btn-default" title="Move up" name="qG_up' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-chevron-up', Icon::SIZE_SMALL)->render() . '</button>'; + } + if ($c != 0 && $fieldType !== 'newlevel') { + $lineHTML[] = '<button class="btn btn-default" title="New level" name="qG_nl' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-chevron-right', Icon::SIZE_SMALL)->render() . '</button>'; + } + if ($fieldType === 'newlevel') { + $lineHTML[] = '<button class="btn btn-default" title="Collapse new level" name="qG_remnl' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-chevron-left', Icon::SIZE_SMALL)->render() . '</button>'; + } + $lineHTML[] = '</div>'; + $lineHTML[] = '</div>'; + $codeArr[$arrCount]['html'] = implode(LF, $lineHTML); + $codeArr[$arrCount]['query'] = $this->getQuerySingle($conf, $c === 0); + $arrCount++; + $c++; + } + $loopCount = 1; + } + $this->queryConfig = $queryConfig; + return $codeArr; + } + + protected function getDateTimePickerField(string $name, string $timestamp, string $type): string + { + $value = strtotime($timestamp) ? date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)strtotime($timestamp)) : ''; + $id = StringUtility::getUniqueId('dt_'); + $html = []; + $html[] = '<div class="col mb-sm-2">'; + $html[] = ' <div class="input-group" id="' . $id . '-wrapper">'; + $html[] = ' <input data-formengine-input-name="' . htmlspecialchars($name) . '" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="' . htmlspecialchars($type) . '" type="text" id="' . $id . '">'; + $html[] = ' <input name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($timestamp) . '" type="hidden">'; + $html[] = ' <button class="btn btn-default" type="button" data-global-event="click" data-action-focus="#' . $id . '">'; + $html[] = $this->iconFactory->getIcon('actions-calendar-alternative', Icon::SIZE_SMALL)->render(); + $html[] = ' </button>'; + $html[] = ' </div>'; + $html[] = '</div>'; + return implode(LF, $html); + } + + protected function makeOptionList(string $fieldName, array $conf, string $table): string + { + $backendUserAuthentication = $this->getBackendUserAuthentication(); + $from_table_Arr = []; + $out = []; + $fieldSetup = $this->fields[$fieldName]; + $languageService = $this->getLanguageService(); + if ($fieldSetup['type'] === 'multiple') { + $optGroupOpen = false; + foreach (($fieldSetup['items'] ?? []) as $val) { + $value = $languageService->sL($val[0]); + if ($val[1] === '--div--') { + if ($optGroupOpen) { + $out[] = '</optgroup>'; + } + $optGroupOpen = true; + $out[] = '<optgroup label="' . htmlspecialchars($value) . '">'; + } elseif (GeneralUtility::inList($conf['inputValue'], $val[1])) { + $out[] = '<option value="' . htmlspecialchars($val[1]) . '" selected>' . htmlspecialchars($value) . '</option>'; + } else { + $out[] = '<option value="' . htmlspecialchars($val[1]) . '">' . htmlspecialchars($value) . '</option>'; + } + } + if ($optGroupOpen) { + $out[] = '</optgroup>'; + } + } + if ($fieldSetup['type'] === 'binary') { + foreach ($fieldSetup['items'] as $key => $val) { + $value = $languageService->sL($val[0]); + if (GeneralUtility::inList($conf['inputValue'], (string)(2 ** $key))) { + $out[] = '<option value="' . 2 ** $key . '" selected>' . htmlspecialchars($value) . '</option>'; + } else { + $out[] = '<option value="' . 2 ** $key . '">' . htmlspecialchars($value) . '</option>'; + } + } + } + if ($fieldSetup['type'] === 'relation') { + $useTablePrefix = 0; + $dontPrefixFirstTable = 0; + foreach (($fieldSetup['items'] ?? []) as $val) { + $value = $languageService->sL($val[0]); + if (GeneralUtility::inList($conf['inputValue'], $val[1])) { + $out[] = '<option value="' . htmlspecialchars($val[1]) . '" selected>' . htmlspecialchars($value) . '</option>'; + } else { + $out[] = '<option value="' . htmlspecialchars($val[1]) . '">' . htmlspecialchars($value) . '</option>'; + } + } + $allowedFields = $fieldSetup['allowed'] ?? ''; + if (str_contains($allowedFields, ',')) { + $from_table_Arr = explode(',', $allowedFields); + $useTablePrefix = 1; + if (!$fieldSetup['prepend_tname']) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $statement = $queryBuilder->select($fieldName) + ->from($table) + ->executeQuery(); + while ($row = $statement->fetchAssociative()) { + if (str_contains($row[$fieldName], ',')) { + $checkContent = explode(',', $row[$fieldName]); + foreach ($checkContent as $singleValue) { + if (!str_contains($singleValue, '_')) { + $dontPrefixFirstTable = 1; + } + } + } else { + $singleValue = $row[$fieldName]; + if ($singleValue !== '' && !str_contains($singleValue, '_')) { + $dontPrefixFirstTable = 1; + } + } + } + } + } else { + $from_table_Arr[0] = $allowedFields; + } + if (!empty($fieldSetup['prepend_tname'])) { + $useTablePrefix = 1; + } + if (!empty($fieldSetup['foreign_table'])) { + $from_table_Arr[0] = $fieldSetup['foreign_table']; + } + $counter = 0; + $tablePrefix = ''; + $outArray = []; + $labelFieldSelect = []; + foreach ($from_table_Arr as $from_table) { + $useSelectLabels = false; + $useAltSelectLabels = false; + if ($useTablePrefix && !$dontPrefixFirstTable && $counter != 1 || $counter === 1) { + $tablePrefix = $from_table . '_'; + } + $counter = 1; + if (is_array($GLOBALS['TCA'][$from_table])) { + $labelField = $GLOBALS['TCA'][$from_table]['ctrl']['label'] ?? ''; + $altLabelField = $GLOBALS['TCA'][$from_table]['ctrl']['label_alt'] ?? ''; + if ($GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items'] ?? false) { + foreach ($GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items'] as $labelArray) { + $labelFieldSelect[$labelArray[1]] = $languageService->sL($labelArray[0]); + } + $useSelectLabels = true; + } + $altLabelFieldSelect = []; + if ($GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items'] ?? false) { + foreach ($GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items'] as $altLabelArray) { + $altLabelFieldSelect[$altLabelArray[1]] = $languageService->sL($altLabelArray[0]); + } + $useAltSelectLabels = true; + } + + if (!($this->tableArray[$from_table] ?? false)) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($from_table); + $queryBuilder->getRestrictions()->removeAll(); + if (empty($this->MOD_SETTINGS['show_deleted'])) { + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + } + $selectFields = ['uid', $labelField]; + if ($altLabelField) { + $selectFields = array_merge($selectFields, GeneralUtility::trimExplode(',', $altLabelField, true)); + } + $queryBuilder->select(...$selectFields) + ->from($from_table) + ->orderBy('uid'); + if (!$backendUserAuthentication->isAdmin()) { + $webMounts = $backendUserAuthentication->returnWebmounts(); + $perms_clause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); + $webMountPageTree = ''; + $webMountPageTreePrefix = ''; + foreach ($webMounts as $webMount) { + if ($webMountPageTree) { + $webMountPageTreePrefix = ','; + } + $webMountPageTree .= $webMountPageTreePrefix + . $this->getTreeList($webMount, 999, 0, $perms_clause); + } + if ($from_table === 'pages') { + $queryBuilder->where( + QueryHelper::stripLogicalOperatorPrefix($perms_clause), + $queryBuilder->expr()->in( + 'uid', + $queryBuilder->createNamedParameter( + GeneralUtility::intExplode(',', $webMountPageTree), + Connection::PARAM_INT_ARRAY + ) + ) + ); + } else { + $queryBuilder->where( + $queryBuilder->expr()->in( + 'pid', + $queryBuilder->createNamedParameter( + GeneralUtility::intExplode(',', $webMountPageTree), + Connection::PARAM_INT_ARRAY + ) + ) + ); + } + } + $statement = $queryBuilder->executeQuery(); + $this->tableArray[$from_table] = $statement->fetchAllAssociative(); + } + + foreach (($this->tableArray[$from_table] ?? []) as $val) { + if ($useSelectLabels) { + $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($labelFieldSelect[$val[$labelField]]); + } elseif ($val[$labelField]) { + $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($val[$labelField]); + } elseif ($useAltSelectLabels) { + $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($altLabelFieldSelect[$val[$altLabelField]]); + } else { + $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($val[$altLabelField]); + } + } + if (isset($this->MOD_SETTINGS['options_sortlabel']) && $this->MOD_SETTINGS['options_sortlabel'] && is_array($outArray)) { + natcasesort($outArray); + } + } + } + foreach ($outArray as $key2 => $val2) { + if (GeneralUtility::inList($conf['inputValue'], $key2)) { + $out[] = '<option value="' . htmlspecialchars($key2) . '" selected>[' . htmlspecialchars($key2) . '] ' . htmlspecialchars($val2) . '</option>'; + } else { + $out[] = '<option value="' . htmlspecialchars($key2) . '">[' . htmlspecialchars($key2) . '] ' . htmlspecialchars($val2) . '</option>'; + } + } + } + return implode(LF, $out); + } + + protected function mkOperatorSelect(string $name, string $op, bool $draw, bool $submit): string + { + $out = []; + if ($draw) { + $out[] = '<div class="row row-cols-auto mb-2">'; + $out[] = ' <div class="col">'; + $out[] = ' <select class="form-select' . ($submit ? ' t3js-submit-change' : '') . '" name="' . htmlspecialchars($name) . '[operator]">'; + $out[] = ' <option value="AND"' . (!$op || $op === 'AND' ? ' selected' : '') . '>' . htmlspecialchars($this->lang['AND']) . '</option>'; + $out[] = ' <option value="OR"' . ($op === 'OR' ? ' selected' : '') . '>' . htmlspecialchars($this->lang['OR']) . '</option>'; + $out[] = ' </select>'; + $out[] = ' </div>'; + $out[] = '</div>'; + } else { + $out[] = '<input type="hidden" value="' . htmlspecialchars($op) . '" name="' . htmlspecialchars($name) . '[operator]">'; + } + return implode(LF, $out); + } + + protected function makeComparisonSelector(string $subscript, string $fieldName, array $conf): string + { + $fieldPrefix = $this->name . $subscript; + $lineHTML = []; + $lineHTML[] = '<div class="col mb-sm-2">'; + $lineHTML[] = $this->mkTypeSelect($fieldPrefix . '[type]', $fieldName); + $lineHTML[] = '</div>'; + $lineHTML[] = '<div class="col mb-sm-2">'; + $lineHTML[] = ' <div class="input-group">'; + $lineHTML[] = $this->mkCompSelect($fieldPrefix . '[comparison]', (string)$conf['comparison'], ($conf['negate'] ?? null) ? 1 : 0); + $lineHTML[] = ' <span class="input-group-addon">'; + $lineHTML[] = ' <input type="checkbox" class="checkbox t3js-submit-click"' . (($conf['negate'] ?? null) ? ' checked' : '') . ' name="' . htmlspecialchars($fieldPrefix) . '[negate]">'; + $lineHTML[] = ' </span>'; + $lineHTML[] = ' </div>'; + $lineHTML[] = ' </div>'; + return implode(LF, $lineHTML); + } + + protected function mkCompSelect(string $name, string $comparison, int $neg): string + { + $compOffSet = $comparison >> 5; + $out = []; + $out[] = '<select class="form-select t3js-submit-change" name="' . $name . '">'; + for ($i = 32 * $compOffSet + $neg; $i < 32 * ($compOffSet + 1); $i += 2) { + if ($this->lang['comparison'][$i . '_'] ?? false) { + $out[] = '<option value="' . $i . '"' . ($i >> 1 === $comparison >> 1 ? ' selected' : '') . '>' . htmlspecialchars($this->lang['comparison'][$i . '_']) . '</option>'; + } + } + $out[] = '</select>'; + return implode(LF, $out); + } + + protected function printCodeArray(array $codeArr, int $recursionLevel = 0): string + { + $out = []; + foreach (array_values($codeArr) as $queryComponent) { + $out[] = '<div class="card">'; + $out[] = '<div class="card-body pb-2">'; + $out[] = $queryComponent['html']; + + if ($this->enableQueryParts) { + $out[] = '<div class="row row-cols-auto mb-2">'; + $out[] = '<div class="col">'; + $out[] = '<code class="m-0">'; + $out[] = htmlspecialchars($queryComponent['query']); + $out[] = '</code>'; + $out[] = '</div>'; + $out[] = '</div>'; + } + if (is_array($queryComponent['sub'] ?? null)) { + $out[] = '<div class="mb-2">'; + $out[] = $this->printCodeArray($queryComponent['sub'], $recursionLevel + 1); + $out[] = '</div>'; + } + $out[] = '</div>'; + $out[] = '</div>'; + } + return implode(LF, $out); + } + + protected function mkFieldToInputSelect(string $name, string $fieldName): string + { + $out = []; + $out[] = '<div class="input-group mb-2">'; + $out[] = ' <span class="input-group-btn">'; + $out[] = $this->updateIcon(); + $out[] = ' </span>'; + $out[] = ' <input type="text" class="form-control t3js-clearable" value="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($name) . '">'; + $out[] = '</div>'; + + $out[] = '<select class="form-select t3js-addfield" name="_fieldListDummy" size="5" data-field="' . htmlspecialchars($name) . '">'; + foreach ($this->fields as $key => $value) { + if (!$value['exclude'] || $this->getBackendUserAuthentication()->check('non_exclude_fields', $this->table . ':' . $key)) { + $label = $this->fields[$key]['label']; + if ($this->showFieldAndTableNames) { + $label .= ' [' . $key . ']'; + } + $out[] = '<option value="' . htmlspecialchars($key) . '"' . ($key === $fieldName ? ' selected' : '') . '>' . htmlspecialchars($label) . '</option>'; + } + } + $out[] = '</select>'; + return implode(LF, $out); + } + + protected function procesData(array $qC = []): void + { + $this->queryConfig = $qC; + $POST = GeneralUtility::_POST(); + // If delete... + if ($POST['qG_del'] ?? false) { + // Initialize array to work on, save special parameters + $ssArr = $this->getSubscript($POST['qG_del']); + $workArr = &$this->queryConfig; + $ssArrSize = count($ssArr) - 1; + $i = 0; + for (; $i < $ssArrSize; $i++) { + $workArr = &$workArr[$ssArr[$i]]; + } + // Delete the entry and move the other entries + unset($workArr[$ssArr[$i]]); + $workArrSize = count((array)$workArr); + for ($j = $ssArr[$i]; $j < $workArrSize; $j++) { + $workArr[$j] = $workArr[$j + 1]; + unset($workArr[$j + 1]); + } + } + // If insert... + if ($POST['qG_ins'] ?? false) { + // Initialize array to work on, save special parameters + $ssArr = $this->getSubscript($POST['qG_ins']); + $workArr = &$this->queryConfig; + $ssArrSize = count($ssArr) - 1; + $i = 0; + for (; $i < $ssArrSize; $i++) { + $workArr = &$workArr[$ssArr[$i]]; + } + // Move all entries above position where new entry is to be inserted + $workArrSize = count((array)$workArr); + for ($j = $workArrSize; $j > $ssArr[$i]; $j--) { + $workArr[$j] = $workArr[$j - 1]; + } + // Clear new entry position + unset($workArr[$ssArr[$i] + 1]); + $workArr[$ssArr[$i] + 1]['type'] = 'FIELD_'; + } + // If move up... + if ($POST['qG_up'] ?? false) { + // Initialize array to work on + $ssArr = $this->getSubscript($POST['qG_up']); + $workArr = &$this->queryConfig; + $ssArrSize = count($ssArr) - 1; + $i = 0; + for (; $i < $ssArrSize; $i++) { + $workArr = &$workArr[$ssArr[$i]]; + } + // Swap entries + $qG_tmp = $workArr[$ssArr[$i]]; + $workArr[$ssArr[$i]] = $workArr[$ssArr[$i] - 1]; + $workArr[$ssArr[$i] - 1] = $qG_tmp; + } + // If new level... + if ($POST['qG_nl'] ?? false) { + // Initialize array to work on + $ssArr = $this->getSubscript($POST['qG_nl']); + $workArr = &$this->queryConfig; + $ssArraySize = count($ssArr) - 1; + $i = 0; + for (; $i < $ssArraySize; $i++) { + $workArr = &$workArr[$ssArr[$i]]; + } + // Do stuff: + $tempEl = $workArr[$ssArr[$i]]; + if (is_array($tempEl)) { + if ($tempEl['type'] !== 'newlevel') { + $workArr[$ssArr[$i]] = [ + 'type' => 'newlevel', + 'operator' => $tempEl['operator'], + 'nl' => [$tempEl], + ]; + } + } + } + // If collapse level... + if ($POST['qG_remnl'] ?? false) { + // Initialize array to work on + $ssArr = $this->getSubscript($POST['qG_remnl']); + $workArr = &$this->queryConfig; + $ssArrSize = count($ssArr) - 1; + $i = 0; + for (; $i < $ssArrSize; $i++) { + $workArr = &$workArr[$ssArr[$i]]; + } + // Do stuff: + $tempEl = $workArr[$ssArr[$i]]; + if (is_array($tempEl)) { + if ($tempEl['type'] === 'newlevel' && is_array($workArr)) { + $a1 = array_slice($workArr, 0, $ssArr[$i]); + $a2 = array_slice($workArr, $ssArr[$i]); + array_shift($a2); + $a3 = $tempEl['nl']; + $a3[0]['operator'] = $tempEl['operator']; + $workArr = array_merge($a1, $a3, $a2); + } + } + } + } + + protected function getSubscript($arr): array + { + $retArr = []; + while (\is_array($arr)) { + reset($arr); + $key = key($arr); + $retArr[] = $key; + if (isset($arr[$key])) { + $arr = $arr[$key]; + } else { + break; + } + } + return $retArr; + } + + protected function getLabelCol(): string + { + return $GLOBALS['TCA'][$this->table]['ctrl']['label']; + } + + protected function mkTypeSelect(string $name, string $fieldName, string $prepend = 'FIELD_'): string + { + $out = []; + $out[] = '<select class="form-select t3js-submit-change" name="' . htmlspecialchars($name) . '">'; + $out[] = '<option value=""></option>'; + foreach ($this->fields as $key => $value) { + if (!($value['exclude'] ?? false) || $this->getBackendUserAuthentication()->check('non_exclude_fields', $this->table . ':' . $key)) { + $label = $this->fields[$key]['label']; + if ($this->showFieldAndTableNames) { + $label .= ' [' . $key . ']'; + } + $out[] = '<option value="' . htmlspecialchars($prepend . $key) . '"' . ($key === $fieldName ? ' selected' : '') . '>' . htmlspecialchars($label) . '</option>'; + } + } + $out[] = '</select>'; + return implode(LF, $out); + } + + protected function updateIcon(): string + { + return '<button class="btn btn-default" title="Update" name="just_update">' . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</button>'; + } + + protected function setAndCleanUpExternalLists(string $name, string $list, string $force = ''): void + { + $fields = array_unique(GeneralUtility::trimExplode(',', $list . ',' . $force, true)); + $reList = []; + foreach ($fields as $fieldName) { + if (isset($this->fields[$fieldName])) { + $reList[] = $fieldName; + } + } + $this->extFieldLists[$name] = implode(',', $reList); + } + + protected function mkTableSelect(string $name, string $cur): string + { + $out = []; + $out[] = '<select class="form-select t3js-submit-change" name="' . $name . '">'; + $out[] = '<option value=""></option>'; + foreach ($GLOBALS['TCA'] as $tN => $value) { + if ($this->getBackendUserAuthentication()->check('tables_select', $tN)) { + $label = $this->getLanguageService()->sL($GLOBALS['TCA'][$tN]['ctrl']['title']); + if ($this->showFieldAndTableNames) { + $label .= ' [' . $tN . ']'; + } + $out[] = '<option value="' . htmlspecialchars($tN) . '"' . ($tN === $cur ? ' selected' : '') . '>' . htmlspecialchars($label) . '</option>'; + } + } + $out[] = '</select>'; + return implode(LF, $out); + } + + /** + * @param array $settings Module settings like checkboxes in the interface + */ + protected function init(string $name, string $table, string $fieldList = '', array $settings = []): void + { + // Analysing the fields in the table. + if (is_array($GLOBALS['TCA'][$table] ?? false)) { + $this->name = $name; + $this->table = $table; + $this->fieldList = $fieldList ?: $this->makeFieldList(); + $this->MOD_SETTINGS = $settings; + $fieldArr = GeneralUtility::trimExplode(',', $this->fieldList, true); + foreach ($fieldArr as $fieldName) { + $fC = $GLOBALS['TCA'][$this->table]['columns'][$fieldName] ?? []; + $this->fields[$fieldName] = $fC['config'] ?? []; + $this->fields[$fieldName]['exclude'] = $fC['exclude'] ?? ''; + if (($this->fields[$fieldName]['type'] ?? '') === 'user' && !isset($this->fields[$fieldName]['type']['userFunc']) + || ($this->fields[$fieldName]['type'] ?? '') === 'none' + ) { + // Do not list type=none "virtual" fields or query them from db, + // and if type is user without defined userFunc + unset($this->fields[$fieldName]); + continue; + } + if (is_array($fC) && ($fC['label'] ?? false)) { + $this->fields[$fieldName]['label'] = rtrim(trim($this->getLanguageService()->sL($fC['label'])), ':'); + switch ($this->fields[$fieldName]['type']) { + case 'input': + if (preg_match('/int|year/i', ($this->fields[$fieldName]['eval'] ?? ''))) { + $this->fields[$fieldName]['type'] = 'number'; + } else { + $this->fields[$fieldName]['type'] = 'text'; + } + break; + case 'number': + // Empty on purpose, we have to keep the type "number". + // Falling back to the "default" case would set the type to "text" + break; + case 'datetime': + if (!in_array($this->fields[$fieldName]['dbType'] ?? '', QueryHelper::getDateTimeTypes(), true)) { + $this->fields[$fieldName]['type'] = 'number'; + } elseif ($this->fields[$fieldName]['dbType'] === 'time') { + $this->fields[$fieldName]['type'] = 'time'; + } else { + $this->fields[$fieldName]['type'] = 'date'; + } + break; + case 'check': + if (count($this->fields[$fieldName]['items'] ?? []) <= 1) { + $this->fields[$fieldName]['type'] = 'boolean'; + } else { + $this->fields[$fieldName]['type'] = 'binary'; + } + break; + case 'radio': + $this->fields[$fieldName]['type'] = 'multiple'; + break; + case 'select': + case 'category': + $this->fields[$fieldName]['type'] = 'multiple'; + if ($this->fields[$fieldName]['foreign_table'] ?? false) { + $this->fields[$fieldName]['type'] = 'relation'; + } + if ($this->fields[$fieldName]['special'] ?? false) { + $this->fields[$fieldName]['type'] = 'text'; + } + break; + case 'group': + $this->fields[$fieldName]['type'] = 'relation'; + break; + case 'user': + case 'flex': + case 'passthrough': + case 'none': + case 'text': + case 'email': + case 'link': + case 'password': + case 'color': + default: + $this->fields[$fieldName]['type'] = 'text'; + } + } else { + $this->fields[$fieldName]['label'] = '[FIELD: ' . $fieldName . ']'; + switch ($fieldName) { + case 'pid': + $this->fields[$fieldName]['type'] = 'relation'; + $this->fields[$fieldName]['allowed'] = 'pages'; + break; + case 'tstamp': + case 'crdate': + $this->fields[$fieldName]['type'] = 'time'; + break; + case 'deleted': + $this->fields[$fieldName]['type'] = 'boolean'; + break; + default: + $this->fields[$fieldName]['type'] = 'number'; + } + } + } + } + /* // EXAMPLE: + $this->queryConfig = array( + array( + 'operator' => 'AND', + 'type' => 'FIELD_space_before_class', + ), + array( + 'operator' => 'AND', + 'type' => 'FIELD_records', + 'negate' => 1, + 'inputValue' => 'foo foo' + ), + array( + 'type' => 'newlevel', + 'nl' => array( + array( + 'operator' => 'AND', + 'type' => 'FIELD_space_before_class', + 'negate' => 1, + 'inputValue' => 'foo foo' + ), + array( + 'operator' => 'AND', + 'type' => 'FIELD_records', + 'negate' => 1, + 'inputValue' => 'foo foo' + ) + ) + ), + array( + 'operator' => 'OR', + 'type' => 'FIELD_maillist', + ) + ); + */ + } + + protected function makeFieldList(): string + { + $fieldListArr = []; + if (is_array($GLOBALS['TCA'][$this->table])) { + $fieldListArr = array_keys($GLOBALS['TCA'][$this->table]['columns'] ?? []); + $fieldListArr[] = 'uid'; + $fieldListArr[] = 'pid'; + $fieldListArr[] = 'deleted'; + if ($GLOBALS['TCA'][$this->table]['ctrl']['tstamp'] ?? false) { + $fieldListArr[] = $GLOBALS['TCA'][$this->table]['ctrl']['tstamp']; + } + if ($GLOBALS['TCA'][$this->table]['ctrl']['crdate'] ?? false) { + $fieldListArr[] = $GLOBALS['TCA'][$this->table]['ctrl']['crdate']; + } + if ($GLOBALS['TCA'][$this->table]['ctrl']['sortby'] ?? false) { + $fieldListArr[] = $GLOBALS['TCA'][$this->table]['ctrl']['sortby']; + } + } + return implode(',', $fieldListArr); + } + + protected function makeStoreControl(): string + { + // Load/Save + $storeArray = $this->initStoreArray(); + + $opt = []; + foreach ($storeArray as $k => $v) { + $opt[] = '<option value="' . htmlspecialchars((string)$k) . '">' . htmlspecialchars((string)$v) . '</option>'; + } + + $markup = []; + $markup[] = '<div class="load-queries">'; + $markup[] = ' <div class="row row-cols-auto">'; + $markup[] = ' <div class="col">'; + $markup[] = ' <select class="form-select" name="storeControl[STORE]" data-assign-store-control-title>' . implode(LF, $opt) . '</select>'; + $markup[] = ' </div>'; + $markup[] = ' <div class="col">'; + $markup[] = ' <input class="form-control" name="storeControl[title]" value="" type="text" max="80">'; + $markup[] = ' </div>'; + $markup[] = ' <div class="col">'; + $markup[] = ' <input class="btn btn-default" type="submit" name="storeControl[LOAD]" value="Load">'; + $markup[] = ' </div>'; + $markup[] = ' <div class="col">'; + $markup[] = ' <input class="btn btn-default" type="submit" name="storeControl[SAVE]" value="Save">'; + $markup[] = ' </div>'; + $markup[] = ' <div class="col">'; + $markup[] = ' <input class="btn btn-default" type="submit" name="storeControl[REMOVE]" value="Remove">'; + $markup[] = ' </div>'; + $markup[] = ' </div>'; + $markup[] = '</div>'; + + return implode(LF, $markup); + } + + protected function procesStoreControl(): string + { + $languageService = $this->getLanguageService(); + $flashMessage = null; + $storeArray = $this->initStoreArray(); + $storeQueryConfigs = (array)(unserialize($this->MOD_SETTINGS['storeQueryConfigs'] ?? '', ['allowed_classes' => false])); + $storeControl = GeneralUtility::_GP('storeControl'); + $storeIndex = (int)($storeControl['STORE'] ?? 0); + $saveStoreArray = 0; + $writeArray = []; + $msg = ''; + if (is_array($storeControl)) { + if ($storeControl['LOAD'] ?? false) { + if ($storeIndex > 0) { + $writeArray = $this->loadStoreQueryConfigs($storeQueryConfigs, $storeIndex, $writeArray); + $saveStoreArray = 1; + $flashMessage = GeneralUtility::makeInstance( + FlashMessage::class, + sprintf($languageService->getLL('query_loaded'), $storeArray[$storeIndex]) + ); + } + } elseif ($storeControl['SAVE'] ?? false) { + if (trim($storeControl['title'])) { + if ($storeIndex > 0) { + $storeArray[$storeIndex] = $storeControl['title']; + } else { + $storeArray[] = $storeControl['title']; + end($storeArray); + $storeIndex = key($storeArray); + } + $storeQueryConfigs = $this->addToStoreQueryConfigs($storeQueryConfigs, (int)$storeIndex); + $saveStoreArray = 1; + $flashMessage = GeneralUtility::makeInstance( + FlashMessage::class, + $languageService->getLL('query_saved') + ); + } + } elseif ($storeControl['REMOVE'] ?? false) { + if ($storeIndex > 0) { + $flashMessage = GeneralUtility::makeInstance( + FlashMessage::class, + sprintf($languageService->getLL('query_removed'), $storeArray[$storeControl['STORE']]) + ); + // Removing + unset($storeArray[$storeControl['STORE']]); + $saveStoreArray = 1; + } + } + if (!empty($flashMessage)) { + $msg = GeneralUtility::makeInstance(FlashMessageRendererResolver::class) + ->resolve() + ->render([$flashMessage]); + } + } + if ($saveStoreArray) { + // Making sure, index 0 is not set! + unset($storeArray[0]); + $writeArray['storeArray'] = serialize($storeArray); + $writeArray['storeQueryConfigs'] = + serialize($this->cleanStoreQueryConfigs($storeQueryConfigs, $storeArray)); + $this->MOD_SETTINGS = BackendUtility::getModuleData( + $this->MOD_MENU, + $writeArray, + $this->moduleName, + 'ses' + ); + } + return $msg; + } + + protected function cleanStoreQueryConfigs(array $storeQueryConfigs, array $storeArray): array + { + if (is_array($storeQueryConfigs)) { + foreach ($storeQueryConfigs as $k => $v) { + if (!isset($storeArray[$k])) { + unset($storeQueryConfigs[$k]); + } + } + } + return $storeQueryConfigs; + } + + protected function addToStoreQueryConfigs(array $storeQueryConfigs, int $index): array + { + $keyArr = explode(',', $this->storeList); + $storeQueryConfigs[$index] = []; + foreach ($keyArr as $k) { + $storeQueryConfigs[$index][$k] = $this->MOD_SETTINGS[$k] ?? null; + } + return $storeQueryConfigs; + } + + protected function loadStoreQueryConfigs(array $storeQueryConfigs, int $storeIndex, array $writeArray): array + { + if ($storeQueryConfigs[$storeIndex]) { + $keyArr = explode(',', $this->storeList); + foreach ($keyArr as $k) { + $writeArray[$k] = $storeQueryConfigs[$storeIndex][$k]; + } + } + return $writeArray; + } + + protected function initStoreArray(): array + { + $storeArray = [ + '0' => '[New]', + ]; + $savedStoreArray = unserialize($this->MOD_SETTINGS['storeArray'] ?? '', ['allowed_classes' => false]); + if (is_array($savedStoreArray)) { + $storeArray = array_merge($storeArray, $savedStoreArray); + } + return $storeArray; + } + + protected function form(): string + { + $markup = []; + $markup[] = '<div class="form-group">'; + $markup[] = '<input placeholder="Search Word" class="form-control" type="search" name="SET[sword]" value="' + . htmlspecialchars($this->MOD_SETTINGS['sword'] ?? '') . '">'; + $markup[] = '</div>'; + $markup[] = '<div class="form-group">'; + $markup[] = '<input class="btn btn-default" type="submit" name="submit" value="Search All Records">'; + $markup[] = '</div>'; + return implode(LF, $markup); + } + + protected function search(): string + { + $swords = $this->MOD_SETTINGS['sword'] ?? ''; + $out = ''; + if ($swords) { + foreach ($GLOBALS['TCA'] as $table => $value) { + // Get fields list + $conf = $GLOBALS['TCA'][$table]; + // Avoid querying tables with no columns + if (empty($conf['columns'])) { + continue; + } + $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); + $tableColumns = $connection->createSchemaManager()->listTableColumns($table); + $normalizedTableColumns = []; + $fieldsInDatabase = []; + foreach ($tableColumns as $column) { + $fieldsInDatabase[] = $column->getName(); + $normalizedTableColumns[trim($column->getName(), $connection->getDatabasePlatform()->getIdentifierQuoteCharacter())] = $column; + } + $fields = array_intersect(array_keys($conf['columns']), $fieldsInDatabase); + + $queryBuilder = $connection->createQueryBuilder(); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $queryBuilder->count('*')->from($table); + $likes = []; + $escapedLikeString = '%' . $queryBuilder->escapeLikeWildcards($swords) . '%'; + foreach ($fields as $field) { + $field = trim($field, $connection->getDatabasePlatform()->getIdentifierQuoteCharacter()); + $quotedField = $queryBuilder->quoteIdentifier($field); + $column = $normalizedTableColumns[$field] ?? $normalizedTableColumns[$quotedField] ?? null; + if ($column !== null + && $connection->getDatabasePlatform() instanceof PostgreSQLPlatform + && !in_array($column->getType()->getName(), [Types::STRING, Types::ASCII_STRING, Types::JSON], true) + ) { + if ($column->getType()->getName() === Types::SMALLINT) { + // we need to cast smallint to int first, otherwise text case below won't work + $quotedField .= '::int'; + } + $quotedField .= '::text'; + } + $likes[] = $queryBuilder->expr()->comparison( + $quotedField, + 'LIKE', + $queryBuilder->createNamedParameter($escapedLikeString) + ); + } + $queryBuilder->orWhere(...$likes); + $count = $queryBuilder->executeQuery()->fetchOne(); + + if ($count > 0) { + $queryBuilder = $connection->createQueryBuilder(); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $queryBuilder->select('uid', $conf['ctrl']['label']) + ->from($table) + ->setMaxResults(200); + $likes = []; + foreach ($fields as $field) { + $field = trim($field, $connection->getDatabasePlatform()->getIdentifierQuoteCharacter()); + $quotedField = $queryBuilder->quoteIdentifier($field); + $column = $normalizedTableColumns[$field] ?? $normalizedTableColumns[$quotedField] ?? null; + if ($column !== null + && $connection->getDatabasePlatform() instanceof PostgreSQLPlatform + && !in_array($column->getType()->getName(), [Types::STRING, Types::ASCII_STRING, Types::JSON], true) + ) { + if ($column->getType()->getName() === Types::SMALLINT) { + // we need to cast smallint to int first, otherwise text case below won't work + $quotedField .= '::int'; + } + $quotedField .= '::text'; + } + $likes[] = $queryBuilder->expr()->comparison( + $quotedField, + 'LIKE', + $queryBuilder->createNamedParameter($escapedLikeString) + ); + } + $statement = $queryBuilder->orWhere(...$likes)->executeQuery(); + $lastRow = null; + $rowArr = []; + while ($row = $statement->fetchAssociative()) { + $rowArr[] = $this->resultRowDisplay($row, $conf, $table); + $lastRow = $row; + } + $markup = []; + $markup[] = '<div class="panel panel-default">'; + $markup[] = ' <div class="panel-heading">'; + $markup[] = htmlspecialchars($this->getLanguageService()->sL($conf['ctrl']['title'])) . ' (' . $count . ')'; + $markup[] = ' </div>'; + $markup[] = ' <table class="table table-striped table-hover">'; + $markup[] = $this->resultRowTitles((array)$lastRow, $conf); + $markup[] = implode(LF, $rowArr); + $markup[] = ' </table>'; + $markup[] = '</div>'; + + $out .= implode(LF, $markup); + } + } + } + return $out; + } + /** * Records overview */ @@ -424,6 +2740,11 @@ class DatabaseIntegrityController return $view->renderResponse('Relations'); } + protected function getBackendUserAuthentication(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; diff --git a/typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php b/typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php deleted file mode 100644 index 93392539756d319d0cd2a0e3b636f6368a4f7c6c..0000000000000000000000000000000000000000 --- a/typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php +++ /dev/null @@ -1,2812 +0,0 @@ -<?php - -/* - * 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! - */ - -namespace TYPO3\CMS\Lowlevel\Database; - -use Doctrine\DBAL\Exception as DBALException; -use Doctrine\DBAL\Platforms\PostgreSQLPlatform; -use Doctrine\DBAL\Types\Types; -use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Backend\Routing\Route; -use TYPO3\CMS\Backend\Routing\UriBuilder; -use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Core\Environment; -use TYPO3\CMS\Core\Database\Connection; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\QueryHelper; -use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; -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\FlashMessageRendererResolver; -use TYPO3\CMS\Core\Messaging\FlashMessageService; -use TYPO3\CMS\Core\Type\Bitmask\Permission; -use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; -use TYPO3\CMS\Core\Utility\CsvUtility; -use TYPO3\CMS\Core\Utility\DebugUtility; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\HttpUtility; -use TYPO3\CMS\Core\Utility\MathUtility; -use TYPO3\CMS\Core\Utility\PathUtility; -use TYPO3\CMS\Core\Utility\StringUtility; - -/** - * Class used in module tools/dbint (advanced search) and which may hold code specific for that module - * - * @internal This class is a specific implementation for the lowlevel extension and is not part of the TYPO3's Core API. - */ -class QueryGenerator -{ - /** - * @var string - */ - protected $storeList = 'search_query_smallparts,search_result_labels,labels_noprefix,show_deleted,queryConfig,queryTable,queryFields,queryLimit,queryOrder,queryOrderDesc,queryOrder2,queryOrder2Desc,queryGroup,search_query_makeQuery'; - - /** - * @var int - */ - protected $noDownloadB = 0; - - /** - * @var array - */ - protected $hookArray = []; - - /** - * @var string - */ - protected $formName = ''; - - /** - * @var IconFactory - */ - protected $iconFactory; - - /** - * @var array - */ - protected $tableArray = []; - - /** - * @var array Settings, usually from the controller - */ - protected $settings = []; - - /** - * @var array information on the menu of this module - */ - protected $menuItems = []; - - /** - * @var string - */ - protected $moduleName; - - /** - * @var array - */ - protected $lang = [ - 'OR' => 'or', - 'AND' => 'and', - 'comparison' => [ - // Type = text offset = 0 - '0_' => 'contains', - '1_' => 'does not contain', - '2_' => 'starts with', - '3_' => 'does not start with', - '4_' => 'ends with', - '5_' => 'does not end with', - '6_' => 'equals', - '7_' => 'does not equal', - // Type = number , offset = 32 - '32_' => 'equals', - '33_' => 'does not equal', - '34_' => 'is greater than', - '35_' => 'is less than', - '36_' => 'is between', - '37_' => 'is not between', - '38_' => 'is in list', - '39_' => 'is not in list', - '40_' => 'binary AND equals', - '41_' => 'binary AND does not equal', - '42_' => 'binary OR equals', - '43_' => 'binary OR does not equal', - // Type = multiple, relation, offset = 64 - '64_' => 'equals', - '65_' => 'does not equal', - '66_' => 'contains', - '67_' => 'does not contain', - '68_' => 'is in list', - '69_' => 'is not in list', - '70_' => 'binary AND equals', - '71_' => 'binary AND does not equal', - '72_' => 'binary OR equals', - '73_' => 'binary OR does not equal', - // Type = date,time offset = 96 - '96_' => 'equals', - '97_' => 'does not equal', - '98_' => 'is greater than', - '99_' => 'is less than', - '100_' => 'is between', - '101_' => 'is not between', - '102_' => 'binary AND equals', - '103_' => 'binary AND does not equal', - '104_' => 'binary OR equals', - '105_' => 'binary OR does not equal', - // Type = boolean, offset = 128 - '128_' => 'is True', - '129_' => 'is False', - // Type = binary , offset = 160 - '160_' => 'equals', - '161_' => 'does not equal', - '162_' => 'contains', - '163_' => 'does not contain', - ], - ]; - - /** - * @var array - */ - protected $compSQL = [ - // Type = text offset = 0 - '0' => '#FIELD# LIKE \'%#VALUE#%\'', - '1' => '#FIELD# NOT LIKE \'%#VALUE#%\'', - '2' => '#FIELD# LIKE \'#VALUE#%\'', - '3' => '#FIELD# NOT LIKE \'#VALUE#%\'', - '4' => '#FIELD# LIKE \'%#VALUE#\'', - '5' => '#FIELD# NOT LIKE \'%#VALUE#\'', - '6' => '#FIELD# = \'#VALUE#\'', - '7' => '#FIELD# != \'#VALUE#\'', - // Type = number, offset = 32 - '32' => '#FIELD# = \'#VALUE#\'', - '33' => '#FIELD# != \'#VALUE#\'', - '34' => '#FIELD# > #VALUE#', - '35' => '#FIELD# < #VALUE#', - '36' => '#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#', - '37' => 'NOT (#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#)', - '38' => '#FIELD# IN (#VALUE#)', - '39' => '#FIELD# NOT IN (#VALUE#)', - '40' => '(#FIELD# & #VALUE#)=#VALUE#', - '41' => '(#FIELD# & #VALUE#)!=#VALUE#', - '42' => '(#FIELD# | #VALUE#)=#VALUE#', - '43' => '(#FIELD# | #VALUE#)!=#VALUE#', - // Type = multiple, relation, offset = 64 - '64' => '#FIELD# = \'#VALUE#\'', - '65' => '#FIELD# != \'#VALUE#\'', - '66' => '#FIELD# LIKE \'%#VALUE#%\' AND #FIELD# LIKE \'%#VALUE1#%\'', - '67' => '(#FIELD# NOT LIKE \'%#VALUE#%\' OR #FIELD# NOT LIKE \'%#VALUE1#%\')', - '68' => '#FIELD# IN (#VALUE#)', - '69' => '#FIELD# NOT IN (#VALUE#)', - '70' => '(#FIELD# & #VALUE#)=#VALUE#', - '71' => '(#FIELD# & #VALUE#)!=#VALUE#', - '72' => '(#FIELD# | #VALUE#)=#VALUE#', - '73' => '(#FIELD# | #VALUE#)!=#VALUE#', - // Type = date, offset = 32 - '96' => '#FIELD# = \'#VALUE#\'', - '97' => '#FIELD# != \'#VALUE#\'', - '98' => '#FIELD# > #VALUE#', - '99' => '#FIELD# < #VALUE#', - '100' => '#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#', - '101' => 'NOT (#FIELD# >= #VALUE# AND #FIELD# <= #VALUE1#)', - '102' => '(#FIELD# & #VALUE#)=#VALUE#', - '103' => '(#FIELD# & #VALUE#)!=#VALUE#', - '104' => '(#FIELD# | #VALUE#)=#VALUE#', - '105' => '(#FIELD# | #VALUE#)!=#VALUE#', - // Type = boolean, offset = 128 - '128' => '#FIELD# = \'1\'', - '129' => '#FIELD# != \'1\'', - // Type = binary = 160 - '160' => '#FIELD# = \'#VALUE#\'', - '161' => '#FIELD# != \'#VALUE#\'', - '162' => '(#FIELD# & #VALUE#)=#VALUE#', - '163' => '(#FIELD# & #VALUE#)=0', - ]; - - /** - * @var array - */ - protected $comp_offsets = [ - 'text' => 0, - 'number' => 1, - 'multiple' => 2, - 'relation' => 2, - 'date' => 3, - 'time' => 3, - 'boolean' => 4, - 'binary' => 5, - ]; - - /** - * Form data name prefix - * - * @var string - */ - protected $name; - - /** - * Table for the query - * - * @var string - */ - protected $table; - - /** - * Field list - * - * @var string - */ - protected $fieldList; - - /** - * Array of the fields possible - * - * @var array - */ - protected $fields = []; - - /** - * @var array - */ - protected $extFieldLists = []; - - /** - * The query config - * - * @var array - */ - protected $queryConfig = []; - - /** - * @var bool - */ - protected $enablePrefix = false; - - /** - * @var bool - */ - protected $enableQueryParts = false; - - /** - * @var string - */ - protected $fieldName; - - /** - * If the current user is an admin and $GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] - * is set to true, the names of fields and tables are displayed. - * - * @var bool - */ - protected $showFieldAndTableNames = false; - - public function __construct(array $settings, array $menuItems, string $moduleName) - { - $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_t3lib_fullsearch.xlf'); - $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); - $this->settings = $settings; - $this->menuItems = $menuItems; - $this->moduleName = $moduleName; - $this->showFieldAndTableNames = $this->getBackendUserAuthentication()->shallDisplayDebugInformation(); - } - - /** - * Get form - * - * @return string - */ - public function form() - { - $markup = []; - $markup[] = '<div class="form-group">'; - $markup[] = '<input placeholder="Search Word" class="form-control" type="search" name="SET[sword]" value="' - . htmlspecialchars($this->settings['sword'] ?? '') . '">'; - $markup[] = '</div>'; - $markup[] = '<div class="form-group">'; - $markup[] = '<input class="btn btn-default" type="submit" name="submit" value="Search All Records">'; - $markup[] = '</div>'; - return implode(LF, $markup); - } - - /** - * Query marker - * - * @return string - */ - public function queryMaker(ServerRequestInterface $request) - { - $output = ''; - $this->hookArray = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3lib_fullsearch'] ?? []; - $msg = $this->procesStoreControl(); - $userTsConfig = $this->getBackendUserAuthentication()->getTSConfig(); - if (!($userTsConfig['mod.']['dbint.']['disableStoreControl'] ?? false)) { - $output .= '<h2>Load/Save Query</h2>'; - $output .= '<div>' . $this->makeStoreControl() . '</div>'; - $output .= $msg; - } - // Query Maker: - $this->init('queryConfig', $this->settings['queryTable'] ?? '', '', $this->settings); - if ($this->formName) { - $this->setFormName($this->formName); - } - $tmpCode = $this->makeSelectorTable($this->settings, $request); - $output .= '<div id="query"></div><h2>Make query</h2><div>' . $tmpCode . '</div>'; - $mQ = $this->settings['search_query_makeQuery']; - // Make form elements: - if ($this->table && is_array($GLOBALS['TCA'][$this->table])) { - if ($mQ) { - // Show query - $this->enablePrefix = true; - $queryString = $this->getQuery($this->queryConfig); - $selectQueryString = $this->getSelectQuery($queryString); - $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table); - - $isConnectionMysql = str_starts_with($connection->getServerVersion(), 'MySQL'); - $fullQueryString = ''; - try { - if ($mQ === 'explain' && $isConnectionMysql) { - // EXPLAIN is no ANSI SQL, for now this is only executed on mysql - // @todo: Move away from getSelectQuery() or model differently - $fullQueryString = 'EXPLAIN ' . $selectQueryString; - $dataRows = $connection->executeQuery('EXPLAIN ' . $selectQueryString)->fetchAllAssociative(); - } elseif ($mQ === 'count') { - $queryBuilder = $connection->createQueryBuilder(); - $queryBuilder->getRestrictions()->removeAll(); - if (empty($this->settings['show_deleted'])) { - $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - } - $queryBuilder->count('*') - ->from($this->table) - ->where(QueryHelper::stripLogicalOperatorPrefix($queryString)); - $fullQueryString = $queryBuilder->getSQL(); - $dataRows = [$queryBuilder->executeQuery()->fetchOne()]; - } else { - $fullQueryString = $selectQueryString; - $dataRows = $connection->executeQuery($selectQueryString)->fetchAllAssociative(); - } - if (!($userTsConfig['mod.']['dbint.']['disableShowSQLQuery'] ?? false)) { - $output .= '<h2>SQL query</h2><div><code>' . htmlspecialchars($fullQueryString) . '</code></div>'; - } - $cPR = $this->getQueryResultCode($mQ, $dataRows, $this->table); - $output .= '<h2>' . ($cPR['header'] ?? '') . '</h2><div>' . $cPR['content'] . '</div>'; - } catch (DBALException $e) { - if (!($userTsConfig['mod.']['dbint.']['disableShowSQLQuery'] ?? false)) { - $output .= '<h2>SQL query</h2><div><code>' . htmlspecialchars($fullQueryString) . '</code></div>'; - } - $out = '<p><strong>Error: <span class="text-danger">' - . htmlspecialchars($e->getMessage()) - . '</span></strong></p>'; - $output .= '<h2>SQL error</h2><div>' . $out . '</div>'; - } - } - } - return '<div class="database-query-builder">' . $output . '</div>'; - } - - /** - * Search - * - * @return string - */ - public function search() - { - $swords = $this->settings['sword'] ?? ''; - $out = ''; - if ($swords) { - foreach ($GLOBALS['TCA'] as $table => $value) { - // Get fields list - $conf = $GLOBALS['TCA'][$table]; - // Avoid querying tables with no columns - if (empty($conf['columns'])) { - continue; - } - $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); - $tableColumns = $connection->createSchemaManager()->listTableColumns($table); - $normalizedTableColumns = []; - $fieldsInDatabase = []; - foreach ($tableColumns as $column) { - $fieldsInDatabase[] = $column->getName(); - $normalizedTableColumns[trim($column->getName(), $connection->getDatabasePlatform()->getIdentifierQuoteCharacter())] = $column; - } - $fields = array_intersect(array_keys($conf['columns']), $fieldsInDatabase); - - $queryBuilder = $connection->createQueryBuilder(); - $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - $queryBuilder->count('*')->from($table); - $likes = []; - $escapedLikeString = '%' . $queryBuilder->escapeLikeWildcards($swords) . '%'; - foreach ($fields as $field) { - $field = trim($field, $connection->getDatabasePlatform()->getIdentifierQuoteCharacter()); - $quotedField = $queryBuilder->quoteIdentifier($field); - $column = $normalizedTableColumns[$field] ?? $normalizedTableColumns[$quotedField] ?? null; - if ($column !== null - && $connection->getDatabasePlatform() instanceof PostgreSQLPlatform - && !in_array($column->getType()->getName(), [Types::STRING, Types::ASCII_STRING, Types::JSON], true) - ) { - if ($column->getType()->getName() === Types::SMALLINT) { - // we need to cast smallint to int first, otherwise text case below won't work - $quotedField .= '::int'; - } - $quotedField .= '::text'; - } - $likes[] = $queryBuilder->expr()->comparison( - $quotedField, - 'LIKE', - $queryBuilder->createNamedParameter($escapedLikeString) - ); - } - $queryBuilder->orWhere(...$likes); - $count = $queryBuilder->executeQuery()->fetchOne(); - - if ($count > 0) { - $queryBuilder = $connection->createQueryBuilder(); - $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - $queryBuilder->select('uid', $conf['ctrl']['label']) - ->from($table) - ->setMaxResults(200); - $likes = []; - foreach ($fields as $field) { - $field = trim($field, $connection->getDatabasePlatform()->getIdentifierQuoteCharacter()); - $quotedField = $queryBuilder->quoteIdentifier($field); - $column = $normalizedTableColumns[$field] ?? $normalizedTableColumns[$quotedField] ?? null; - if ($column !== null - && $connection->getDatabasePlatform() instanceof PostgreSQLPlatform - && !in_array($column->getType()->getName(), [Types::STRING, Types::ASCII_STRING, Types::JSON], true) - ) { - if ($column->getType()->getName() === Types::SMALLINT) { - // we need to cast smallint to int first, otherwise text case below won't work - $quotedField .= '::int'; - } - $quotedField .= '::text'; - } - $likes[] = $queryBuilder->expr()->comparison( - $quotedField, - 'LIKE', - $queryBuilder->createNamedParameter($escapedLikeString) - ); - } - $statement = $queryBuilder->orWhere(...$likes)->executeQuery(); - $lastRow = null; - $rowArr = []; - while ($row = $statement->fetchAssociative()) { - $rowArr[] = $this->resultRowDisplay($row, $conf, $table); - $lastRow = $row; - } - $markup = []; - $markup[] = '<div class="panel panel-default">'; - $markup[] = ' <div class="panel-heading">'; - $markup[] = htmlspecialchars($this->getLanguageService()->sL($conf['ctrl']['title'])) . ' (' . $count . ')'; - $markup[] = ' </div>'; - $markup[] = ' <table class="table table-striped table-hover">'; - $markup[] = $this->resultRowTitles((array)$lastRow, $conf); - $markup[] = implode(LF, $rowArr); - $markup[] = ' </table>'; - $markup[] = '</div>'; - - $out .= implode(LF, $markup); - } - } - } - return $out; - } - - /** - * Sets the current name of the input form. - * - * @param string $formName The name of the form. - */ - public function setFormName($formName) - { - $this->formName = trim($formName); - } - - /** - * Make store control - * - * @return string - */ - protected function makeStoreControl() - { - // Load/Save - $storeArray = $this->initStoreArray(); - - $opt = []; - foreach ($storeArray as $k => $v) { - $opt[] = '<option value="' . htmlspecialchars($k) . '">' . htmlspecialchars($v) . '</option>'; - } - - $markup = []; - $markup[] = '<div class="load-queries">'; - $markup[] = ' <div class="row row-cols-auto">'; - $markup[] = ' <div class="col">'; - $markup[] = ' <select class="form-select" name="storeControl[STORE]" data-assign-store-control-title>' . implode(LF, $opt) . '</select>'; - $markup[] = ' </div>'; - $markup[] = ' <div class="col">'; - $markup[] = ' <input class="form-control" name="storeControl[title]" value="" type="text" max="80">'; - $markup[] = ' </div>'; - $markup[] = ' <div class="col">'; - $markup[] = ' <input class="btn btn-default" type="submit" name="storeControl[LOAD]" value="Load">'; - $markup[] = ' </div>'; - $markup[] = ' <div class="col">'; - $markup[] = ' <input class="btn btn-default" type="submit" name="storeControl[SAVE]" value="Save">'; - $markup[] = ' </div>'; - $markup[] = ' <div class="col">'; - $markup[] = ' <input class="btn btn-default" type="submit" name="storeControl[REMOVE]" value="Remove">'; - $markup[] = ' </div>'; - $markup[] = ' </div>'; - $markup[] = '</div>'; - - return implode(LF, $markup); - } - - /** - * Init store array - * - * @return array - */ - protected function initStoreArray() - { - $storeArray = [ - '0' => '[New]', - ]; - $savedStoreArray = unserialize($this->settings['storeArray'] ?? '', ['allowed_classes' => false]); - if (is_array($savedStoreArray)) { - $storeArray = array_merge($storeArray, $savedStoreArray); - } - return $storeArray; - } - - /** - * Clean store query configs - * - * @param array $storeQueryConfigs - * @param array $storeArray - * @return array - */ - protected function cleanStoreQueryConfigs($storeQueryConfigs, $storeArray) - { - if (is_array($storeQueryConfigs)) { - foreach ($storeQueryConfigs as $k => $v) { - if (!isset($storeArray[$k])) { - unset($storeQueryConfigs[$k]); - } - } - } - return $storeQueryConfigs; - } - - /** - * Add to store query configs - */ - protected function addToStoreQueryConfigs(array $storeQueryConfigs, int $index): array - { - $keyArr = explode(',', $this->storeList); - $storeQueryConfigs[$index] = []; - foreach ($keyArr as $k) { - $storeQueryConfigs[$index][$k] = $this->settings[$k] ?? null; - } - return $storeQueryConfigs; - } - - /** - * Load store query configs - * - * @param array $storeQueryConfigs - * @param int $storeIndex - * @param array $writeArray - * @return array - */ - protected function loadStoreQueryConfigs($storeQueryConfigs, $storeIndex, $writeArray) - { - if ($storeQueryConfigs[$storeIndex]) { - $keyArr = explode(',', $this->storeList); - foreach ($keyArr as $k) { - $writeArray[$k] = $storeQueryConfigs[$storeIndex][$k]; - } - } - return $writeArray; - } - - /** - * Process store control - * - * @return string - */ - protected function procesStoreControl() - { - $languageService = $this->getLanguageService(); - $flashMessage = null; - $storeArray = $this->initStoreArray(); - $storeQueryConfigs = (array)(unserialize($this->settings['storeQueryConfigs'] ?? '', ['allowed_classes' => false])); - $storeControl = GeneralUtility::_GP('storeControl'); - $storeIndex = (int)($storeControl['STORE'] ?? 0); - $saveStoreArray = 0; - $writeArray = []; - $msg = ''; - if (is_array($storeControl)) { - if ($storeControl['LOAD'] ?? false) { - if ($storeIndex > 0) { - $writeArray = $this->loadStoreQueryConfigs($storeQueryConfigs, $storeIndex, $writeArray); - $saveStoreArray = 1; - $flashMessage = GeneralUtility::makeInstance( - FlashMessage::class, - sprintf($languageService->getLL('query_loaded'), $storeArray[$storeIndex]) - ); - } - } elseif ($storeControl['SAVE'] ?? false) { - if (trim($storeControl['title'])) { - if ($storeIndex > 0) { - $storeArray[$storeIndex] = $storeControl['title']; - } else { - $storeArray[] = $storeControl['title']; - end($storeArray); - $storeIndex = key($storeArray); - } - $storeQueryConfigs = $this->addToStoreQueryConfigs($storeQueryConfigs, (int)$storeIndex); - $saveStoreArray = 1; - $flashMessage = GeneralUtility::makeInstance( - FlashMessage::class, - $languageService->getLL('query_saved') - ); - } - } elseif ($storeControl['REMOVE'] ?? false) { - if ($storeIndex > 0) { - $flashMessage = GeneralUtility::makeInstance( - FlashMessage::class, - sprintf($languageService->getLL('query_removed'), $storeArray[$storeControl['STORE']]) - ); - // Removing - unset($storeArray[$storeControl['STORE']]); - $saveStoreArray = 1; - } - } - if (!empty($flashMessage)) { - $msg = GeneralUtility::makeInstance(FlashMessageRendererResolver::class) - ->resolve() - ->render([$flashMessage]); - } - } - if ($saveStoreArray) { - // Making sure, index 0 is not set! - unset($storeArray[0]); - $writeArray['storeArray'] = serialize($storeArray); - $writeArray['storeQueryConfigs'] = - serialize($this->cleanStoreQueryConfigs($storeQueryConfigs, $storeArray)); - $this->settings = BackendUtility::getModuleData( - $this->menuItems, - $writeArray, - $this->moduleName, - 'ses' - ); - } - return $msg; - } - - /** - * Get query result code - * - * @param string $type - * @param array $dataRows Rows to display - * @param string $table - * @return array HTML-code for "header" and "content" - * @throws \TYPO3\CMS\Core\Exception - */ - protected function getQueryResultCode($type, array $dataRows, $table) - { - $out = ''; - $cPR = []; - switch ($type) { - case 'count': - $cPR['header'] = 'Count'; - $cPR['content'] = '<br><strong>' . (int)$dataRows[0] . '</strong> records selected.'; - break; - case 'all': - $rowArr = []; - $dataRow = null; - foreach ($dataRows as $dataRow) { - $rowArr[] = $this->resultRowDisplay($dataRow, $GLOBALS['TCA'][$table], $table); - } - if (is_array($this->hookArray['beforeResultTable'] ?? false)) { - foreach ($this->hookArray['beforeResultTable'] as $_funcRef) { - $out .= GeneralUtility::callUserFunction($_funcRef, $this->settings); - } - } - if (!empty($rowArr)) { - $cPR['header'] = 'Result'; - $out .= '<table class="table table-striped table-hover">' - . $this->resultRowTitles((array)$dataRow, $GLOBALS['TCA'][$table]) . implode(LF, $rowArr) - . '</table>'; - } else { - $this->renderNoResultsFoundMessage(); - } - - $cPR['content'] = $out; - break; - case 'csv': - $rowArr = []; - $first = 1; - foreach ($dataRows as $dataRow) { - if ($first) { - $rowArr[] = $this->csvValues(array_keys($dataRow)); - $first = 0; - } - $rowArr[] = $this->csvValues($dataRow, ',', '"', $GLOBALS['TCA'][$table], $table); - } - if (!empty($rowArr)) { - $cPR['header'] = 'Result'; - $out .= '<textarea name="whatever" rows="20" class="text-monospace" style="width:100%">' - . htmlspecialchars(implode(LF, $rowArr)) - . '</textarea>'; - if (!$this->noDownloadB) { - $out .= '<br><input class="btn btn-default" type="submit" name="download_file" ' - . 'value="Click to download file">'; - } - // Downloads file: - // @todo: args. routing anyone? - if (GeneralUtility::_GP('download_file')) { - $filename = 'TYPO3_' . $table . '_export_' . date('dmy-Hi') . '.csv'; - $mimeType = 'application/octet-stream'; - header('Content-Type: ' . $mimeType); - header('Content-Disposition: attachment; filename=' . $filename); - echo implode(CRLF, $rowArr); - die; - } - } else { - $this->renderNoResultsFoundMessage(); - } - $cPR['content'] = $out; - break; - case 'explain': - default: - foreach ($dataRows as $dataRow) { - $out .= '<br />' . DebugUtility::viewArray($dataRow); - } - $cPR['header'] = 'Explain SQL query'; - $cPR['content'] = $out; - } - return $cPR; - } - /** - * CSV values - * - * @param array $row - * @param string $delim - * @param string $quote - * @param array $conf - * @param string $table - * @return string A single line of CSV - */ - protected function csvValues($row, $delim = ',', $quote = '"', $conf = [], $table = '') - { - $valueArray = $row; - if (($this->settings['search_result_labels'] ?? false) && $table) { - foreach ($valueArray as $key => $val) { - $valueArray[$key] = $this->getProcessedValueExtra($table, $key, $val, $conf, ';'); - } - } - return CsvUtility::csvValues($valueArray, (string)$delim, (string)$quote); - } - - /** - * Result row display - * - * @param array $row - * @param array $conf - * @param string $table - * @return string - */ - protected function resultRowDisplay($row, $conf, $table) - { - $languageService = $this->getLanguageService(); - $out = '<tr>'; - foreach ($row as $fieldName => $fieldValue) { - if (GeneralUtility::inList($this->settings['queryFields'] ?? '', $fieldName) - || !($this->settings['queryFields'] ?? false) - && $fieldName !== 'pid' - && $fieldName !== 'deleted' - ) { - if ($this->settings['search_result_labels'] ?? false) { - $fVnew = $this->getProcessedValueExtra($table, $fieldName, $fieldValue, $conf, '<br />'); - } else { - $fVnew = htmlspecialchars($fieldValue); - } - $out .= '<td>' . $fVnew . '</td>'; - } - } - $out .= '<td>'; - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - - if (!($row['deleted'] ?? false)) { - $out .= '<div class="btn-group" role="group">'; - $url = (string)$uriBuilder->buildUriFromRoute('record_edit', [ - 'edit' => [ - $table => [ - $row['uid'] => 'edit', - ], - ], - 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri() - . HttpUtility::buildQueryString(['SET' => (array)GeneralUtility::_POST('SET')], '&'), - ]); - $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url) . '">' - . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'; - $out .= '</div><div class="btn-group" role="group">'; - $out .= sprintf( - '<a class="btn btn-default" href="#" data-dispatch-action="%s" data-dispatch-args-list="%s">%s</a>', - 'TYPO3.InfoWindow.showItem', - htmlspecialchars($table . ',' . $row['uid']), - $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() - ); - $out .= '</div>'; - } else { - $out .= '<div class="btn-group" role="group">'; - $out .= '<a class="btn btn-default" href="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('tce_db', [ - 'cmd' => [ - $table => [ - $row['uid'] => [ - 'undelete' => 1, - ], - ], - ], - 'redirect' => GeneralUtility::linkThisScript(), - ])) . '" title="' . htmlspecialchars($languageService->getLL('undelete_only')) . '">'; - $out .= $this->iconFactory->getIcon('actions-edit-restore', Icon::SIZE_SMALL)->render() . '</a>'; - $formEngineParameters = [ - 'edit' => [ - $table => [ - $row['uid'] => 'edit', - ], - ], - 'returnUrl' => GeneralUtility::linkThisScript(), - ]; - $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', $formEngineParameters); - $out .= '<a class="btn btn-default" href="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('tce_db', [ - 'cmd' => [ - $table => [ - $row['uid'] => [ - 'undelete' => 1, - ], - ], - ], - 'redirect' => $redirectUrl, - ])) . '" title="' . htmlspecialchars($languageService->getLL('undelete_and_edit')) . '">'; - $out .= $this->iconFactory->getIcon('actions-delete-edit', Icon::SIZE_SMALL)->render() . '</a>'; - $out .= '</div>'; - } - $_params = [$table => $row]; - if (is_array($this->hookArray['additionalButtons'] ?? false)) { - foreach ($this->hookArray['additionalButtons'] as $_funcRef) { - $out .= GeneralUtility::callUserFunction($_funcRef, $_params); - } - } - $out .= '</td></tr>'; - return $out; - } - - /** - * Get processed value extra - * - * @param string $table - * @param string $fieldName - * @param string $fieldValue - * @param array $conf Not used - * @param string $splitString - * @return string - */ - protected function getProcessedValueExtra($table, $fieldName, $fieldValue, $conf, $splitString) - { - $out = ''; - $fields = []; - // Analysing the fields in the table. - if (is_array($GLOBALS['TCA'][$table] ?? null)) { - $fC = $GLOBALS['TCA'][$table]['columns'][$fieldName] ?? null; - $fields = $fC['config'] ?? []; - $fields['exclude'] = $fC['exclude'] ?? ''; - if (is_array($fC) && $fC['label']) { - $fields['label'] = preg_replace('/:$/', '', trim($this->getLanguageService()->sL($fC['label']))); - switch ($fields['type']) { - case 'input': - if (GeneralUtility::inList($fields['eval'] ?? '', 'year')) { - $fields['type'] = 'number'; - } else { - $fields['type'] = 'text'; - } - break; - case 'number': - // Empty on purpose, we have to keep the type "number". - // Falling back to the "default" case would set the type to "text" - break; - case 'datetime': - if (!in_array($fields['dbType'] ?? '', QueryHelper::getDateTimeTypes(), true)) { - $fields['type'] = 'number'; - } elseif ($fields['dbType'] === 'time') { - $fields['type'] = 'time'; - } else { - $fields['type'] = 'date'; - } - break; - case 'check': - if (!($fields['items'] ?? false)) { - $fields['type'] = 'boolean'; - } else { - $fields['type'] = 'binary'; - } - break; - case 'radio': - $fields['type'] = 'multiple'; - break; - case 'select': - case 'category': - $fields['type'] = 'multiple'; - if ($fields['foreign_table'] ?? false) { - $fields['type'] = 'relation'; - } - if ($fields['special'] ?? false) { - $fields['type'] = 'text'; - } - break; - case 'group': - $fields['type'] = 'relation'; - break; - case 'user': - case 'flex': - case 'passthrough': - case 'none': - case 'text': - case 'email': - case 'link': - case 'password': - case 'color': - default: - $fields['type'] = 'text'; - } - } else { - $fields['label'] = '[FIELD: ' . $fieldName . ']'; - switch ($fieldName) { - case 'pid': - $fields['type'] = 'relation'; - $fields['allowed'] = 'pages'; - break; - case 'tstamp': - case 'crdate': - $fields['type'] = 'time'; - break; - default: - $fields['type'] = 'number'; - } - } - } - switch ($fields['type']) { - case 'date': - if ($fieldValue != -1) { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $out = (string)@strftime('%d-%m-%Y', (int)$fieldValue); - } - break; - case 'time': - if ($fieldValue != -1) { - if ($splitString === '<br />') { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $out = (string)@strftime('%H:%M' . $splitString . '%d-%m-%Y', (int)$fieldValue); - } else { - // @todo Replace deprecated strftime in php 8.1. Suppress warning in v11. - $out = (string)@strftime('%H:%M %d-%m-%Y', (int)$fieldValue); - } - } - break; - case 'multiple': - case 'binary': - case 'relation': - $out = $this->makeValueList($fieldName, $fieldValue, $fields, $table, $splitString); - break; - case 'boolean': - $out = $fieldValue ? 'True' : 'False'; - break; - default: - $out = htmlspecialchars($fieldValue); - } - return $out; - } - - /** - * Recursively fetch all descendants of a given page - * - * @param int $id uid of the page - * @param int $depth - * @param int $begin - * @param string $permsClause - * @return string comma separated list of descendant pages - */ - protected function getTreeList($id, $depth, $begin = 0, $permsClause = '') - { - $depth = (int)$depth; - $begin = (int)$begin; - $id = (int)$id; - if ($id < 0) { - $id = abs($id); - } - if ($begin == 0) { - $theList = (string)$id; - } else { - $theList = ''; - } - if ($id && $depth > 0) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); - $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - $statement = $queryBuilder->select('uid') - ->from('pages') - ->where( - $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)), - $queryBuilder->expr()->eq('sys_language_uid', 0) - ) - ->orderBy('uid'); - if ($permsClause !== '') { - $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($permsClause)); - } - $statement = $queryBuilder->executeQuery(); - while ($row = $statement->fetchAssociative()) { - if ($begin <= 0) { - $theList .= ',' . $row['uid']; - } - if ($depth > 1) { - $theSubList = $this->getTreeList($row['uid'], $depth - 1, $begin - 1, $permsClause); - if (!empty($theList) && !empty($theSubList) && ($theSubList[0] !== ',')) { - $theList .= ','; - } - $theList .= $theSubList; - } - } - } - return $theList; - } - - /** - * Make value list - * - * @param string $fieldName - * @param string $fieldValue - * @param array $conf - * @param string $table - * @param string $splitString - * @return string - */ - protected function makeValueList($fieldName, $fieldValue, $conf, $table, $splitString) - { - $backendUserAuthentication = $this->getBackendUserAuthentication(); - $languageService = $this->getLanguageService(); - $from_table_Arr = []; - $fieldSetup = $conf; - $out = ''; - if ($fieldSetup['type'] === 'multiple') { - foreach (($fieldSetup['items'] ?? []) as $val) { - $value = $languageService->sL($val[0]); - if (GeneralUtility::inList($fieldValue, $val[1]) || $fieldValue == $val[1]) { - if ($out !== '') { - $out .= $splitString; - } - $out .= htmlspecialchars($value); - } - } - } - if ($fieldSetup['type'] === 'binary') { - foreach ($fieldSetup['items'] as $val) { - $value = $languageService->sL($val[0]); - if ($out !== '') { - $out .= $splitString; - } - $out .= htmlspecialchars($value); - } - } - if ($fieldSetup['type'] === 'relation') { - $dontPrefixFirstTable = 0; - $useTablePrefix = 0; - foreach (($fieldSetup['items'] ?? []) as $val) { - if (str_starts_with($val[0], 'LLL:')) { - $value = $languageService->sL($val[0]); - } else { - $value = $val[0]; - } - if (GeneralUtility::inList($fieldValue, $value) || $fieldValue == $value) { - if ($out !== '') { - $out .= $splitString; - } - $out .= htmlspecialchars($value); - } - } - if (str_contains($fieldSetup['allowed'] ?? '', ',')) { - $from_table_Arr = explode(',', $fieldSetup['allowed']); - $useTablePrefix = 1; - if (!$fieldSetup['prepend_tname']) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); - $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - $statement = $queryBuilder->select($fieldName)->from($table)->executeQuery(); - while ($row = $statement->fetchAssociative()) { - if (str_contains($row[$fieldName], ',')) { - $checkContent = explode(',', $row[$fieldName]); - foreach ($checkContent as $singleValue) { - if (!str_contains($singleValue, '_')) { - $dontPrefixFirstTable = 1; - } - } - } else { - $singleValue = $row[$fieldName]; - if ($singleValue !== '' && !str_contains($singleValue, '_')) { - $dontPrefixFirstTable = 1; - } - } - } - } - } else { - $from_table_Arr[0] = $fieldSetup['allowed'] ?? null; - } - if (!empty($fieldSetup['prepend_tname'])) { - $useTablePrefix = 1; - } - if (!empty($fieldSetup['foreign_table'])) { - $from_table_Arr[0] = $fieldSetup['foreign_table']; - } - $counter = 0; - $useSelectLabels = 0; - $useAltSelectLabels = 0; - $tablePrefix = ''; - $labelFieldSelect = []; - foreach ($from_table_Arr as $from_table) { - if ($useTablePrefix && !$dontPrefixFirstTable && $counter != 1 || $counter == 1) { - $tablePrefix = $from_table . '_'; - } - $counter = 1; - if (is_array($GLOBALS['TCA'][$from_table] ?? null)) { - $labelField = $GLOBALS['TCA'][$from_table]['ctrl']['label'] ?? ''; - $altLabelField = $GLOBALS['TCA'][$from_table]['ctrl']['label_alt'] ?? ''; - if (is_array($GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items'] ?? false)) { - $items = $GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items']; - foreach ($items as $labelArray) { - $labelFieldSelect[$labelArray[1]] = $languageService->sL($labelArray[0]); - } - $useSelectLabels = 1; - } - $altLabelFieldSelect = []; - if (is_array($GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items'] ?? false)) { - $items = $GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items']; - foreach ($items as $altLabelArray) { - $altLabelFieldSelect[$altLabelArray[1]] = $languageService->sL($altLabelArray[0]); - } - $useAltSelectLabels = 1; - } - - if (empty($this->tableArray[$from_table])) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($from_table); - $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - $selectFields = ['uid', $labelField]; - if ($altLabelField) { - $selectFields = array_merge($selectFields, GeneralUtility::trimExplode(',', $altLabelField, true)); - } - $queryBuilder->select(...$selectFields) - ->from($from_table) - ->orderBy('uid'); - if (!$backendUserAuthentication->isAdmin()) { - $webMounts = $backendUserAuthentication->returnWebmounts(); - $perms_clause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); - $webMountPageTree = ''; - $webMountPageTreePrefix = ''; - foreach ($webMounts as $webMount) { - if ($webMountPageTree) { - $webMountPageTreePrefix = ','; - } - $webMountPageTree .= $webMountPageTreePrefix - . $this->getTreeList($webMount, 999, 0, $perms_clause); - } - if ($from_table === 'pages') { - $queryBuilder->where( - QueryHelper::stripLogicalOperatorPrefix($perms_clause), - $queryBuilder->expr()->in( - 'uid', - $queryBuilder->createNamedParameter( - GeneralUtility::intExplode(',', $webMountPageTree), - Connection::PARAM_INT_ARRAY - ) - ) - ); - } else { - $queryBuilder->where( - $queryBuilder->expr()->in( - 'pid', - $queryBuilder->createNamedParameter( - GeneralUtility::intExplode(',', $webMountPageTree), - Connection::PARAM_INT_ARRAY - ) - ) - ); - } - } - $statement = $queryBuilder->executeQuery(); - $this->tableArray[$from_table] = []; - while ($row = $statement->fetchAssociative()) { - $this->tableArray[$from_table][] = $row; - } - } - - foreach ($this->tableArray[$from_table] as $key => $val) { - $this->settings['labels_noprefix'] = - ($this->settings['labels_noprefix'] ?? '') == 1 - ? 'on' - : $this->settings['labels_noprefix']; - $prefixString = - $this->settings['labels_noprefix'] === 'on' - ? '' - : ' [' . $tablePrefix . $val['uid'] . '] '; - if ($out !== '') { - $out .= $splitString; - } - if (GeneralUtility::inList($fieldValue, $tablePrefix . $val['uid']) - || $fieldValue == $tablePrefix . $val['uid']) { - if ($useSelectLabels) { - $out .= htmlspecialchars($prefixString . $labelFieldSelect[$val[$labelField]]); - } elseif ($val[$labelField]) { - $out .= htmlspecialchars($prefixString . $val[$labelField]); - } elseif ($useAltSelectLabels) { - $out .= htmlspecialchars($prefixString . $altLabelFieldSelect[$val[$altLabelField]]); - } else { - $out .= htmlspecialchars($prefixString . $val[$altLabelField]); - } - } - } - } - } - } - return $out; - } - - /** - * Render table header - * - * @param array|null $row Table columns - * @param array $conf Table TCA - * @return string HTML of table header - */ - protected function resultRowTitles($row, $conf) - { - $languageService = $this->getLanguageService(); - $tableHeader = []; - // Start header row - $tableHeader[] = '<thead><tr>'; - // Iterate over given columns - foreach ($row ?? [] as $fieldName => $fieldValue) { - if (GeneralUtility::inList($this->settings['queryFields'] ?? '', $fieldName) - || !($this->settings['queryFields'] ?? false) - && $fieldName !== 'pid' - && $fieldName !== 'deleted' - ) { - if ($this->settings['search_result_labels'] ?? false) { - $title = $languageService->sL(($conf['columns'][$fieldName]['label'] ?? false) ?: $fieldName); - } else { - $title = $languageService->sL($fieldName); - } - $tableHeader[] = '<th>' . htmlspecialchars($title) . '</th>'; - } - } - // Add empty icon column - $tableHeader[] = '<th></th>'; - // Close header row - $tableHeader[] = '</tr></thead>'; - return implode(LF, $tableHeader); - } - /** - * @throws \InvalidArgumentException - * @throws \TYPO3\CMS\Core\Exception - */ - private function renderNoResultsFoundMessage() - { - $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, 'No rows selected!', '', ContextualFeedbackSeverity::INFO); - $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); - $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); - $defaultFlashMessageQueue->enqueue($flashMessage); - } - - /** - * Make a list of fields for current table - * - * @return string Separated list of fields - */ - protected function makeFieldList() - { - $fieldListArr = []; - if (is_array($GLOBALS['TCA'][$this->table])) { - $fieldListArr = array_keys($GLOBALS['TCA'][$this->table]['columns'] ?? []); - $fieldListArr[] = 'uid'; - $fieldListArr[] = 'pid'; - $fieldListArr[] = 'deleted'; - if ($GLOBALS['TCA'][$this->table]['ctrl']['tstamp'] ?? false) { - $fieldListArr[] = $GLOBALS['TCA'][$this->table]['ctrl']['tstamp']; - } - if ($GLOBALS['TCA'][$this->table]['ctrl']['crdate'] ?? false) { - $fieldListArr[] = $GLOBALS['TCA'][$this->table]['ctrl']['crdate']; - } - if ($GLOBALS['TCA'][$this->table]['ctrl']['sortby'] ?? false) { - $fieldListArr[] = $GLOBALS['TCA'][$this->table]['ctrl']['sortby']; - } - } - return implode(',', $fieldListArr); - } - - /** - * Init function - * - * @param string $name The name - * @param string $table The table name - * @param string $fieldList The field list - * @param array $settings Module settings like checkboxes in the interface - */ - protected function init($name, $table, $fieldList = '', array $settings = []) - { - // Analysing the fields in the table. - if (is_array($GLOBALS['TCA'][$table] ?? false)) { - $this->name = $name; - $this->table = $table; - $this->fieldList = $fieldList ?: $this->makeFieldList(); - $this->settings = $settings; - $fieldArr = GeneralUtility::trimExplode(',', $this->fieldList, true); - foreach ($fieldArr as $fieldName) { - $fC = $GLOBALS['TCA'][$this->table]['columns'][$fieldName] ?? []; - $this->fields[$fieldName] = $fC['config'] ?? []; - $this->fields[$fieldName]['exclude'] = $fC['exclude'] ?? ''; - if (($this->fields[$fieldName]['type'] ?? '') === 'user' && !isset($this->fields[$fieldName]['type']['userFunc']) - || ($this->fields[$fieldName]['type'] ?? '') === 'none' - ) { - // Do not list type=none "virtual" fields or query them from db, - // and if type is user without defined userFunc - unset($this->fields[$fieldName]); - continue; - } - if (is_array($fC) && ($fC['label'] ?? false)) { - $this->fields[$fieldName]['label'] = rtrim(trim($this->getLanguageService()->sL($fC['label'])), ':'); - switch ($this->fields[$fieldName]['type']) { - case 'input': - if (preg_match('/int|year/i', ($this->fields[$fieldName]['eval'] ?? ''))) { - $this->fields[$fieldName]['type'] = 'number'; - } else { - $this->fields[$fieldName]['type'] = 'text'; - } - break; - case 'number': - // Empty on purpose, we have to keep the type "number". - // Falling back to the "default" case would set the type to "text" - break; - case 'datetime': - if (!in_array($this->fields[$fieldName]['dbType'] ?? '', QueryHelper::getDateTimeTypes(), true)) { - $this->fields[$fieldName]['type'] = 'number'; - } elseif ($this->fields[$fieldName]['dbType'] === 'time') { - $this->fields[$fieldName]['type'] = 'time'; - } else { - $this->fields[$fieldName]['type'] = 'date'; - } - break; - case 'check': - if (count($this->fields[$fieldName]['items'] ?? []) <= 1) { - $this->fields[$fieldName]['type'] = 'boolean'; - } else { - $this->fields[$fieldName]['type'] = 'binary'; - } - break; - case 'radio': - $this->fields[$fieldName]['type'] = 'multiple'; - break; - case 'select': - case 'category': - $this->fields[$fieldName]['type'] = 'multiple'; - if ($this->fields[$fieldName]['foreign_table'] ?? false) { - $this->fields[$fieldName]['type'] = 'relation'; - } - if ($this->fields[$fieldName]['special'] ?? false) { - $this->fields[$fieldName]['type'] = 'text'; - } - break; - case 'group': - $this->fields[$fieldName]['type'] = 'relation'; - break; - case 'user': - case 'flex': - case 'passthrough': - case 'none': - case 'text': - case 'email': - case 'link': - case 'password': - case 'color': - default: - $this->fields[$fieldName]['type'] = 'text'; - } - } else { - $this->fields[$fieldName]['label'] = '[FIELD: ' . $fieldName . ']'; - switch ($fieldName) { - case 'pid': - $this->fields[$fieldName]['type'] = 'relation'; - $this->fields[$fieldName]['allowed'] = 'pages'; - break; - case 'tstamp': - case 'crdate': - $this->fields[$fieldName]['type'] = 'time'; - break; - case 'deleted': - $this->fields[$fieldName]['type'] = 'boolean'; - break; - default: - $this->fields[$fieldName]['type'] = 'number'; - } - } - } - } - /* // EXAMPLE: - $this->queryConfig = array( - array( - 'operator' => 'AND', - 'type' => 'FIELD_space_before_class', - ), - array( - 'operator' => 'AND', - 'type' => 'FIELD_records', - 'negate' => 1, - 'inputValue' => 'foo foo' - ), - array( - 'type' => 'newlevel', - 'nl' => array( - array( - 'operator' => 'AND', - 'type' => 'FIELD_space_before_class', - 'negate' => 1, - 'inputValue' => 'foo foo' - ), - array( - 'operator' => 'AND', - 'type' => 'FIELD_records', - 'negate' => 1, - 'inputValue' => 'foo foo' - ) - ) - ), - array( - 'operator' => 'OR', - 'type' => 'FIELD_maillist', - ) - ); - */ - } - - /** - * Set and clean up external lists - * - * @param string $name The name - * @param string $list The list - * @param string $force - */ - protected function setAndCleanUpExternalLists($name, $list, $force = '') - { - $fields = array_unique(GeneralUtility::trimExplode(',', $list . ',' . $force, true)); - $reList = []; - foreach ($fields as $fieldName) { - if (isset($this->fields[$fieldName])) { - $reList[] = $fieldName; - } - } - $this->extFieldLists[$name] = implode(',', $reList); - } - - /** - * Process data - * - * @param array $qC Query config - */ - protected function procesData($qC = []) - { - $this->queryConfig = $qC; - $POST = GeneralUtility::_POST(); - // If delete... - if ($POST['qG_del'] ?? false) { - // Initialize array to work on, save special parameters - $ssArr = $this->getSubscript($POST['qG_del']); - $workArr = &$this->queryConfig; - $ssArrSize = count($ssArr) - 1; - $i = 0; - for (; $i < $ssArrSize; $i++) { - $workArr = &$workArr[$ssArr[$i]]; - } - // Delete the entry and move the other entries - unset($workArr[$ssArr[$i]]); - $workArrSize = count((array)$workArr); - for ($j = $ssArr[$i]; $j < $workArrSize; $j++) { - $workArr[$j] = $workArr[$j + 1]; - unset($workArr[$j + 1]); - } - } - // If insert... - if ($POST['qG_ins'] ?? false) { - // Initialize array to work on, save special parameters - $ssArr = $this->getSubscript($POST['qG_ins']); - $workArr = &$this->queryConfig; - $ssArrSize = count($ssArr) - 1; - $i = 0; - for (; $i < $ssArrSize; $i++) { - $workArr = &$workArr[$ssArr[$i]]; - } - // Move all entries above position where new entry is to be inserted - $workArrSize = count((array)$workArr); - for ($j = $workArrSize; $j > $ssArr[$i]; $j--) { - $workArr[$j] = $workArr[$j - 1]; - } - // Clear new entry position - unset($workArr[$ssArr[$i] + 1]); - $workArr[$ssArr[$i] + 1]['type'] = 'FIELD_'; - } - // If move up... - if ($POST['qG_up'] ?? false) { - // Initialize array to work on - $ssArr = $this->getSubscript($POST['qG_up']); - $workArr = &$this->queryConfig; - $ssArrSize = count($ssArr) - 1; - $i = 0; - for (; $i < $ssArrSize; $i++) { - $workArr = &$workArr[$ssArr[$i]]; - } - // Swap entries - $qG_tmp = $workArr[$ssArr[$i]]; - $workArr[$ssArr[$i]] = $workArr[$ssArr[$i] - 1]; - $workArr[$ssArr[$i] - 1] = $qG_tmp; - } - // If new level... - if ($POST['qG_nl'] ?? false) { - // Initialize array to work on - $ssArr = $this->getSubscript($POST['qG_nl']); - $workArr = &$this->queryConfig; - $ssArraySize = count($ssArr) - 1; - $i = 0; - for (; $i < $ssArraySize; $i++) { - $workArr = &$workArr[$ssArr[$i]]; - } - // Do stuff: - $tempEl = $workArr[$ssArr[$i]]; - if (is_array($tempEl)) { - if ($tempEl['type'] !== 'newlevel') { - $workArr[$ssArr[$i]] = [ - 'type' => 'newlevel', - 'operator' => $tempEl['operator'], - 'nl' => [$tempEl], - ]; - } - } - } - // If collapse level... - if ($POST['qG_remnl'] ?? false) { - // Initialize array to work on - $ssArr = $this->getSubscript($POST['qG_remnl']); - $workArr = &$this->queryConfig; - $ssArrSize = count($ssArr) - 1; - $i = 0; - for (; $i < $ssArrSize; $i++) { - $workArr = &$workArr[$ssArr[$i]]; - } - // Do stuff: - $tempEl = $workArr[$ssArr[$i]]; - if (is_array($tempEl)) { - if ($tempEl['type'] === 'newlevel' && is_array($workArr)) { - $a1 = array_slice($workArr, 0, $ssArr[$i]); - $a2 = array_slice($workArr, $ssArr[$i]); - array_shift($a2); - $a3 = $tempEl['nl']; - $a3[0]['operator'] = $tempEl['operator']; - $workArr = array_merge($a1, $a3, $a2); - } - } - } - } - - /** - * Clean up query config - * - * @param array $queryConfig Query config - * @return array - */ - protected function cleanUpQueryConfig($queryConfig) - { - // Since we don't traverse the array using numeric keys in the upcoming while-loop make sure it's fresh and clean before displaying - if (!empty($queryConfig) && is_array($queryConfig)) { - ksort($queryConfig); - } else { - // queryConfig should never be empty! - if (!isset($queryConfig[0]) || empty($queryConfig[0]['type'])) { - // Make sure queryConfig is an array - $queryConfig = []; - $queryConfig[0] = ['type' => 'FIELD_']; - } - } - // Traverse: - foreach ($queryConfig as $key => $conf) { - $fieldName = ''; - if (str_starts_with(($conf['type'] ?? ''), 'FIELD_')) { - $fieldName = substr($conf['type'], 6); - $fieldType = $this->fields[$fieldName]['type'] ?? ''; - } elseif (($conf['type'] ?? '') === 'newlevel') { - $fieldType = $conf['type']; - } else { - $fieldType = 'ignore'; - } - switch ($fieldType) { - case 'newlevel': - if (!$queryConfig[$key]['nl']) { - $queryConfig[$key]['nl'][0]['type'] = 'FIELD_'; - } - $queryConfig[$key]['nl'] = $this->cleanUpQueryConfig($queryConfig[$key]['nl']); - break; - case 'userdef': - break; - case 'ignore': - default: - $verifiedName = $this->verifyType($fieldName); - $queryConfig[$key]['type'] = 'FIELD_' . $this->verifyType($verifiedName); - if ((int)($conf['comparison'] ?? 0) >> 5 !== (int)($this->comp_offsets[$fieldType] ?? 0)) { - $conf['comparison'] = (int)($this->comp_offsets[$fieldType] ?? 0) << 5; - } - $queryConfig[$key]['comparison'] = $this->verifyComparison($conf['comparison'] ?? '0', ($conf['negate'] ?? null) ? 1 : 0); - $queryConfig[$key]['inputValue'] = $this->cleanInputVal($queryConfig[$key]); - $queryConfig[$key]['inputValue1'] = $this->cleanInputVal($queryConfig[$key], '1'); - } - } - return $queryConfig; - } - - /** - * Get form elements - * - * @param int $subLevel - * @param string $queryConfig - * @param string $parent - * @return array - */ - protected function getFormElements($subLevel = 0, $queryConfig = '', $parent = '') - { - $codeArr = []; - if (!is_array($queryConfig)) { - $queryConfig = $this->queryConfig; - } - $c = 0; - $arrCount = 0; - $loopCount = 0; - foreach ($queryConfig as $key => $conf) { - $fieldName = ''; - $subscript = $parent . '[' . $key . ']'; - $lineHTML = []; - $lineHTML[] = $this->mkOperatorSelect($this->name . $subscript, ($conf['operator'] ?? ''), (bool)$c, ($conf['type'] ?? '') !== 'FIELD_'); - if (str_starts_with(($conf['type'] ?? ''), 'FIELD_')) { - $fieldName = substr($conf['type'], 6); - $this->fieldName = $fieldName; - $fieldType = $this->fields[$fieldName]['type'] ?? ''; - if ((int)($conf['comparison'] ?? 0) >> 5 !== (int)($this->comp_offsets[$fieldType] ?? 0)) { - $conf['comparison'] = (int)($this->comp_offsets[$fieldType] ?? 0) << 5; - } - //nasty nasty... - //make sure queryConfig contains _actual_ comparevalue. - //mkCompSelect don't care, but getQuery does. - $queryConfig[$key]['comparison'] += isset($conf['negate']) - $conf['comparison'] % 2; - } elseif (($conf['type'] ?? '') === 'newlevel') { - $fieldType = $conf['type']; - } else { - $fieldType = 'ignore'; - } - $fieldPrefix = htmlspecialchars($this->name . $subscript); - switch ($fieldType) { - case 'ignore': - break; - case 'newlevel': - if (!$queryConfig[$key]['nl']) { - $queryConfig[$key]['nl'][0]['type'] = 'FIELD_'; - } - $lineHTML[] = '<input type="hidden" name="' . $fieldPrefix . '[type]" value="newlevel">'; - $codeArr[$arrCount]['sub'] = $this->getFormElements($subLevel + 1, $queryConfig[$key]['nl'], $subscript . '[nl]'); - break; - case 'userdef': - $lineHTML[] = ''; - break; - case 'date': - $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; - $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); - if ($conf['comparison'] === 100 || $conf['comparison'] === 101) { - // between - $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'date'); - $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue1]', $conf['inputValue1'], 'date'); - } else { - $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'date'); - } - $lineHTML[] = '</div>'; - break; - case 'time': - $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; - $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); - if ($conf['comparison'] === 100 || $conf['comparison'] === 101) { - // between: - $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'datetime'); - $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue1]', $conf['inputValue1'], 'datetime'); - } else { - $lineHTML[] = $this->getDateTimePickerField($fieldPrefix . '[inputValue]', $conf['inputValue'], 'datetime'); - } - $lineHTML[] = '</div>'; - break; - case 'multiple': - case 'binary': - case 'relation': - $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; - $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); - $lineHTML[] = '<div class="col mb-sm-2">'; - if ($conf['comparison'] === 68 || $conf['comparison'] === 69 || $conf['comparison'] === 162 || $conf['comparison'] === 163) { - $lineHTML[] = '<select class="form-select" name="' . $fieldPrefix . '[inputValue][]" multiple="multiple">'; - } elseif ($conf['comparison'] === 66 || $conf['comparison'] === 67) { - if (is_array($conf['inputValue'])) { - $conf['inputValue'] = implode(',', $conf['inputValue']); - } - $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue'] ?? '') . '" name="' . $fieldPrefix . '[inputValue]">'; - } elseif ($conf['comparison'] === 64) { - if (is_array($conf['inputValue'])) { - $conf['inputValue'] = $conf['inputValue'][0]; - } - $lineHTML[] = '<select class="form-select t3js-submit-change" name="' . $fieldPrefix . '[inputValue]">'; - } else { - $lineHTML[] = '<select class="form-select t3js-submit-change" name="' . $fieldPrefix . '[inputValue]">'; - } - if ($conf['comparison'] != 66 && $conf['comparison'] != 67) { - $lineHTML[] = $this->makeOptionList($fieldName, $conf, $this->table); - $lineHTML[] = '</select>'; - } - $lineHTML[] = '</div>'; - $lineHTML[] = '</div>'; - break; - case 'boolean': - $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; - $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); - $lineHTML[] = '<input type="hidden" value="1" name="' . $fieldPrefix . '[inputValue]">'; - $lineHTML[] = '</div>'; - break; - default: - $lineHTML[] = '<div class="row row-cols-auto mb-2 mb-sm-0">'; - $lineHTML[] = $this->makeComparisonSelector($subscript, $fieldName, $conf); - $lineHTML[] = '<div class="col mb-sm-2">'; - if ($conf['comparison'] === 37 || $conf['comparison'] === 36) { - // between: - $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue'] ?? '') . '" name="' . $fieldPrefix . '[inputValue]">'; - $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue1'] ?? '') . '" name="' . $fieldPrefix . '[inputValue1]">'; - } else { - $lineHTML[] = '<input class="form-control t3js-clearable" type="text" value="' . htmlspecialchars($conf['inputValue'] ?? '') . '" name="' . $fieldPrefix . '[inputValue]">'; - } - $lineHTML[] = '</div>'; - $lineHTML[] = '</div>'; - } - if ($fieldType !== 'ignore') { - $lineHTML[] = '<div class="row row-cols-auto mb-2">'; - $lineHTML[] = '<div class="btn-group">'; - $lineHTML[] = $this->updateIcon(); - if ($loopCount) { - $lineHTML[] = '<button class="btn btn-default" title="Remove condition" name="qG_del' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-delete', Icon::SIZE_SMALL)->render() . '</button>'; - } - $lineHTML[] = '<button class="btn btn-default" title="Add condition" name="qG_ins' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-plus', Icon::SIZE_SMALL)->render() . '</button>'; - if ($c != 0) { - $lineHTML[] = '<button class="btn btn-default" title="Move up" name="qG_up' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-chevron-up', Icon::SIZE_SMALL)->render() . '</button>'; - } - if ($c != 0 && $fieldType !== 'newlevel') { - $lineHTML[] = '<button class="btn btn-default" title="New level" name="qG_nl' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-chevron-right', Icon::SIZE_SMALL)->render() . '</button>'; - } - if ($fieldType === 'newlevel') { - $lineHTML[] = '<button class="btn btn-default" title="Collapse new level" name="qG_remnl' . htmlspecialchars($subscript) . '">' . $this->iconFactory->getIcon('actions-chevron-left', Icon::SIZE_SMALL)->render() . '</button>'; - } - $lineHTML[] = '</div>'; - $lineHTML[] = '</div>'; - $codeArr[$arrCount]['html'] = implode(LF, $lineHTML); - $codeArr[$arrCount]['query'] = $this->getQuerySingle($conf, $c === 0); - $arrCount++; - $c++; - } - $loopCount = 1; - } - $this->queryConfig = $queryConfig; - return $codeArr; - } - - /** - * @param string $subscript - * @param string $fieldName - * @param array $conf - * - * @return string - */ - protected function makeComparisonSelector($subscript, $fieldName, $conf) - { - $fieldPrefix = $this->name . $subscript; - $lineHTML = []; - $lineHTML[] = '<div class="col mb-sm-2">'; - $lineHTML[] = $this->mkTypeSelect($fieldPrefix . '[type]', $fieldName); - $lineHTML[] = '</div>'; - $lineHTML[] = '<div class="col mb-sm-2">'; - $lineHTML[] = ' <div class="input-group">'; - $lineHTML[] = $this->mkCompSelect($fieldPrefix . '[comparison]', $conf['comparison'], ($conf['negate'] ?? null) ? 1 : 0); - $lineHTML[] = ' <span class="input-group-addon">'; - $lineHTML[] = ' <input type="checkbox" class="checkbox t3js-submit-click"' . (($conf['negate'] ?? null) ? ' checked' : '') . ' name="' . htmlspecialchars($fieldPrefix) . '[negate]">'; - $lineHTML[] = ' </span>'; - $lineHTML[] = ' </div>'; - $lineHTML[] = ' </div>'; - return implode(LF, $lineHTML); - } - - /** - * Make option list - * - * @param string $fieldName - * @param array $conf - * @param string $table - * @return string - */ - protected function makeOptionList($fieldName, $conf, $table) - { - $backendUserAuthentication = $this->getBackendUserAuthentication(); - $from_table_Arr = []; - $out = []; - $fieldSetup = $this->fields[$fieldName]; - $languageService = $this->getLanguageService(); - if ($fieldSetup['type'] === 'multiple') { - $optGroupOpen = false; - foreach (($fieldSetup['items'] ?? []) as $val) { - $value = $languageService->sL($val[0]); - if ($val[1] === '--div--') { - if ($optGroupOpen) { - $out[] = '</optgroup>'; - } - $optGroupOpen = true; - $out[] = '<optgroup label="' . htmlspecialchars($value) . '">'; - } elseif (GeneralUtility::inList($conf['inputValue'], $val[1])) { - $out[] = '<option value="' . htmlspecialchars($val[1]) . '" selected>' . htmlspecialchars($value) . '</option>'; - } else { - $out[] = '<option value="' . htmlspecialchars($val[1]) . '">' . htmlspecialchars($value) . '</option>'; - } - } - if ($optGroupOpen) { - $out[] = '</optgroup>'; - } - } - if ($fieldSetup['type'] === 'binary') { - foreach ($fieldSetup['items'] as $key => $val) { - $value = $languageService->sL($val[0]); - if (GeneralUtility::inList($conf['inputValue'], (string)(2 ** $key))) { - $out[] = '<option value="' . 2 ** $key . '" selected>' . htmlspecialchars($value) . '</option>'; - } else { - $out[] = '<option value="' . 2 ** $key . '">' . htmlspecialchars($value) . '</option>'; - } - } - } - if ($fieldSetup['type'] === 'relation') { - $useTablePrefix = 0; - $dontPrefixFirstTable = 0; - foreach (($fieldSetup['items'] ?? []) as $val) { - $value = $languageService->sL($val[0]); - if (GeneralUtility::inList($conf['inputValue'], $val[1])) { - $out[] = '<option value="' . htmlspecialchars($val[1]) . '" selected>' . htmlspecialchars($value) . '</option>'; - } else { - $out[] = '<option value="' . htmlspecialchars($val[1]) . '">' . htmlspecialchars($value) . '</option>'; - } - } - $allowedFields = $fieldSetup['allowed'] ?? ''; - if (str_contains($allowedFields, ',')) { - $from_table_Arr = explode(',', $allowedFields); - $useTablePrefix = 1; - if (!$fieldSetup['prepend_tname']) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); - $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - $statement = $queryBuilder->select($fieldName) - ->from($table) - ->executeQuery(); - while ($row = $statement->fetchAssociative()) { - if (str_contains($row[$fieldName], ',')) { - $checkContent = explode(',', $row[$fieldName]); - foreach ($checkContent as $singleValue) { - if (!str_contains($singleValue, '_')) { - $dontPrefixFirstTable = 1; - } - } - } else { - $singleValue = $row[$fieldName]; - if ($singleValue !== '' && !str_contains($singleValue, '_')) { - $dontPrefixFirstTable = 1; - } - } - } - } - } else { - $from_table_Arr[0] = $allowedFields; - } - if (!empty($fieldSetup['prepend_tname'])) { - $useTablePrefix = 1; - } - if (!empty($fieldSetup['foreign_table'])) { - $from_table_Arr[0] = $fieldSetup['foreign_table']; - } - $counter = 0; - $tablePrefix = ''; - $outArray = []; - $labelFieldSelect = []; - foreach ($from_table_Arr as $from_table) { - $useSelectLabels = false; - $useAltSelectLabels = false; - if ($useTablePrefix && !$dontPrefixFirstTable && $counter != 1 || $counter === 1) { - $tablePrefix = $from_table . '_'; - } - $counter = 1; - if (is_array($GLOBALS['TCA'][$from_table])) { - $labelField = $GLOBALS['TCA'][$from_table]['ctrl']['label'] ?? ''; - $altLabelField = $GLOBALS['TCA'][$from_table]['ctrl']['label_alt'] ?? ''; - if ($GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items'] ?? false) { - foreach ($GLOBALS['TCA'][$from_table]['columns'][$labelField]['config']['items'] as $labelArray) { - $labelFieldSelect[$labelArray[1]] = $languageService->sL($labelArray[0]); - } - $useSelectLabels = true; - } - $altLabelFieldSelect = []; - if ($GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items'] ?? false) { - foreach ($GLOBALS['TCA'][$from_table]['columns'][$altLabelField]['config']['items'] as $altLabelArray) { - $altLabelFieldSelect[$altLabelArray[1]] = $languageService->sL($altLabelArray[0]); - } - $useAltSelectLabels = true; - } - - if (!($this->tableArray[$from_table] ?? false)) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($from_table); - $queryBuilder->getRestrictions()->removeAll(); - if (empty($this->settings['show_deleted'])) { - $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - } - $selectFields = ['uid', $labelField]; - if ($altLabelField) { - $selectFields = array_merge($selectFields, GeneralUtility::trimExplode(',', $altLabelField, true)); - } - $queryBuilder->select(...$selectFields) - ->from($from_table) - ->orderBy('uid'); - if (!$backendUserAuthentication->isAdmin()) { - $webMounts = $backendUserAuthentication->returnWebmounts(); - $perms_clause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); - $webMountPageTree = ''; - $webMountPageTreePrefix = ''; - foreach ($webMounts as $webMount) { - if ($webMountPageTree) { - $webMountPageTreePrefix = ','; - } - $webMountPageTree .= $webMountPageTreePrefix - . $this->getTreeList($webMount, 999, 0, $perms_clause); - } - if ($from_table === 'pages') { - $queryBuilder->where( - QueryHelper::stripLogicalOperatorPrefix($perms_clause), - $queryBuilder->expr()->in( - 'uid', - $queryBuilder->createNamedParameter( - GeneralUtility::intExplode(',', $webMountPageTree), - Connection::PARAM_INT_ARRAY - ) - ) - ); - } else { - $queryBuilder->where( - $queryBuilder->expr()->in( - 'pid', - $queryBuilder->createNamedParameter( - GeneralUtility::intExplode(',', $webMountPageTree), - Connection::PARAM_INT_ARRAY - ) - ) - ); - } - } - $statement = $queryBuilder->executeQuery(); - $this->tableArray[$from_table] = $statement->fetchAllAssociative(); - } - - foreach (($this->tableArray[$from_table] ?? []) as $val) { - if ($useSelectLabels) { - $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($labelFieldSelect[$val[$labelField]]); - } elseif ($val[$labelField]) { - $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($val[$labelField]); - } elseif ($useAltSelectLabels) { - $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($altLabelFieldSelect[$val[$altLabelField]]); - } else { - $outArray[$tablePrefix . $val['uid']] = htmlspecialchars($val[$altLabelField]); - } - } - if (isset($this->settings['options_sortlabel']) && $this->settings['options_sortlabel'] && is_array($outArray)) { - natcasesort($outArray); - } - } - } - foreach ($outArray as $key2 => $val2) { - if (GeneralUtility::inList($conf['inputValue'], $key2)) { - $out[] = '<option value="' . htmlspecialchars($key2) . '" selected>[' . htmlspecialchars($key2) . '] ' . htmlspecialchars($val2) . '</option>'; - } else { - $out[] = '<option value="' . htmlspecialchars($key2) . '">[' . htmlspecialchars($key2) . '] ' . htmlspecialchars($val2) . '</option>'; - } - } - } - return implode(LF, $out); - } - - /** - * Print code array - * - * @param array $codeArr - * @param int $recursionLevel - * @return string - */ - protected function printCodeArray($codeArr, $recursionLevel = 0) - { - $out = []; - foreach (array_values($codeArr) as $queryComponent) { - $out[] = '<div class="card">'; - $out[] = '<div class="card-body pb-2">'; - $out[] = $queryComponent['html']; - - if ($this->enableQueryParts) { - $out[] = '<div class="row row-cols-auto mb-2">'; - $out[] = '<div class="col">'; - $out[] = '<code class="m-0">'; - $out[] = htmlspecialchars($queryComponent['query']); - $out[] = '</code>'; - $out[] = '</div>'; - $out[] = '</div>'; - } - if (is_array($queryComponent['sub'] ?? null)) { - $out[] = '<div class="mb-2">'; - $out[] = $this->printCodeArray($queryComponent['sub'], $recursionLevel + 1); - $out[] = '</div>'; - } - $out[] = '</div>'; - $out[] = '</div>'; - } - return implode(LF, $out); - } - - /** - * Make operator select - * - * @param string $name - * @param string $op - * @param bool $draw - * @param bool $submit - * @return string - */ - protected function mkOperatorSelect($name, $op, $draw, $submit) - { - $out = []; - if ($draw) { - $out[] = '<div class="row row-cols-auto mb-2">'; - $out[] = ' <div class="col">'; - $out[] = ' <select class="form-select' . ($submit ? ' t3js-submit-change' : '') . '" name="' . htmlspecialchars($name) . '[operator]">'; - $out[] = ' <option value="AND"' . (!$op || $op === 'AND' ? ' selected' : '') . '>' . htmlspecialchars($this->lang['AND']) . '</option>'; - $out[] = ' <option value="OR"' . ($op === 'OR' ? ' selected' : '') . '>' . htmlspecialchars($this->lang['OR']) . '</option>'; - $out[] = ' </select>'; - $out[] = ' </div>'; - $out[] = '</div>'; - } else { - $out[] = '<input type="hidden" value="' . htmlspecialchars($op) . '" name="' . htmlspecialchars($name) . '[operator]">'; - } - return implode(LF, $out); - } - - /** - * Make type select - * - * @param string $name - * @param string $fieldName - * @param string $prepend - * @return string - */ - protected function mkTypeSelect($name, $fieldName, $prepend = 'FIELD_') - { - $out = []; - $out[] = '<select class="form-select t3js-submit-change" name="' . htmlspecialchars($name) . '">'; - $out[] = '<option value=""></option>'; - foreach ($this->fields as $key => $value) { - if (!($value['exclude'] ?? false) || $this->getBackendUserAuthentication()->check('non_exclude_fields', $this->table . ':' . $key)) { - $label = $this->fields[$key]['label']; - if ($this->showFieldAndTableNames) { - $label .= ' [' . $key . ']'; - } - $out[] = '<option value="' . htmlspecialchars($prepend . $key) . '"' . ($key === $fieldName ? ' selected' : '') . '>' . htmlspecialchars($label) . '</option>'; - } - } - $out[] = '</select>'; - return implode(LF, $out); - } - - /** - * Verify type - * - * @param string $fieldName - * @return string - */ - protected function verifyType($fieldName) - { - $first = ''; - foreach ($this->fields as $key => $value) { - if (!$first) { - $first = $key; - } - if ($key === $fieldName) { - return $key; - } - } - return $first; - } - - /** - * Verify comparison - * - * @param string $comparison - * @param int $neg - * @return int - */ - protected function verifyComparison($comparison, $neg) - { - $compOffSet = $comparison >> 5; - $first = -1; - for ($i = 32 * $compOffSet + $neg; $i < 32 * ($compOffSet + 1); $i += 2) { - if ($first === -1) { - $first = $i; - } - if ($i >> 1 === $comparison >> 1) { - return $i; - } - } - return $first; - } - - /** - * Make field to input select - * - * @param string $name - * @param string $fieldName - * @return string - */ - protected function mkFieldToInputSelect($name, $fieldName) - { - $out = []; - $out[] = '<div class="input-group mb-2">'; - $out[] = ' <span class="input-group-btn">'; - $out[] = $this->updateIcon(); - $out[] = ' </span>'; - $out[] = ' <input type="text" class="form-control t3js-clearable" value="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($name) . '">'; - $out[] = '</div>'; - - $out[] = '<select class="form-select t3js-addfield" name="_fieldListDummy" size="5" data-field="' . htmlspecialchars($name) . '">'; - foreach ($this->fields as $key => $value) { - if (!$value['exclude'] || $this->getBackendUserAuthentication()->check('non_exclude_fields', $this->table . ':' . $key)) { - $label = $this->fields[$key]['label']; - if ($this->showFieldAndTableNames) { - $label .= ' [' . $key . ']'; - } - $out[] = '<option value="' . htmlspecialchars($key) . '"' . ($key === $fieldName ? ' selected' : '') . '>' . htmlspecialchars($label) . '</option>'; - } - } - $out[] = '</select>'; - return implode(LF, $out); - } - - /** - * Make table select - * - * @param string $name - * @param string $cur - * @return string - */ - protected function mkTableSelect($name, $cur) - { - $out = []; - $out[] = '<select class="form-select t3js-submit-change" name="' . $name . '">'; - $out[] = '<option value=""></option>'; - foreach ($GLOBALS['TCA'] as $tN => $value) { - if ($this->getBackendUserAuthentication()->check('tables_select', $tN)) { - $label = $this->getLanguageService()->sL($GLOBALS['TCA'][$tN]['ctrl']['title']); - if ($this->showFieldAndTableNames) { - $label .= ' [' . $tN . ']'; - } - $out[] = '<option value="' . htmlspecialchars($tN) . '"' . ($tN === $cur ? ' selected' : '') . '>' . htmlspecialchars($label) . '</option>'; - } - } - $out[] = '</select>'; - return implode(LF, $out); - } - - /** - * Make comparison select - * - * @param string $name - * @param string $comparison - * @param int $neg - * @return string - */ - protected function mkCompSelect($name, $comparison, $neg) - { - $compOffSet = $comparison >> 5; - $out = []; - $out[] = '<select class="form-select t3js-submit-change" name="' . $name . '">'; - for ($i = 32 * $compOffSet + $neg; $i < 32 * ($compOffSet + 1); $i += 2) { - if ($this->lang['comparison'][$i . '_'] ?? false) { - $out[] = '<option value="' . $i . '"' . ($i >> 1 === $comparison >> 1 ? ' selected' : '') . '>' . htmlspecialchars($this->lang['comparison'][$i . '_']) . '</option>'; - } - } - $out[] = '</select>'; - return implode(LF, $out); - } - - /** - * Get subscript - * - * @param array $arr - */ - protected function getSubscript($arr): array - { - $retArr = []; - while (\is_array($arr)) { - reset($arr); - $key = key($arr); - $retArr[] = $key; - if (isset($arr[$key])) { - $arr = $arr[$key]; - } else { - break; - } - } - return $retArr; - } - - /** - * Get query - * - * @param array $queryConfig - * @param string $pad - * @return string - */ - protected function getQuery($queryConfig, $pad = '') - { - $qs = ''; - // Since we don't traverse the array using numeric keys in the upcoming whileloop make sure it's fresh and clean - ksort($queryConfig); - $first = true; - foreach ($queryConfig as $key => $conf) { - $conf = $this->convertIso8601DatetimeStringToUnixTimestamp($conf); - switch ($conf['type']) { - case 'newlevel': - $qs .= LF . $pad . trim($conf['operator']) . ' (' . $this->getQuery( - $queryConfig[$key]['nl'], - $pad . ' ' - ) . LF . $pad . ')'; - break; - default: - $qs .= LF . $pad . $this->getQuerySingle($conf, $first); - } - $first = false; - } - return $qs; - } - - /** - * Convert ISO-8601 timestamp (string) into unix timestamp (int) - */ - protected function convertIso8601DatetimeStringToUnixTimestamp(array $conf): array - { - if ($this->isDateOfIso8601Format($conf['inputValue'] ?? '')) { - $conf['inputValue'] = strtotime($conf['inputValue']); - if ($this->isDateOfIso8601Format($conf['inputValue1'] ?? '')) { - $conf['inputValue1'] = strtotime($conf['inputValue1']); - } - } - - return $conf; - } - - /** - * Checks if the given value is of the ISO 8601 format. - * - * @param mixed $date - */ - protected function isDateOfIso8601Format($date): bool - { - if (!is_int($date) && !is_string($date)) { - return false; - } - $format = 'Y-m-d\\TH:i:s\\Z'; - $formattedDate = \DateTime::createFromFormat($format, (string)$date); - return $formattedDate && $formattedDate->format($format) === $date; - } - - /** - * Get single query - * - * @param array $conf - * @param bool $first - * @return string - */ - protected function getQuerySingle($conf, $first) - { - $comparison = (int)($conf['comparison'] ?? 0); - $qs = ''; - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table); - $prefix = $this->enablePrefix ? $this->table . '.' : ''; - if (!$first) { - // Is it OK to insert the AND operator if none is set? - $operator = strtoupper(trim($conf['operator'] ?? '')); - if (!in_array($operator, ['AND', 'OR'], true)) { - $operator = 'AND'; - } - $qs .= $operator . ' '; - } - $qsTmp = str_replace('#FIELD#', $prefix . trim(substr($conf['type'], 6)), $this->compSQL[$comparison] ?? ''); - $inputVal = $this->cleanInputVal($conf); - if ($comparison === 68 || $comparison === 69) { - $inputVal = explode(',', (string)$inputVal); - foreach ($inputVal as $key => $fileName) { - $inputVal[$key] = $queryBuilder->quote($fileName); - } - $inputVal = implode(',', $inputVal); - $qsTmp = str_replace('#VALUE#', $inputVal, $qsTmp); - } elseif ($comparison === 162 || $comparison === 163) { - $inputValArray = explode(',', (string)$inputVal); - $inputVal = 0; - foreach ($inputValArray as $fileName) { - $inputVal += (int)$fileName; - } - $qsTmp = str_replace('#VALUE#', (string)$inputVal, $qsTmp); - } else { - if (is_array($inputVal)) { - $inputVal = $inputVal[0]; - } - // @todo This is weired, as it seems that it quotes the value as string and remove - // quotings using the trim() method. Should be investagated/refactored. - $qsTmp = str_replace('#VALUE#', trim($queryBuilder->quote((string)$inputVal), '\''), $qsTmp); - } - if ($comparison === 37 || $comparison === 36 || $comparison === 66 || $comparison === 67 || $comparison === 100 || $comparison === 101) { - // between: - $inputVal = $this->cleanInputVal($conf, '1'); - // @todo This is weired, as it seems that it quotes the value as string and remove - // quotings using the trim() method. Should be investagated/refactored. - $qsTmp = str_replace('#VALUE1#', trim($queryBuilder->quote((string)$inputVal), '\''), $qsTmp); - } - $qs .= trim((string)$qsTmp); - return $qs; - } - - /** - * Clean input value - * - * @param array $conf - * @param string $suffix - * @return string|int|float|null - */ - protected function cleanInputVal($conf, $suffix = '') - { - $comparison = (int)($conf['comparison'] ?? 0); - if ($comparison >> 5 === 0 || ($comparison === 32 || $comparison === 33 || $comparison === 64 || $comparison === 65 || $comparison === 66 || $comparison === 67 || $comparison === 96 || $comparison === 97)) { - $inputVal = $conf['inputValue' . $suffix] ?? null; - } elseif ($comparison === 39 || $comparison === 38) { - // in list: - $inputVal = implode(',', GeneralUtility::intExplode(',', (string)($conf['inputValue' . $suffix] ?? ''))); - } elseif ($comparison === 68 || $comparison === 69 || $comparison === 162 || $comparison === 163) { - // in list: - if (is_array($conf['inputValue' . $suffix] ?? false)) { - $inputVal = implode(',', $conf['inputValue' . $suffix]); - } elseif ($conf['inputValue' . $suffix] ?? false) { - $inputVal = $conf['inputValue' . $suffix]; - } else { - $inputVal = 0; - } - } elseif (!is_array($conf['inputValue' . $suffix] ?? null) && strtotime($conf['inputValue' . $suffix] ?? '')) { - $inputVal = $conf['inputValue' . $suffix]; - } elseif (!is_array($conf['inputValue' . $suffix] ?? null) && MathUtility::canBeInterpretedAsInteger($conf['inputValue' . $suffix] ?? null)) { - $inputVal = (int)$conf['inputValue' . $suffix]; - } else { - // TODO: Six eyes looked at this code and nobody understood completely what is going on here and why we - // fallback to float casting, the whole class smells like it needs a refactoring. - $inputVal = (float)($conf['inputValue' . $suffix] ?? 0.0); - } - return $inputVal; - } - - /** - * Update icon - * - * @return string - */ - protected function updateIcon() - { - return '<button class="btn btn-default" title="Update" name="just_update">' . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</button>'; - } - - /** - * Get label column - * - * @return string - */ - protected function getLabelCol() - { - return $GLOBALS['TCA'][$this->table]['ctrl']['label']; - } - - /** - * Make selector table - * - * @param array $modSettings - * @param string $enableList - * @return string - */ - protected function makeSelectorTable($modSettings, ServerRequestInterface $request, $enableList = 'table,fields,query,group,order,limit') - { - $out = []; - $enableArr = explode(',', $enableList); - $userTsConfig = $this->getBackendUserAuthentication()->getTSConfig(); - - // Make output - if (in_array('table', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableSelectATable'] ?? false)) { - $out[] = '<div class="form-group">'; - $out[] = '<label for="SET[queryTable]">Select a table:</label>'; - $out[] = '<div class="row row-cols-auto">'; - $out[] = '<div class="col">'; - $out[] = $this->mkTableSelect('SET[queryTable]', $this->table); - $out[] = '</div>'; - $out[] = '</div>'; - $out[] = '</div>'; - } - if ($this->table) { - // Init fields: - $this->setAndCleanUpExternalLists('queryFields', $modSettings['queryFields'] ?? '', 'uid,' . $this->getLabelCol()); - $this->setAndCleanUpExternalLists('queryGroup', $modSettings['queryGroup'] ?? ''); - $this->setAndCleanUpExternalLists('queryOrder', ($modSettings['queryOrder'] ?? '') . ',' . ($modSettings['queryOrder2'] ?? '')); - // Limit: - $this->extFieldLists['queryLimit'] = $modSettings['queryLimit'] ?? ''; - if (!$this->extFieldLists['queryLimit']) { - $this->extFieldLists['queryLimit'] = 100; - } - $parts = GeneralUtility::intExplode(',', (string)$this->extFieldLists['queryLimit']); - $limitBegin = 0; - $limitLength = (int)($this->extFieldLists['queryLimit'] ?? 0); - if ($parts[1] ?? null) { - $limitBegin = (int)$parts[0]; - $limitLength = (int)$parts[1]; - } - $this->extFieldLists['queryLimit'] = implode(',', array_slice($parts, 0, 2)); - // Insert Descending parts - if ($this->extFieldLists['queryOrder']) { - $descParts = explode(',', ($modSettings['queryOrderDesc'] ?? '') . ',' . ($modSettings['queryOrder2Desc'] ?? '')); - $orderParts = explode(',', $this->extFieldLists['queryOrder']); - $reList = []; - foreach ($orderParts as $kk => $vv) { - $reList[] = $vv . ($descParts[$kk] ? ' DESC' : ''); - } - $this->extFieldLists['queryOrder_SQL'] = implode(',', $reList); - } - // Query Generator: - $this->procesData(($modSettings['queryConfig'] ?? false) ? unserialize($modSettings['queryConfig'] ?? '', ['allowed_classes' => false]) : []); - $this->queryConfig = $this->cleanUpQueryConfig($this->queryConfig); - $this->enableQueryParts = (bool)($modSettings['search_query_smallparts'] ?? false); - $codeArr = $this->getFormElements(); - $queryCode = $this->printCodeArray($codeArr); - if (in_array('fields', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableSelectFields'] ?? false)) { - $out[] = '<div class="form-group form-group-with-button-addon">'; - $out[] = ' <label for="SET[queryFields]">Select fields:</label>'; - $out[] = $this->mkFieldToInputSelect('SET[queryFields]', $this->extFieldLists['queryFields']); - $out[] = '</div>'; - } - if (in_array('query', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableMakeQuery'] ?? false)) { - $out[] = '<div class="form-group">'; - $out[] = ' <label>Make Query:</label>'; - $out[] = $queryCode; - $out[] = '</div>'; - } - if (in_array('group', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableGroupBy'] ?? false)) { - $out[] = '<div class="form-group">'; - $out[] = '<label for="SET[queryGroup]">Group By:</label>'; - $out[] = '<div class="row row-cols-auto">'; - $out[] = '<div class="col">'; - $out[] = $this->mkTypeSelect('SET[queryGroup]', $this->extFieldLists['queryGroup'], ''); - $out[] = '</div>'; - $out[] = '</div>'; - $out[] = '</div>'; - } - if (in_array('order', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableOrderBy'] ?? false)) { - $orderByArr = explode(',', $this->extFieldLists['queryOrder']); - $orderBy = []; - $orderBy[] = '<div class="row row-cols-auto align-items-center">'; - $orderBy[] = '<div class="col">'; - $orderBy[] = $this->mkTypeSelect('SET[queryOrder]', $orderByArr[0], ''); - $orderBy[] = '</div>'; - $orderBy[] = '<div class="col mt-2">'; - $orderBy[] = '<div class="form-check">'; - $orderBy[] = self::getFuncCheck(0, 'SET[queryOrderDesc]', $modSettings['queryOrderDesc'] ?? '', $request, '', '', 'id="checkQueryOrderDesc"'); - $orderBy[] = '<label class="form-check-label" for="checkQueryOrderDesc">Descending</label>'; - $orderBy[] = '</div>'; - $orderBy[] = '</div>'; - $orderBy[] = '</div>'; - - if ($orderByArr[0]) { - $orderBy[] = '<div class="row row-cols-auto align-items-center mt-2">'; - $orderBy[] = '<div class="col">'; - $orderBy[] = '<div class="input-group">'; - $orderBy[] = $this->mkTypeSelect('SET[queryOrder2]', $orderByArr[1] ?? '', ''); - $orderBy[] = '</div>'; - $orderBy[] = '</div>'; - $orderBy[] = '<div class="col mt-2">'; - $orderBy[] = '<div class="form-check">'; - $orderBy[] = self::getFuncCheck(0, 'SET[queryOrder2Desc]', $modSettings['queryOrder2Desc'] ?? false, $request, '', '', 'id="checkQueryOrder2Desc"'); - $orderBy[] = '<label class="form-check-label" for="checkQueryOrder2Desc">Descending</label>'; - $orderBy[] = '</div>'; - $orderBy[] = '</div>'; - $orderBy[] = '</div>'; - } - $out[] = '<div class="form-group">'; - $out[] = ' <label>Order By:</label>'; - $out[] = implode(LF, $orderBy); - $out[] = '</div>'; - } - if (in_array('limit', $enableArr) && !($userTsConfig['mod.']['dbint.']['disableLimit'] ?? false)) { - $limit = []; - $limit[] = '<div class="input-group">'; - $limit[] = ' <span class="input-group-btn">'; - $limit[] = $this->updateIcon(); - $limit[] = ' </span>'; - $limit[] = ' <input type="text" class="form-control" value="' . htmlspecialchars($this->extFieldLists['queryLimit']) . '" name="SET[queryLimit]" id="queryLimit">'; - $limit[] = '</div>'; - - $prevLimit = $limitBegin - $limitLength < 0 ? 0 : $limitBegin - $limitLength; - $prevButton = ''; - $nextButton = ''; - - if ($limitBegin) { - $prevButton = '<input type="button" class="btn btn-default" value="previous ' . htmlspecialchars((string)$limitLength) . '" data-value="' . htmlspecialchars($prevLimit . ',' . $limitLength) . '">'; - } - if (!$limitLength) { - $limitLength = 100; - } - - $nextLimit = $limitBegin + $limitLength; - if ($nextLimit < 0) { - $nextLimit = 0; - } - if ($nextLimit) { - $nextButton = '<input type="button" class="btn btn-default" value="next ' . htmlspecialchars((string)$limitLength) . '" data-value="' . htmlspecialchars($nextLimit . ',' . $limitLength) . '">'; - } - - $out[] = '<div class="form-group">'; - $out[] = ' <label>Limit:</label>'; - $out[] = ' <div class="row row-cols-auto">'; - $out[] = ' <div class="col">'; - $out[] = implode(LF, $limit); - $out[] = ' </div>'; - $out[] = ' <div class="col">'; - $out[] = ' <div class="btn-group t3js-limit-submit">'; - $out[] = $prevButton; - $out[] = $nextButton; - $out[] = ' </div>'; - $out[] = ' </div>'; - $out[] = ' <div class="col">'; - $out[] = ' <div class="btn-group t3js-limit-submit">'; - $out[] = ' <input type="button" class="btn btn-default" data-value="10" value="10">'; - $out[] = ' <input type="button" class="btn btn-default" data-value="20" value="20">'; - $out[] = ' <input type="button" class="btn btn-default" data-value="50" value="50">'; - $out[] = ' <input type="button" class="btn btn-default" data-value="100" value="100">'; - $out[] = ' </div>'; - $out[] = ' </div>'; - $out[] = ' </div>'; - $out[] = '</div>'; - } - } - return implode(LF, $out); - } - - /** - * Get select query - * - * @param string $qString - */ - protected function getSelectQuery($qString = ''): string - { - $backendUserAuthentication = $this->getBackendUserAuthentication(); - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table); - $queryBuilder->getRestrictions()->removeAll(); - if (empty($this->settings['show_deleted'])) { - $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - } - $deleteField = $GLOBALS['TCA'][$this->table]['ctrl']['delete'] ?? ''; - $fieldList = GeneralUtility::trimExplode( - ',', - $this->extFieldLists['queryFields'] - . ',pid' - . ($deleteField ? ',' . $deleteField : '') - ); - $queryBuilder->select(...$fieldList) - ->from($this->table); - - if ($this->extFieldLists['queryGroup']) { - $queryBuilder->groupBy(...QueryHelper::parseGroupBy($this->extFieldLists['queryGroup'])); - } - if ($this->extFieldLists['queryOrder']) { - foreach (QueryHelper::parseOrderBy($this->extFieldLists['queryOrder_SQL']) as $orderPair) { - [$fieldName, $order] = $orderPair; - $queryBuilder->addOrderBy($fieldName, $order); - } - } - $queryLimit = (string)($this->extFieldLists['queryLimit'] ?? ''); - if ($queryLimit) { - // Explode queryLimit to fetch the limit and a possible offset - $parts = GeneralUtility::intExplode(',', $queryLimit); - if ($parts[1] ?? null) { - // Offset and limit are given - $queryBuilder->setFirstResult($parts[0]); - $queryBuilder->setMaxResults($parts[1]); - } else { - // Only the limit is given - $queryBuilder->setMaxResults($parts[0]); - } - } - - if (!$backendUserAuthentication->isAdmin()) { - $webMounts = $backendUserAuthentication->returnWebmounts(); - $perms_clause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); - $webMountPageTree = ''; - $webMountPageTreePrefix = ''; - foreach ($webMounts as $webMount) { - if ($webMountPageTree) { - $webMountPageTreePrefix = ','; - } - $webMountPageTree .= $webMountPageTreePrefix - . $this->getTreeList($webMount, 999, 0, $perms_clause); - } - // createNamedParameter() is not used here because the SQL fragment will only include - // the :dcValueX placeholder when the query is returned as a string. The value for the - // placeholder would be lost in the process. - if ($this->table === 'pages') { - $queryBuilder->where( - QueryHelper::stripLogicalOperatorPrefix($perms_clause), - $queryBuilder->expr()->in( - 'uid', - GeneralUtility::intExplode(',', $webMountPageTree) - ) - ); - } else { - $queryBuilder->where( - $queryBuilder->expr()->in( - 'pid', - GeneralUtility::intExplode(',', $webMountPageTree) - ) - ); - } - } - if (!$qString) { - $qString = $this->getQuery($this->queryConfig); - } - $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($qString)); - - return $queryBuilder->getSQL(); - } - - /** - * @param string $name the field name - * @param string $timestamp ISO-8601 timestamp - * @param string $type [datetime, date, time, timesec, year] - * - * @return string - */ - protected function getDateTimePickerField($name, $timestamp, $type) - { - $value = strtotime($timestamp) ? date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)strtotime($timestamp)) : ''; - $id = StringUtility::getUniqueId('dt_'); - $html = []; - $html[] = '<div class="col mb-sm-2">'; - $html[] = ' <div class="input-group" id="' . $id . '-wrapper">'; - $html[] = ' <input data-formengine-input-name="' . htmlspecialchars($name) . '" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="' . htmlspecialchars($type) . '" type="text" id="' . $id . '">'; - $html[] = ' <input name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($timestamp) . '" type="hidden">'; - $html[] = ' <button class="btn btn-default" type="button" data-global-event="click" data-action-focus="#' . $id . '">'; - $html[] = $this->iconFactory->getIcon('actions-calendar-alternative', Icon::SIZE_SMALL)->render(); - $html[] = ' </button>'; - $html[] = ' </div>'; - $html[] = '</div>'; - return implode(LF, $html); - } - - protected function getBackendUserAuthentication(): BackendUserAuthentication - { - return $GLOBALS['BE_USER']; - } - - protected function getLanguageService(): LanguageService - { - return $GLOBALS['LANG']; - } - - //################################ - // copied over from BackendUtility to enable deprecation of the original method - // @todo finish fluidification of template and remove HTML generation from controller - //################################ - - /** - * Checkbox function menu. - * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox. - * - * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=... - * @param string $elementName The form elements name, probably something like "SET[...] - * @param string|bool $currentValue The value to be selected currently. - * @param string $script The script to send the &id to, if empty it's automatically found - * @param string $addParams Additional parameters to pass to the script. - * @param string $tagParams Additional attributes for the checkbox input tag - * @return string HTML code for checkbox - * @see getFuncMenu() - */ - protected static function getFuncCheck( - $mainParams, - $elementName, - $currentValue, - ServerRequestInterface $request, - $script = '', - $addParams = '', - $tagParams = '' - ) { - // relies on module 'TYPO3/CMS/Backend/ActionDispatcher' - $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $request, $script); - $attributes = GeneralUtility::implodeAttributes([ - 'type' => 'checkbox', - 'class' => 'form-check-input', - 'name' => $elementName, - 'value' => '1', - 'data-global-event' => 'change', - 'data-action-navigate' => '$data=~s/$value/', - 'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName), - 'data-empty-value' => '0', - ], true); - return - '<input ' . $attributes . - ($currentValue ? ' checked="checked"' : '') . - ($tagParams ? ' ' . $tagParams : '') . - ' />'; - } - - /** - * Builds the URL to the current script with given arguments - * - * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=... - * @param string $addParams Additional parameters to pass to the script. - * @param string $script The script to send the &id to, if empty it's automatically found - * @return string The complete script URL - * @todo Check if this can be removed or replaced by routing - */ - protected static function buildScriptUrl($mainParams, $addParams, ServerRequestInterface $request, $script = '') - { - if (!is_array($mainParams)) { - $mainParams = ['id' => $mainParams]; - } - - $route = $request->getAttribute('route'); - if ($route instanceof Route) { - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $scriptUrl = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'), $mainParams); - $scriptUrl .= $addParams; - } else { - if (!$script) { - $script = PathUtility::basename(Environment::getCurrentScript()); - } - $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams; - } - - return $scriptUrl; - } -} diff --git a/typo3/sysext/lowlevel/Tests/Functional/Database/QueryGeneratorTest.php b/typo3/sysext/lowlevel/Tests/Functional/Controller/DatabaseIntegrityControllerTest.php similarity index 88% rename from typo3/sysext/lowlevel/Tests/Functional/Database/QueryGeneratorTest.php rename to typo3/sysext/lowlevel/Tests/Functional/Controller/DatabaseIntegrityControllerTest.php index 515823bd3720aac9efe436b8c4c1f2a3f72fe7d9..bc7e5119ee14a010bd1484b3ec26436761083051 100644 --- a/typo3/sysext/lowlevel/Tests/Functional/Database/QueryGeneratorTest.php +++ b/typo3/sysext/lowlevel/Tests/Functional/Controller/DatabaseIntegrityControllerTest.php @@ -15,7 +15,7 @@ declare(strict_types=1); * The TYPO3 project - inspiring people to share! */ -namespace TYPO3\CMS\Lowlevel\Tests\Functional\Database; +namespace TYPO3\CMS\Lowlevel\Tests\Functional\Controller; use TYPO3\CMS\Backend\Routing\Route; use TYPO3\CMS\Core\Database\ConnectionPool; @@ -24,10 +24,10 @@ use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Localization\LanguageServiceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Lowlevel\Database\QueryGenerator; +use TYPO3\CMS\Lowlevel\Controller\DatabaseIntegrityController; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; -class QueryGeneratorTest extends FunctionalTestCase +class DatabaseIntegrityControllerTest extends FunctionalTestCase { protected function setUp(): void { @@ -44,7 +44,7 @@ class QueryGeneratorTest extends FunctionalTestCase { $id = 1; $depth = 0; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth); self::assertEquals($id, $treeList); } @@ -56,7 +56,7 @@ class QueryGeneratorTest extends FunctionalTestCase { $id = 0; $depth = 1; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth); self::assertEquals($id, $treeList); } @@ -68,7 +68,7 @@ class QueryGeneratorTest extends FunctionalTestCase { $id = -1; $depth = 0; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth); self::assertEquals(1, $treeList); } @@ -81,7 +81,7 @@ class QueryGeneratorTest extends FunctionalTestCase $id = 0; $depth = 0; $begin = 1; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth, $begin); self::assertSame('', $treeList); } @@ -93,7 +93,7 @@ class QueryGeneratorTest extends FunctionalTestCase { $id = 1; $depth = 1; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth); self::assertEquals($id, $treeList); } @@ -106,7 +106,7 @@ class QueryGeneratorTest extends FunctionalTestCase $this->importCSVDataSet(__DIR__ . '/Fixtures/TestGetPageTreeStraightTreeSet.csv'); $id = 1; $depth = 99; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth, 0, 'hidden=0'); self::assertSame('1,2,3,4,5', $treeList); } @@ -118,7 +118,7 @@ class QueryGeneratorTest extends FunctionalTestCase public function getTreeListReturnsListOfIdsWithBeginSetToZero(int $id, int $depth, string $expectation): void { $this->importCSVDataSet(__DIR__ . '/Fixtures/TestGetPageTreeStraightTreeSet.csv'); - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth); self::assertSame($expectation, $treeList); } @@ -157,7 +157,7 @@ class QueryGeneratorTest extends FunctionalTestCase public function getTreeListReturnsListOfIdsWithBeginSetToMinusOne(int $id, int $depth, string $expectation): void { $this->importCSVDataSet(__DIR__ . '/Fixtures/TestGetPageTreeStraightTreeSet.csv'); - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth, -1); self::assertSame($expectation, $treeList); } @@ -197,7 +197,7 @@ class QueryGeneratorTest extends FunctionalTestCase $id = 1; $depth = 3; $this->importCSVDataSet(__DIR__ . '/Fixtures/TestGetPageTreeBranchedTreeSet.csv'); - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth); self::assertSame('1,2,3,4,5', $treeList); } @@ -211,7 +211,7 @@ class QueryGeneratorTest extends FunctionalTestCase $depth = 3; $begin = 1; $this->importCSVDataSet(__DIR__ . '/Fixtures/TestGetPageTreeBranchedTreeSet.csv'); - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth, $begin); self::assertSame('2,3,4,5', $treeList); } @@ -225,7 +225,7 @@ class QueryGeneratorTest extends FunctionalTestCase $depth = 3; $begin = 2; $this->importCSVDataSet(__DIR__ . '/Fixtures/TestGetPageTreeBranchedTreeSet.csv'); - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $treeList = $subject->_call('getTreeList', $id, $depth, $begin); self::assertSame('3,5', $treeList); } @@ -296,7 +296,7 @@ class QueryGeneratorTest extends FunctionalTestCase 'inputValue1' => $inputValue1, ], ]; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $subject->_call('init', 'queryConfig', 'aTable'); self::assertSame($expected, trim($subject->_call('getQuery', $inputConf), "\n\r")); } @@ -311,7 +311,7 @@ class QueryGeneratorTest extends FunctionalTestCase // ' ECT 'INJ %quoteCharacter%%commentStart% %commentEnd%%quoteCharacter% ECT', ]; - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $comparisons = array_keys($subject->_get('compSQL')); foreach ($injectors as $injector) { foreach ($comparisons as $comparison) { @@ -372,7 +372,7 @@ class QueryGeneratorTest extends FunctionalTestCase $route = new \stdClass(); $request = (new ServerRequest())->withAttribute('route', $route); - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); $subject->_set('iconFactory', $iconFactoryMock); $subject->_call('init', 'queryConfig', $settings['queryTable']); $subject->_call('makeSelectorTable', $settings, $request); diff --git a/typo3/sysext/lowlevel/Tests/Functional/Database/Fixtures/TestGetPageTreeBranchedTreeSet.csv b/typo3/sysext/lowlevel/Tests/Functional/Controller/Fixtures/TestGetPageTreeBranchedTreeSet.csv similarity index 100% rename from typo3/sysext/lowlevel/Tests/Functional/Database/Fixtures/TestGetPageTreeBranchedTreeSet.csv rename to typo3/sysext/lowlevel/Tests/Functional/Controller/Fixtures/TestGetPageTreeBranchedTreeSet.csv diff --git a/typo3/sysext/lowlevel/Tests/Functional/Database/Fixtures/TestGetPageTreeStraightTreeSet.csv b/typo3/sysext/lowlevel/Tests/Functional/Controller/Fixtures/TestGetPageTreeStraightTreeSet.csv similarity index 100% rename from typo3/sysext/lowlevel/Tests/Functional/Database/Fixtures/TestGetPageTreeStraightTreeSet.csv rename to typo3/sysext/lowlevel/Tests/Functional/Controller/Fixtures/TestGetPageTreeStraightTreeSet.csv diff --git a/typo3/sysext/lowlevel/Tests/Unit/Database/QueryGeneratorTest.php b/typo3/sysext/lowlevel/Tests/Unit/Controller/DatabaseIntegrityControllerTest.php similarity index 91% rename from typo3/sysext/lowlevel/Tests/Unit/Database/QueryGeneratorTest.php rename to typo3/sysext/lowlevel/Tests/Unit/Controller/DatabaseIntegrityControllerTest.php index da9d50c02d0e32c013585513dfc121f4d9b3d2ed..17ad4dbe54ff2699d7e2ff22e5602bed95ca9f1a 100644 --- a/typo3/sysext/lowlevel/Tests/Unit/Database/QueryGeneratorTest.php +++ b/typo3/sysext/lowlevel/Tests/Unit/Controller/DatabaseIntegrityControllerTest.php @@ -15,12 +15,12 @@ declare(strict_types=1); * The TYPO3 project - inspiring people to share! */ -namespace TYPO3\CMS\Lowlevel\Tests\Unit\Database; +namespace TYPO3\CMS\Lowlevel\Tests\Unit\Controller; -use TYPO3\CMS\Lowlevel\Database\QueryGenerator; +use TYPO3\CMS\Lowlevel\Controller\DatabaseIntegrityController; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; -class QueryGeneratorTest extends UnitTestCase +class DatabaseIntegrityControllerTest extends UnitTestCase { public function getSubscriptReturnsExpectedValuesDataProvider(): array { @@ -110,7 +110,7 @@ class QueryGeneratorTest extends UnitTestCase */ public function getSubscriptReturnsExpectedValues($input, array $expectedArray): void { - $subject = $this->getAccessibleMock(QueryGenerator::class, null, [], '', false); + $subject = $this->getAccessibleMock(DatabaseIntegrityController::class, null, [], '', false); self::assertSame($expectedArray, $subject->_call('getSubscript', $input)); } }