commit edc7fea85ac1e60a6046c2854a0418a99f6c8708 Author: Sven Wappler Date: Mon Aug 10 22:31:30 2026 +0200 TYPO3 v15 dev-main snapshot () diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/Classes/Controller/Event/ModifyInfoModuleContentEvent.php b/Classes/Controller/Event/ModifyInfoModuleContentEvent.php new file mode 100644 index 0000000..3bc35ee --- /dev/null +++ b/Classes/Controller/Event/ModifyInfoModuleContentEvent.php @@ -0,0 +1,108 @@ +access; + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + public function getCurrentModule(): ModuleInterface + { + return $this->currentModule; + } + + public function getModuleTemplate(): ModuleTemplate + { + return $this->moduleTemplate; + } + + /** + * Set content for the header. Can also be used to e.g. reorder existing content. + * IMPORTANT: This overwrites existing content from previous listeners! + */ + public function setHeaderContent(string $content): void + { + $this->headerContent = $content; + } + + /** + * Add additional content to the header + */ + public function addHeaderContent(string $content): void + { + $this->headerContent .= $content; + } + + public function getHeaderContent(): string + { + return $this->headerContent; + } + + /** + * Set content for the footer. Can also be used to e.g. reorder existing content. + * IMPORTANT: This overwrites existing content from previous listeners! + */ + public function setFooterContent(string $content): void + { + $this->footerContent = $content; + } + + /** + * Add additional content to the footer + */ + public function addFooterContent(string $content): void + { + $this->footerContent .= $content; + } + + public function getFooterContent(): string + { + return $this->footerContent; + } +} diff --git a/Classes/Controller/PageInformationController.php b/Classes/Controller/PageInformationController.php new file mode 100644 index 0000000..5d3950c --- /dev/null +++ b/Classes/Controller/PageInformationController.php @@ -0,0 +1,734 @@ + Pagetree Overview + * + * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API. + */ +#[AsController] +readonly class PageInformationController +{ + public function __construct( + protected IconFactory $iconFactory, + protected UriBuilder $uriBuilder, + protected ModuleTemplateFactory $moduleTemplateFactory, + protected EventDispatcherInterface $eventDispatcher, + protected TcaSchemaFactory $tcaSchemaFactory, + protected ComponentFactory $componentFactory, + protected BackendLayoutView $backendLayoutView, + protected ConnectionPool $connectionPool, + protected LocalizationRepository $localizationRepository, + ) {} + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $languageService = $this->getLanguageService(); + $module = $request->getAttribute('module'); + $moduleData = $request->getAttribute('moduleData'); + $currentSite = $request->getAttribute('site'); + $pageId = (int)($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0); + + $pageinfo = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: []; + $hasAccess = false; + if (($pageId > 0 && $pageinfo !== []) || ($backendUser->isAdmin() && $pageId === 0)) { + $hasAccess = true; + } + if ($pageId === 0 && $backendUser->isAdmin()) { + $pageinfo = ['title' => '[root-level]', 'uid' => 0, 'pid' => 0]; + } + + $siteLanguages = [ + $currentSite->getDefaultLanguage()->getLanguageId() => $currentSite->getDefaultLanguage(), + ]; + foreach ($currentSite->getAvailableLanguages($this->getBackendUser(), false, $pageId) as $language) { + $siteLanguages[$language->getLanguageId()] = $language; + } + + $fieldConfiguration = $this->getFieldConfiguration($pageId); + $allowedModuleOptions = $this->getModuleOptions($siteLanguages, $fieldConfiguration); + if ($moduleData->cleanUp($allowedModuleOptions)) { + $backendUser->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray()); + } + $selectedDepth = (int)($moduleData->get('depth') ?? 0); + $selectedGroup = (string)($moduleData->get('pages') ?? '0'); // field or table list to render + $selectedLanguage = (int)($moduleData->get('lang') ?? 0); + + $mainContent = $this->renderMainTable($pageId, $selectedDepth, $selectedLanguage, $siteLanguages, $request, $fieldConfiguration[$selectedGroup]['fields'] ?? []); + + $view = $this->moduleTemplateFactory->create($request); + $view->assign('hasAccess', $hasAccess); + if ($hasAccess) { + $view->setTitle($languageService->sL($module->getTitle()), $pageId !== 0 && isset($pageinfo['title']) ? $pageinfo['title'] : ''); + $view->getDocHeaderComponent()->setPageBreadcrumb($pageinfo); + $view->makeDocHeaderModuleMenu(['id' => $pageId]); + $view->getDocHeaderComponent()->setShortcutContext($module->getIdentifier(), sprintf('%s [%d]', $languageService->sL($module->getTitle()), $pageId), ['id' => $pageId]); + $previewUriBuilder = PreviewUriBuilder::create($pageinfo); + if ($previewUriBuilder->isPreviewable()) { + // View page + $previewDataAttributes = $previewUriBuilder + ->withRootLine(BackendUtility::BEgetRootLine($pageinfo['uid'])) + ->buildDispatcherDataAttributes(); + $viewButton = $this->componentFactory->createLinkButton() + ->setHref('#') + ->setDataAttributes($previewDataAttributes ?? []) + ->setDisabled(!$previewDataAttributes) + ->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) + ->setIcon($this->iconFactory->getIcon('actions-view-page', IconSize::SMALL)) + ->setShowLabelText(true); + $view->addButtonToButtonBar($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 2); + } + } + $event = $this->eventDispatcher->dispatch(new ModifyInfoModuleContentEvent($hasAccess, $request, $module, $view)); + if ($hasAccess) { + $view->assignMultiple([ + 'pageUid' => $pageId, + 'content' => $mainContent, + 'depthDropdownOptions' => $allowedModuleOptions['depth'], + 'depthDropdownCurrentValue' => $selectedDepth, + 'pagesDropdownOptions' => $allowedModuleOptions['pages'], + 'pagesDropdownCurrentValue' => $selectedGroup, + 'langDropdownOptions' => $allowedModuleOptions['lang'], + 'langDropdownCurrentValue' => $selectedLanguage, + 'headerContent' => $event->getHeaderContent(), + 'footerContent' => $event->getFooterContent(), + ]); + } + return $view->renderResponse('PageInformation'); + } + + protected function getModuleOptions(array $siteLanguages, array $fieldConfiguration): array + { + $languageService = $this->getLanguageService(); + $menu = [ + 'pages' => [], + 'depth' => [ + 0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'), + 1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'), + 2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'), + 3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'), + 4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'), + 999 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'), + ], + 'lang' => [], + ]; + foreach ($fieldConfiguration as $key => $item) { + $menu['pages'][$key] = $item['label']; + } + foreach ($siteLanguages as $language) { + $menu['lang'][$language->getLanguageId()] = $language->getTitle(); + } + return $menu; + } + + /** + * Generate configuration for field and table selection from TSConfig. + */ + protected function getFieldConfiguration(int $pageId): array + { + $languageService = $this->getLanguageService(); + $fieldConfiguration = []; + $modTSconfig = BackendUtility::getPagesTSconfig($pageId)['mod.']['web_info.']['fieldDefinitions.'] ?? []; + $allowedTables = $this->getAllowedTableNames(); + foreach ($modTSconfig as $key => $item) { + $fieldList = str_replace('###ALL_TABLES###', implode(',', $allowedTables), $item['fields']); + $fields = GeneralUtility::trimExplode(',', $fieldList, true); + $key = trim($key, '.'); + $fieldConfiguration[$key] = [ + 'label' => $item['label'] ? $languageService->sL($item['label']) : $key, + 'fields' => $fields, + ]; + } + return $fieldConfiguration; + } + + /** + * A list of table names allowed to be listed when ###ALL_TABLES### is used in TSConfig. + * Some tables like 'pages' are blinded by default, all remaining ones are user access checked. + */ + protected function getAllowedTableNames(): array + { + $hideTables = ['pages', 'sys_filemounts', 'be_users', 'be_groups']; // Never show these tables + $allowedTables = []; + foreach ($this->tcaSchemaFactory->all() as $schemaName => $schema) { + if (in_array($schemaName, $hideTables, true) + || $schema->hasCapability(TcaSchemaCapability::HideInUi) + || !$this->getBackendUser()->check('tables_select', $schemaName) + ) { + continue; + } + $allowedTables[] = 'table_' . $schemaName; + } + return $allowedTables; + } + + /** + * Renders records from the pages table from page id + * + * @return string HTML for the listing + */ + protected function renderMainTable(int $id, int $depth, int $language, array $siteLanguages, ServerRequestInterface $request, array $fieldArray): string + { + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUser(); + $out = ''; + $pagesSchema = $this->tcaSchemaFactory->get('pages'); + $languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + $translationOriginFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(); + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $row = $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)), + $backendUser->getPagePermsClause(Permission::PAGE_SHOW) + ) + ->executeQuery() + ->fetchAssociative(); + BackendUtility::workspaceOL('pages', $row); + if ($language > 0) { + $localizedPageRecord = $this->localizationRepository->getPageTranslations($row['uid'], [$language], $this->getBackendUser()->workspace); + if ($localizedPageRecord !== []) { + $row = reset($localizedPageRecord)->toArray(); + $row['uid'] = $row[$translationOriginFieldName]; + } + } + if (!is_array($row)) { + return ''; + } + + $editUids = []; + // Getting children + $theRows = $this->getPageRecordsRecursive($row['uid'], $depth, $language); + // Get tree root page + $treeRootPage = $this->getTreeRootPage($row['uid'], $row[$languageFieldName]); + if ($backendUser->doesUserHaveAccess($treeRootPage, Permission::PAGE_EDIT) && $treeRootPage['uid'] > 0) { + $editUids[] = $treeRootPage['uid']; + } + $out .= $this->pages_drawItem($treeRootPage, $request, $siteLanguages, $fieldArray); + // Traverse all pages selected: + foreach ($theRows as $sRow) { + if ($backendUser->doesUserHaveAccess($sRow, Permission::PAGE_EDIT)) { + $editUids[] = $sRow['uid']; + } + $out .= $this->pages_drawItem($sRow, $request, $siteLanguages, $fieldArray); + } + // Header line is drawn + $headerCells = []; + $editIdList = implode(',', $editUids); + // Traverse fields (as set above) in order to create header values: + foreach ($fieldArray as $field) { + $editButton = ''; + if ( + $editIdList + && $pagesSchema->hasField($field) + && $backendUser->check('tables_modify', 'pages') + && $backendUser->check('non_exclude_fields', 'pages:' . $field) + ) { + $iTitle = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:editThisColumn'), + rtrim(trim($languageService->sL($pagesSchema->getField($field)->getLabel())), ':') + ); + $urlParameters = [ + 'edit' => [ + 'pages' => [ + $editIdList => 'edit', + ], + ], + 'columnsOnly' => [ + 'pages' => [$field], + ], + 'module' => 'web_info_overview', + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + $editButton = '' + . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . ''; + } + switch ($field) { + case 'title': + $headerCells[$field] = $editButton . ' ' + . $languageService->sL($pagesSchema->getField($field)->getLabel()) + . ''; + break; + case 'uid': + $headerCells[$field] = ''; + break; + case 'actual_backend_layout': + $headerCells[$field] = htmlspecialchars($languageService->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:actual_backend_layout')); + break; + default: + if (str_starts_with($field, 'table_')) { + $f2 = substr($field, 6); + if ($this->tcaSchemaFactory->has($f2)) { + $schema = $this->tcaSchemaFactory->get($f2); + $headerCells[$field] = ' ' + . '' + . $this->iconFactory->getIconForRecord($f2, [], IconSize::SMALL)->render() + . ''; + } + } else { + if ($pagesSchema->hasField($field)) { + $headerCells[$field] = $editButton . ' ' + . htmlspecialchars($languageService->sL($pagesSchema->getField($field)->getLabel())) + . ''; + } else { + // Invalid field configured in `mod.web_info.fieldDefinitions.*`, + // using field name as header label. + $headerCells[$field] = $editButton . ' ' + . htmlspecialchars($field) + . ''; + } + } + } + } + return ' +
+ + + ' . $this->addElement($headerCells, $fieldArray) . ' + + + ' . $out . ' + +
+
'; + } + + /** + * Get tree root page + * + * @param int $pid Starting page + * @param int $language Selected site language + */ + protected function getTreeRootPage(int $pid, int $language): array + { + $backendUser = $this->getBackendUser(); + $pagesSchema = $this->tcaSchemaFactory->get('pages'); + $languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + $translationOriginFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(); + + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace)); + + if ($language > 0) { + return $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq($translationOriginFieldName, $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)), + $queryBuilder->expr()->eq($languageFieldName, $queryBuilder->createNamedParameter($language, Connection::PARAM_INT)), + $backendUser->getPagePermsClause(Permission::PAGE_SHOW) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + } + + return $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)), + $backendUser->getPagePermsClause(Permission::PAGE_SHOW) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + } + + /** + * Adds pages-rows to an array, selecting recursively in the page tree. + * + * @param int $pid Starting page id to select from + * @param string $iconPrefix Prefix for icon code. + * @param int $depth Depth (decreasing) + * @param array $rows Array which will accumulate page rows + * @return array $rows with added rows. + */ + protected function getPageRecordsRecursive(int $pid, int $depth, int $language, string $iconPrefix = '', array $rows = []): array + { + $backendUser = $this->getBackendUser(); + $pagesSchema = $this->tcaSchemaFactory->get('pages'); + $languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + + $depth--; + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace)); + + $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)), + $queryBuilder->expr()->eq($languageFieldName, $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)), + $backendUser->getPagePermsClause(Permission::PAGE_SHOW) + ); + + if ($pagesSchema->hasCapability(TcaSchemaCapability::SortByField)) { + $queryBuilder->orderBy($pagesSchema->getCapability(TcaSchemaCapability::SortByField)->getFieldName()); + } + + if ($depth >= 0) { + $countQueryBuilder = clone $queryBuilder; + $countQueryBuilder->resetOrderBy()->count('uid'); + $rowCount = $countQueryBuilder->executeQuery()->fetchOne(); + $result = $queryBuilder->executeQuery(); + $count = 0; + while ($row = $result->fetchAssociative()) { + BackendUtility::workspaceOL('pages', $row); + $uid = (int)$row['uid']; + if (is_array($row)) { + if ($language > 0) { + $localizedPageRecord = $this->localizationRepository->getPageTranslations($uid, [$language], $this->getBackendUser()->workspace); + if ($localizedPageRecord === []) { + continue; + } + $row = reset($localizedPageRecord)->toArray(); + } + $count++; + $row['treeIcons'] = $iconPrefix + . ''; + $rows[] = $row; + // Get the branch + $spaceOutIcons = ''; + $rows = $this->getPageRecordsRecursive( + $uid, + $row['php_tree_stop'] ? 0 : $depth, + $language, + $iconPrefix . $spaceOutIcons, + $rows + ); + } + } + } + + return $rows; + } + + /** + * Adds a list item for the pages-rendering + */ + protected function pages_drawItem(array $row, ServerRequestInterface $request, array $siteLanguages, array $fieldArray): string + { + $languageService = $this->getLanguageService(); + $backendUser = $this->getBackendUser(); + $pagesSchema = $this->tcaSchemaFactory->get('pages'); + $languageFieldName = $pagesSchema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); + $backendLayouts = $this->getBackendLayouts($row, 'backend_layout'); + $backendLayoutsNextLevel = $this->getBackendLayouts($row, 'backend_layout_next_level'); + $userTsConfig = $this->getBackendUser()->getTSConfig(); + $theIcon = $this->getIcon($row); + // Preparing and getting the data-array + $theData = []; + foreach ($fieldArray as $field) { + switch ($field) { + case 'title': + $showPageId = !empty($userTsConfig['options.']['pageTree.']['showPageIdWithTitle']); + $pTitle = htmlspecialchars( + (string)BackendUtility::getProcessedValue( + 'pages', + $field, + $row[$field], + 0, + false, + false, + 0, + true, + 0, + $row + ) + ); + $theData[$field] = '
' + . ($row['treeIcons'] ?? '') + . $theIcon + . '' + . ($showPageId ? '[' . $row['uid'] . '] ' : '') + . $pTitle + . '' + . '
'; + break; + case $languageFieldName: + if (count($siteLanguages) === 1) { + $theData[$field] = ''; + break; + } + $siteLanguage = $siteLanguages[$row[$languageFieldName]] ?? null; + if (!$siteLanguage) { + $theData[$field] = ''; + break; + } + $theData[$field] = $this->iconFactory->getIcon($siteLanguage->getFlagIdentifier(), IconSize::SMALL)->setTitle($siteLanguage->getTitle())->render() + . ' ' . $siteLanguage->getTitle(); + break; + case 'php_tree_stop': + // Intended fall through + case 'TSconfig': + $theData[$field] = $row[$field] ? 'x' : ' '; + break; + case 'actual_backend_layout': + $backendLayout = $this->backendLayoutView->getBackendLayoutForPage((int)$row['uid']); + $theData[$field] = htmlspecialchars($languageService->sL($backendLayout->getTitle())); + break; + case 'backend_layout': + $layoutValue = $backendLayouts[$row[$field]] ?? null; + $theData[$field] = $this->resolveBackendLayoutValue($layoutValue, $field, $row); + break; + case 'backend_layout_next_level': + $layoutValue = $backendLayoutsNextLevel[$row[$field]] ?? null; + $theData[$field] = $this->resolveBackendLayoutValue($layoutValue, $field, $row); + break; + case 'uid': + $uid = 0; + $editButton = ''; + $viewButton = ''; + if ($backendUser->doesUserHaveAccess($row, 2) && $row['uid'] > 0) { + $uid = (int)$row['uid']; + $urlParameters = [ + 'edit' => [ + 'pages' => [ + $row['uid'] => 'edit', + ], + ], + 'module' => 'web_info_overview', + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]; + $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + $previewDataAttributes = PreviewUriBuilder::create($row) + ->withRootLine(BackendUtility::BEgetRootLine($row['uid'])) + ->serializeDispatcherAttributes(); + $viewButton + = ''; + if ($backendUser->check('tables_modify', 'pages')) { + $editButton + = '' + . $this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)->render() + . ''; + } + } + // Since the uid is overwritten with the edit button markup, we need to store + // the actual uid to be able to add it as data attribute to the table data cell. + // This also makes the distinction between record rows and the header line simpler. + $theData['_UID_'] = $uid; + $theData[$field] = '
' . $viewButton . $editButton . '
'; + break; + case 'shortcut': + case 'shortcut_mode': + if ((int)$row['doktype'] === PageRepository::DOKTYPE_SHORTCUT) { + $theData[$field] = htmlspecialchars((string)BackendUtility::getProcessedValue('pages', $field, $row[$field], 0, false, false, 0, true, 0, $row)); + } + break; + default: + if (str_starts_with($field, 'table_')) { + $f2 = substr($field, 6); + if ($this->tcaSchemaFactory->has($f2)) { + $c = $this->numberOfRecords($f2, (int)$row['uid']); + $theData[$field] = ($c ?: ''); + } + } else { + $theData[$field] = htmlspecialchars((string)BackendUtility::getProcessedValue('pages', $field, $row[$field], 0, false, false, 0, true, 0, $row)); + } + } + } + return $this->addElement($theData, $fieldArray); + } + + /** + * Creates the icon image tag for the page and wraps it in a link which will trigger the click menu. + */ + protected function getIcon(array $row): string + { + $backendUser = $this->getBackendUser(); + $icon = '' . $this->iconFactory->getIconForRecord('pages', $row, IconSize::SMALL)->render() . ''; + // The icon with link + if ($backendUser->checkRecordEditAccess('pages', $row)->isAllowed) { + $icon = BackendUtility::wrapClickMenuOnIcon($icon, 'pages', $row['uid']); + } + return $icon; + } + + /** + * Counts and returns the number of records on the page with $pid + */ + protected function numberOfRecords(string $table, int $pid): int + { + if (!$this->tcaSchemaFactory->has($table)) { + return 0; + } + $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + return (int)$queryBuilder->count('uid') + ->from($table) + ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT))) + ->executeQuery() + ->fetchOne(); + } + + /** + * Returns a table-row with the content from the fields in the input data array. + * + * @param array $data Record with field values, NOT htmlspecialchar'ed + * @return string HTML content for the table row + */ + protected function addElement(array $data, array $fieldArray): string + { + // Start up: + $attributes = ''; + $rowTag = 'th'; + if (isset($data['_UID_'])) { + $l10nParent = isset($data['_l10nparent_']) ? (int)$data['_l10nparent_'] : 0; + $attributes = ' data-uid="' . $data['_UID_'] . '" data-l10nparent="' . $l10nParent . '"'; + $rowTag = 'td'; + } + $out = ''; + // Init rendering. + $colsp = ''; + $lastKey = ''; + $c = 0; + // __label is used as the label key to circumvent problems with uid used as label (see #67756) + // as it was introduced later on, check if it really exists before using it + if (array_key_exists('__label', $data)) { + $fieldArray[0] = '__label'; + } + // Traverse field array which contains the data to present: + foreach ($fieldArray as $vKey) { + if (isset($data[$vKey])) { + $cssClass = ''; + if ($lastKey === 'title') { + $cssClass = 'col-title col-responsive'; + } + if ($lastKey) { + $out .= '<' . $rowTag . ' class="' . $cssClass . '"' . $colsp . '>' . $data[$lastKey] . ''; + } + $lastKey = $vKey; + $c = 1; + } else { + if (!$lastKey) { + $lastKey = $vKey; + } + $c++; + } + if ($c > 1) { + $colsp = ' colspan="' . $c . '"'; + } else { + $colsp = ''; + } + } + if ($lastKey) { + $cssClass = ''; + if ($lastKey === 'title') { + $cssClass = 'col-title-flexible'; + } + $out .= '<' . $rowTag . ' class="' . $cssClass . ' nowrap"' . $colsp . '>' . $data[$lastKey] . ''; + } + $out .= ''; + return $out; + } + + protected function getBackendLayouts(array $row, string $field): array + { + $languageService = $this->getLanguageService(); + $configuration = ['row' => $row, 'table' => 'pages', 'field' => $field, 'items' => []]; + // Below we call the itemsProcFunc to retrieve properly resolved backend layout items, + // including the translated labels and the correct field values (backend layout identifiers). + $this->backendLayoutView->addBackendLayoutItems($configuration); + $backendLayouts = []; + foreach ($configuration['items'] ?? [] as $backendLayout) { + if (($backendLayout['label'] ?? false) && ($backendLayout['value'] ?? false)) { + $backendLayouts[$backendLayout['value']] = $languageService->sL($backendLayout['label']) ?: $backendLayout['label']; + } + } + return $backendLayouts; + } + + protected function resolveBackendLayoutValue(?string $layoutValue, string $field, array $row): string + { + $languageService = $this->getLanguageService(); + if ($layoutValue !== null) { + // Directly return the resolved layout value from BackendLayoutView + return htmlspecialchars($layoutValue); + } + $layoutValue = htmlspecialchars((string)BackendUtility::getProcessedValue('pages', $field, $row[$field], 0, false, false, 0, true, 0, $row)); + if ($layoutValue !== '') { + // If getProcessedValue() returns a non-empty string, the database field + // is filled with an invalid value (the backend layout does no longer exist). + return sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $layoutValue); + } + return ''; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/TranslationStatusController.php b/Classes/Controller/TranslationStatusController.php new file mode 100644 index 0000000..e6603e1 --- /dev/null +++ b/Classes/Controller/TranslationStatusController.php @@ -0,0 +1,513 @@ + Localization overview + * + * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API. + */ +#[AsController] +readonly class TranslationStatusController +{ + public function __construct( + private IconFactory $iconFactory, + private UriBuilder $uriBuilder, + private ModuleProvider $moduleProvider, + private ModuleTemplateFactory $moduleTemplateFactory, + private EventDispatcherInterface $eventDispatcher, + private TcaSchemaFactory $tcaSchemaFactory, + private ComponentFactory $componentFactory, + private ConnectionPool $connectionPool, + ) {} + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $backendUser = $this->getBackendUser(); + $languageService = $this->getLanguageService(); + $module = $request->getAttribute('module'); + $moduleData = $request->getAttribute('moduleData'); + $currentSite = $request->getAttribute('site'); + $pageId = (int)($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0); + + $pageinfo = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: []; + $hasAccess = false; + if (($pageId && $pageinfo !== []) || ($backendUser->isAdmin() && $pageId === 0)) { + $hasAccess = true; + } + if ($pageId === 0 && $backendUser->isAdmin()) { + $pageinfo = ['title' => '[root-level]', 'uid' => 0, 'pid' => 0]; + } + + $siteLanguages = $currentSite->getAvailableLanguages($backendUser, false, $pageId); + $allowedModuleOptions = $this->getModuleOptions($siteLanguages); + if ($moduleData->cleanUp($allowedModuleOptions)) { + $backendUser->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray()); + } + $selectedDepth = (int)$moduleData->get('depth'); + $selectedLanguage = (int)$moduleData->get('lang'); + + $mainContent = ''; + if ($pageId > 0) { + $tree = $this->getTree($pageId, $selectedDepth); + $mainContent = $this->renderL10nTable($tree, $request, $siteLanguages, $selectedLanguage); + } + + $view = $this->moduleTemplateFactory->create($request); + $view->assign('hasAccess', $hasAccess); + if ($hasAccess) { + $view->setTitle($languageService->sL($module->getTitle()), $pageId !== 0 && isset($pageinfo['title']) ? $pageinfo['title'] : ''); + $view->getDocHeaderComponent()->setPageBreadcrumb($pageinfo); + $view->makeDocHeaderModuleMenu(['id' => $pageId]); + $view->getDocHeaderComponent()->setShortcutContext($module->getIdentifier(), sprintf('%s [%d]', $languageService->sL($module->getTitle()), $pageId), ['id' => $pageId]); + $previewUriBuilder = PreviewUriBuilder::create($pageinfo); + if ($previewUriBuilder->isPreviewable()) { + $previewDataAttributes = $previewUriBuilder + ->withRootLine(BackendUtility::BEgetRootLine($pageinfo['uid'])) + ->buildDispatcherDataAttributes(); + $viewButton = $this->componentFactory->createLinkButton() + ->setHref('#') + ->setDataAttributes($previewDataAttributes ?? []) + ->setDisabled(!$previewDataAttributes) + ->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) + ->setIcon($this->iconFactory->getIcon('actions-view-page', IconSize::SMALL)) + ->setShowLabelText(true); + $view->addButtonToButtonBar($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 2); + } + } + $event = $this->eventDispatcher->dispatch(new ModifyInfoModuleContentEvent($hasAccess, $request, $module, $view)); + if ($hasAccess) { + $view->assignMultiple([ + 'pageUid' => $pageId, + 'depthDropdownOptions' => $allowedModuleOptions['depth'], + 'depthDropdownCurrentValue' => $selectedDepth, + 'langDropdownOptions' => $allowedModuleOptions['lang'], + 'langDropdownCurrentValue' => $selectedLanguage, + 'content' => $mainContent, + 'headerContent' => $event->getHeaderContent(), + 'footerContent' => $event->getFooterContent(), + ]); + } + return $view->renderResponse('TranslationStatus'); + } + + private function getModuleOptions(array $siteLanguages): array + { + $languageService = $this->getLanguageService(); + $menuArray = [ + 'depth' => [ + 0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'), + 1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'), + 2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'), + 3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'), + 4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'), + 999 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'), + ], + 'lang' => [], + ]; + foreach ($siteLanguages as $language) { + if ($language->getLanguageId() === 0) { + $menuArray['lang'][0] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.allLanguages'); + } else { + $menuArray['lang'][$language->getLanguageId()] = $language->getTitle(); + } + } + return $menuArray; + } + + private function getTree(int $pageId, int $selectedDepth): PageTreeView + { + $tree = GeneralUtility::makeInstance(PageTreeView::class); + $tree->init('AND ' . $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + $tree->tree[] = ['row' => BackendUtility::getRecordWSOL('pages', $pageId)]; + // Create the tree from starting point + if ($selectedDepth) { + $tree->getTree($pageId, $selectedDepth); + } + return $tree; + } + + /** + * Rendering the localization information table. + * + * @param PageTreeView $tree The Page tree data + * @return string HTML for the localization information table. + */ + private function renderL10nTable(PageTreeView $tree, ServerRequestInterface $request, array $siteLanguages, int $selectedLanguage): string + { + $lang = $this->getLanguageService(); + $backendUser = $this->getBackendUser(); + // Put together the TREE: + $output = ''; + $langRecUids = []; + + $userTsConfig = $backendUser->getTSConfig(); + $showPageId = !empty($userTsConfig['options.']['pageTree.']['showPageIdWithTitle']); + + $pageModule = 'web_layout'; + $pageModuleAccess = $this->moduleProvider->accessGranted($pageModule, $backendUser); + + foreach ($tree->tree as $data) { + $tCells = []; + $langRecUids[0][] = $data['row']['uid']; + $pageTitle = ($showPageId ? '[' . (int)$data['row']['uid'] . '] ' : '') . $data['row']['title']; + // Page icons / titles etc. + if ($pageModuleAccess) { + $pageModuleLink = (string)$this->uriBuilder->buildUriFromRoute($pageModule, ['id' => $data['row']['uid'], 'languages' => [0], 'viewMode' => PageViewMode::LayoutView->value]); + $pageModuleLink = '' . htmlspecialchars($pageTitle) . ''; + } else { + $pageModuleLink = htmlspecialchars($pageTitle); + } + $icon = '' + . $this->iconFactory->getIconForRecord('pages', $data['row'], IconSize::SMALL)->setTitle(BackendUtility::getRecordIconAltText($data['row'], 'pages', false))->render() + . ''; + + if ($backendUser->checkRecordEditAccess('pages', $data['row'])->isAllowed) { + $icon = BackendUtility::wrapClickMenuOnIcon($icon, 'pages', $data['row']['uid']); + } + + $tCells[] = '' + . '
' + . (!empty($data['depthData']) ? $data['depthData'] : '') + . ($data['HTML'] ?? '') + . $icon + . '' + . $pageModuleLink + . ((string)$data['row']['nav_title'] !== '' ? ' [Nav: ' . htmlspecialchars($data['row']['nav_title']) . ']' : '') + . '' + . '
' + . ''; + $previewUriBuilder = PreviewUriBuilder::create($data['row']); + // DEFAULT language: + $pageTranslationVisibility = new PageTranslationVisibility((int)($data['row']['l18n_cfg'] ?? 0)); + $status = $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() ? 'danger' : 'success'; + // Create links: + $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => [ + 'pages' => [ + $data['row']['uid'] => 'edit', + ], + ], + 'module' => 'web_info_translations', + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]); + $info = ''; + if ($backendUser->check('tables_modify', 'pages')) { + $info .= '' . $this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)->render() . ''; + } + $info .= ' '; + $info .= $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() ? 'D' : ' '; + $info .= $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() ? 'N' : ' '; + // Put into cell: + $tCells[] = '
' . $info . '
'; + $tCells[] = '' + . ($this->getContentElementCount((int)$data['row']['uid'], 0) ?: '-') + . ''; + // Traverse system languages: + foreach ($siteLanguages as $siteLanguage) { + $languageId = $siteLanguage->getLanguageId(); + if ($languageId === 0) { + continue; + } + if ($selectedLanguage === 0 || $selectedLanguage === $languageId) { + $row = $this->getLangStatus((int)$data['row']['uid'], $languageId); + if ($pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() || $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) { + $status = 'danger'; + } else { + $status = ''; + } + if (is_array($row)) { + $langRecUids[$languageId][] = $row['uid']; + if (!$row['_HIDDEN']) { + $status = 'success'; + } + if ($row['_COUNT'] > 1) { + $status = 'warning'; + } + $info = ($showPageId ? ' [' . (int)$row['uid'] . '] ' : '') + . htmlspecialchars($row['title']) + . ((string)$row['nav_title'] !== '' ? ' [Nav: ' . htmlspecialchars($row['nav_title']) . ']' : '') + . ($row['_COUNT'] > 1 ? '
' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_badThingThereAre') . '
' : ''); + + if ($pageModuleAccess) { + $pageModuleLink = (string)$this->uriBuilder->buildUriFromRoute($pageModule, ['id' => $data['row']['uid'], 'language' => [$languageId], 'viewMode' => PageViewMode::LanguageComparisonView->value]); + $pageModuleLink = '' . $info . ''; + } else { + $pageModuleLink = $info; + } + $icon = '' + . $this->iconFactory->getIconForRecord('pages', $row, IconSize::SMALL)->setTitle(BackendUtility::getRecordIconAltText($row, 'pages', false))->render() + . ''; + $tCells[] = '' + . BackendUtility::wrapClickMenuOnIcon($icon, 'pages', (int)$row['uid']) + . $pageModuleLink + . ''; + // Edit whole record: + // Create links: + $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => [ + 'pages' => [ + $row['uid'] => 'edit', + ], + ], + 'module' => 'web_info_translations', + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]); + // ViewPageLink + $info = ''; + $info .= '' . $this->iconFactory->getIcon('actions-open', IconSize::SMALL)->render() . ''; + $tCells[] = '
' . $info . '
'; + $tCells[] = '' . ($this->getContentElementCount((int)$data['row']['uid'], $languageId) ?: '-') . ''; + } else { + $idName = sprintf('new-overlay-%d-%d', $languageId, $data['row']['uid']); + $info = '
' + . '' + . '' + . '
'; + $tCells[] = ' '; + $tCells[] = ' '; + $tCells[] = '' . $info . ''; + } + } + } + $output .= '' . implode('', $tCells) . ''; + } + // Put together HEADER: + $headerCells = []; + $headerCells[] = '' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_page') . ''; + if ($backendUser->check('tables_modify', 'pages')) { + $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => [ + 'pages' => [ + implode(',', $langRecUids[0]) => 'edit', + ], + ], + 'columnsOnly' => [ + 'pages' => ['title', 'nav_title', 'l18n_cfg', 'hidden'], + ], + 'module' => 'web_info_translations', + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]); + $editIco = '' . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . ''; + } else { + $editIco = ''; + } + if (isset($siteLanguages[0])) { + $defaultLanguageLabel = $siteLanguages[0]->getTitle(); + } else { + $defaultLanguageLabel = $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_default'); + } + $headerCells[] = '' . htmlspecialchars($defaultLanguageLabel) . ' ' . $editIco . ''; + foreach ($siteLanguages as $siteLanguage) { + $languageId = $siteLanguage->getLanguageId(); + if ($languageId === 0) { + continue; + } + if ($selectedLanguage === 0 || $selectedLanguage === $languageId) { + // Title: + $headerCells[] = '' . htmlspecialchars($siteLanguage->getTitle()) . ''; + // Edit language overlay records: + if (is_array($langRecUids[$languageId] ?? null)) { + $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => [ + 'pages' => [ + implode(',', $langRecUids[$languageId]) => 'edit', + ], + ], + 'columnsOnly' => [ + 'pages' => ['title', 'nav_title', 'hidden'], + ], + 'module' => 'web_info_translations', + 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]); + $editButton = '' . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . ''; + } else { + $editButton = ''; + } + // Create new overlay records: + $createLink = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [ + 'redirect' => $request->getAttribute('normalizedParams')->getRequestUri(), + ]); + $newButton = '' . $this->iconFactory->getIcon('actions-document-new', IconSize::SMALL, null, IconState::STATE_DISABLED)->render() . ''; + + $headerCells[] = '' . $editButton . ''; + $headerCells[] = '' . $newButton . ''; + } + } + + $output + = '
' + . '' + . '' + . '' + . implode('', $headerCells) + . '' + . '' + . '' + . $output + . '' + . '
' + . '
'; + return $output; + } + + /** + * Get an alternative language record for a specific page / language + * + * @param int $pageId Page ID to look up for. + * @param int $langId Language UID to select for. + * @return array|bool translated pages record + */ + private function getLangStatus(int $pageId, int $langId): bool|array + { + $schema = $this->tcaSchemaFactory->get('pages'); + /** @var LanguageAwareSchemaCapability $languageCapability */ + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); + $queryBuilder + ->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)) + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $result = $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq( + $languageCapability->getTranslationOriginPointerField()->getName(), + $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) + ) + ) + ->andWhere( + $queryBuilder->expr()->eq( + $languageCapability->getLanguageField()->getName(), + $queryBuilder->createNamedParameter($langId, Connection::PARAM_INT) + ) + ) + ->executeQuery(); + + $row = $result->fetchAssociative(); + BackendUtility::workspaceOL('pages', $row); + if (is_array($row)) { + $row['_COUNT'] = $queryBuilder->count('uid')->executeQuery()->fetchOne(); + $row['_HIDDEN'] = $row['hidden'] || (int)$row['endtime'] > 0 && (int)$row['endtime'] < $GLOBALS['EXEC_TIME'] || $GLOBALS['EXEC_TIME'] < (int)$row['starttime']; + } + $result->free(); + return $row; + } + + /** + * Counting content elements for a single language on a page. + * + * @param int $pageId Page id to select for. + * @param int $sysLang Sys language uid + * @return int Number of content elements from the PID where the language is set to a certain value. + */ + private function getContentElementCount(int $pageId, int $sysLang): int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); + return (int)$queryBuilder + ->count('uid') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) + ) + ) + ->andWhere( + $queryBuilder->expr()->eq( + 'sys_language_uid', + $queryBuilder->createNamedParameter($sysLang, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php new file mode 100644 index 0000000..4bb257d --- /dev/null +++ b/Configuration/Backend/Modules.php @@ -0,0 +1,45 @@ + [ + 'parent' => 'content_status', + 'position' => ['before' => '*'], + 'access' => 'user', + 'path' => '/module/web/info/overview', + 'iconIdentifier' => 'module-info', + 'labels' => 'info.modules.overview', + 'routes' => [ + '_default' => [ + 'target' => PageInformationController::class . '::handleRequest', + ], + ], + 'moduleData' => [ + 'pages' => '0', + 'depth' => 0, + 'lang' => 0, + ], + ], + 'web_info_translations' => [ + 'parent' => 'content_status', + 'position' => ['after' => 'web_info_overview'], + 'access' => 'user', + 'path' => '/module/web/info/translations', + 'iconIdentifier' => 'module-info', + 'labels' => 'info.modules.translations', + 'routes' => [ + '_default' => [ + 'target' => TranslationStatusController::class . '::handleRequest', + ], + ], + 'moduleData' => [ + 'depth' => 0, + 'lang' => 0, + ], + ], +]; diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..1cf8300 --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,8 @@ + [], + 'imports' => [ + '@typo3/info/' => 'EXT:info/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..cb3863e --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + TYPO3\CMS\Info\: + resource: '../Classes/*' diff --git a/Configuration/page.tsconfig b/Configuration/page.tsconfig new file mode 100644 index 0000000..d3c6ef0 --- /dev/null +++ b/Configuration/page.tsconfig @@ -0,0 +1,18 @@ +mod.web_info.fieldDefinitions { + 0 { + label = LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:pages_0 + fields = title,uid,sys_language_uid,slug,starttime,endtime,fe_group,target,link,shortcut,shortcut_mode + } + 1 { + label = LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:pages_1 + fields = title,uid,###ALL_TABLES### + } + 2 { + label = LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:pages_2 + fields = title,uid,sys_language_uid,lastUpdated,newUntil,cache_timeout,php_tree_stop,TSconfig,is_siteroot + } + 3 { + label = LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:pages_layouts + fields = title,uid,sys_language_uid,actual_backend_layout,backend_layout,backend_layout_next_level,layout + } +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..fe3e21f --- /dev/null +++ b/README.rst @@ -0,0 +1,11 @@ +======================== +TYPO3 extension ``info`` +======================== + +This TYPO3 backend module displays general information, such as a page tree +overview and localization information. + +:Repository: https://github.com/typo3/typo3 +:Issues: https://forge.typo3.org/ +:Read online: https://docs.typo3.org/ +:Packagist: https://packagist.org/packages/typo3/cms-info diff --git a/Resources/Private/Language/Modules/overview.xlf b/Resources/Private/Language/Modules/overview.xlf new file mode 100644 index 0000000..8c27b81 --- /dev/null +++ b/Resources/Private/Language/Modules/overview.xlf @@ -0,0 +1,18 @@ + + + +
+ + + View page records and settings in a tree structure with detailed metadata. + + + Pagetree Overview + + + + + + + + diff --git a/Resources/Private/Language/Modules/translations.xlf b/Resources/Private/Language/Modules/translations.xlf new file mode 100644 index 0000000..a5ffb7a --- /dev/null +++ b/Resources/Private/Language/Modules/translations.xlf @@ -0,0 +1,18 @@ + + + +
+ + + Check translation status and manage localized content for pages. + + + Localization Overview + + + + + + + + diff --git a/Resources/Private/Language/locallang_webinfo.xlf b/Resources/Private/Language/locallang_webinfo.xlf new file mode 100644 index 0000000..b9a7f59 --- /dev/null +++ b/Resources/Private/Language/locallang_webinfo.xlf @@ -0,0 +1,89 @@ + + + +
+ + + Pagetree overview + + + Basic settings + + + Cache and Age + + + Record overview + + + Layouts + + + Actual backend layout + + + Localization overview + + + Edit page content + + + Edit translated page content + + + View page + + + View translated page + + + Edit page properties + + + Edit translated page properties + + + Edit all page properties + + + Edit all translated page properties + + + Multiple translated page records exist for this language, but only one is allowed. Please remove the extra records. + + + Page + + + Default + + + Create new translation headers + + + Content Element Count + + + Depth + + + Type + + + Language + + + No access + + + You don't have access to this module. + + + There are no page information available. + + + Please select a page in the page tree. + + + + diff --git a/Resources/Private/Layouts/Module.fluid.html b/Resources/Private/Layouts/Module.fluid.html new file mode 100644 index 0000000..d36e88c --- /dev/null +++ b/Resources/Private/Layouts/Module.fluid.html @@ -0,0 +1,27 @@ + + + + +
+ + + +
+ +
+ +
+
+ + {headerContent} + + {footerContent} +
+
+ + + + diff --git a/Resources/Private/Partials/DropdownMenu.fluid.html b/Resources/Private/Partials/DropdownMenu.fluid.html new file mode 100644 index 0000000..c8c20d4 --- /dev/null +++ b/Resources/Private/Partials/DropdownMenu.fluid.html @@ -0,0 +1,24 @@ + + +
+ +
+ + diff --git a/Resources/Private/Templates/PageInformation.fluid.html b/Resources/Private/Templates/PageInformation.fluid.html new file mode 100644 index 0000000..4d49731 --- /dev/null +++ b/Resources/Private/Templates/PageInformation.fluid.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ {content} +
+ + + +
+
+
+
+
+ +
+ + diff --git a/Resources/Private/Templates/TranslationStatus.fluid.html b/Resources/Private/Templates/TranslationStatus.fluid.html new file mode 100644 index 0000000..2bfe0a4 --- /dev/null +++ b/Resources/Private/Templates/TranslationStatus.fluid.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + +
+
+
+ + +
+
+ + +
+
+ {content} +
+
+ + + +
+
+
+
+
+ +
+ + diff --git a/Resources/Public/Icons/Extension.png b/Resources/Public/Icons/Extension.png new file mode 100644 index 0000000..889c612 Binary files /dev/null and b/Resources/Public/Icons/Extension.png differ diff --git a/Resources/Public/JavaScript/translation-status.js b/Resources/Public/JavaScript/translation-status.js new file mode 100644 index 0000000..6634a08 --- /dev/null +++ b/Resources/Public/JavaScript/translation-status.js @@ -0,0 +1,13 @@ +/* + * 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! + */ +import r from"@typo3/core/event/regular-event.js";import e from"@typo3/backend/icons.js";class i{constructor(){this.registerEvents()}registerEvents(){new r("click",this.toggleNewButton).delegateTo(document,'input[type="checkbox"][data-lang]')}async toggleNewButton(){const t=document.querySelector(`.t3js-language-new[data-lang="${this.dataset.lang}"]`),a=t.querySelector(".t3js-icon"),n=document.querySelectorAll(`input[type="checkbox"][data-lang="${this.dataset.lang}"]:checked`),s=new URL(location.origin+t.dataset.editUrl);n.forEach(c=>{s.searchParams.set(`cmd[pages][${c.dataset.uid}][localize]`,this.dataset.lang)});const o=n.length===0;t.href=s.toString(),t.classList.toggle("disabled",o);const l=await e.getIcon(a.dataset.identifier,e.sizes.small,null,o?e.states.disabled:e.states.default);a.replaceWith(document.createRange().createContextualFragment(l))}}var d=new i;export{d as default}; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..555d2d2 --- /dev/null +++ b/composer.json @@ -0,0 +1,59 @@ +{ + "name": "typo3/cms-info", + "type": "typo3-cms-framework", + "description": "TYPO3 CMS Info - TYPO3 backend module for displaying information, such as a pagetree overview and localization information.", + "homepage": "https://typo3.community/", + "funding": [ + { + "type": "membership", + "url": "https://typo3.org/membership" + } + ], + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "support": { + "issues": "https://forge.typo3.org/issues/", + "forum": "https://talk.typo3.org/", + "source": "https://github.com/TYPO3/typo3/", + "docs": "https://docs.typo3.org/", + "rss": "https://news.typo3.com/rss/", + "chat": "https://typo3.community/meet/slack/", + "security": "https://typo3.org/security/" + }, + "config": { + "sort-packages": true + }, + "require": { + "typo3/cms-core": "15.0.*@dev" + }, + "conflict": { + "typo3/cms": "*" + }, + "replace": { + "typo3/cms-info-pagetsconfig": "self.version" + }, + "extra": { + "branch-alias": { + "dev-main": "15.0.x-dev" + }, + "typo3/cms": { + "Package": { + "partOfFactoryDefault": true + }, + "extension-key": "info" + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Info\\": "Classes/" + } + } +}