moduleTemplateFactory->create($request); $pageinfo = []; $pidInfo = []; $newPagesInto = false; $newContentInto = false; $newPagesAfter = false; $tRows = []; $beUser = $this->getBackendUserAuthentication(); // Page-selection permission clause (reading) $permsClause = $beUser->getPagePermsClause(Permission::PAGE_SHOW); // This will hide records from display - it has nothing to do with user rights!! $pidList = (string)($beUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? ''); if (!empty($pidList)) { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); $permsClause .= ' AND ' . $queryBuilder->expr()->notIn( 'pages.uid', GeneralUtility::intExplode(',', $pidList) ); } // Setting GPvars: $parsedBody = $request->getParsedBody(); $queryParams = $request->getQueryParams(); // The page id to operate from $pageUid = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0); $returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request); // Setting up the context sensitive menu: $this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-menu.js'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/new-content-element-wizard-button.js'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/page-wizard/new-page-wizard-button.js'); // If a positive id is supplied, ask for the page record with permission information contained: if ($pageUid > 0) { $pageinfo = BackendUtility::readPageAccess($pageUid, $permsClause) ?: []; } // If a page-record was returned, the user had read-access to the page. if ($pageinfo['uid'] ?? false) { // Get record of parent page $pidInfo = BackendUtility::getRecord('pages', ($pageinfo['pid'] ?? 0)) ?? []; // Checking the permissions for the user with regard to the parent page: Can he create new pages, new // content record, new page after? if ($beUser->doesUserHaveAccess($pageinfo, Permission::PAGE_NEW)) { $newPagesInto = true; } if ($beUser->doesUserHaveAccess($pageinfo, Permission::CONTENT_EDIT)) { $newContentInto = true; } if (($beUser->isAdmin() || !empty($pidInfo)) && $beUser->doesUserHaveAccess($pidInfo, Permission::PAGE_NEW)) { $newPagesAfter = true; } $breadcrumbContext = new BreadcrumbContext( $this->recordFactory->createResolvedRecordFromDatabaseRow('pages', $pageinfo), [] ); $view->getDocHeaderComponent()->setBreadcrumbContext($breadcrumbContext); } elseif ($beUser->isAdmin()) { // Admins can do it all $newPagesInto = true; $newContentInto = true; $newPagesAfter = false; } else { // People with no permission can do nothing $newPagesInto = false; $newContentInto = false; $newPagesAfter = false; } $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']; if ($pageinfo['uid'] ?? false) { $title = strip_tags(BackendUtility::getRecordTitle('pages', $pageinfo)); } $view->setTitle($title); // Acquiring TSconfig for this module/current page: $web_list_modTSconfig = BackendUtility::getPagesTSconfig($pageinfo['uid'] ?? 0)['mod.']['web_list.'] ?? []; $allowedNewTables = GeneralUtility::trimExplode(',', $web_list_modTSconfig['allowedNewTables'] ?? '', true); $deniedNewTables = GeneralUtility::trimExplode(',', $web_list_modTSconfig['deniedNewTables'] ?? '', true); // Acquiring TSconfig for this module/parent page $web_list_modTSconfig_pid = BackendUtility::getPagesTSconfig($pageinfo['pid'] ?? 0)['mod.']['web_list.'] ?? []; $allowedNewTables_pid = GeneralUtility::trimExplode(',', $web_list_modTSconfig_pid['allowedNewTables'] ?? '', true); $deniedNewTables_pid = GeneralUtility::trimExplode(',', $web_list_modTSconfig_pid['deniedNewTables'] ?? '', true); if (!$this->isRecordCreationAllowedForTable('pages', $allowedNewTables, $deniedNewTables)) { $newPagesInto = false; } if (!$this->isRecordCreationAllowedForTable('pages', $allowedNewTables_pid, $deniedNewTables_pid)) { $newPagesAfter = false; } // If there was a page - or if the user is admin (admins has access to the root) we proceed, otherwise just output the header if (empty($pageinfo['uid']) && !$this->getBackendUserAuthentication()->isAdmin()) { return $view->renderResponse('NewRecord/NewRecord'); } $lang = $this->getLanguageService(); // Get TSconfig for current page $pageTS = BackendUtility::getPagesTSconfig($pageUid); // Finish initializing new pages options with TSconfig // Each new page option may be hidden by TSconfig $displayNewPagesIntoLink = $newPagesInto && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageInside']); $displayNewPagesAfterLink = $newPagesAfter && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageAfter']); $iconFile = [ 'backendaccess' => $this->iconFactory->getIcon('status-user-group-backend', IconSize::SMALL), 'content' => $this->iconFactory->getIcon('content-panel', IconSize::SMALL)->render(), 'frontendaccess' => $this->iconFactory->getIcon('status-user-group-frontend', IconSize::SMALL), 'system' => $this->iconFactory->getIcon('apps-pagetree-root', IconSize::SMALL), ]; $groupTitles = [ 'backendaccess' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:recordgroup.backendaccess'), 'content' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:recordgroup.content'), 'frontendaccess' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:recordgroup.frontendaccess'), 'system' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:system_records'), ]; $allowedTables = []; foreach ($this->tcaSchemaFactory->all() as $table => $schema) { $isTablesAllowed = match ($table) { 'pages' => $this->isRecordCreationAllowedForTable('pages', $allowedNewTables, $deniedNewTables), 'tt_content' => false, // Skip, as inserting content elements is part of the page module default => $newContentInto && $this->isRecordCreationAllowedForTable($table, $allowedNewTables, $deniedNewTables) && $this->isTableAllowedOnPage($schema, $pageinfo, $pageUid) }; if ($isTablesAllowed) { $allowedTables[] = $table; } } $groupedLinksOnTop = []; foreach ($allowedTables as $table) { $schema = $this->tcaSchemaFactory->get($table); $ctrlTitle = $schema->getTitle(); if ($table === 'pages') { // New pages INSIDE this pages $newPageLinks = []; if ($displayNewPagesIntoLink && $this->isTableAllowedOnPage($schema, $pageinfo, $pageUid)) { // Create link to new page inside $newPageLinks['inside'] = [ 'icon' => $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL), 'label' => $lang->sL($ctrlTitle) . ' (' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.inside') . ')', 'wizardConfiguration' => ['positionData' => ['pageUid' => $pageUid, 'insertPosition' => 'inside']], ]; } // New pages AFTER this pages if ($displayNewPagesAfterLink && $this->isTableAllowedOnPage($schema, $pidInfo, $pageUid)) { $newPageLinks['after'] = [ 'icon' => $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL), 'label' => $lang->sL($ctrlTitle) . ' (' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.after') . ')', 'wizardConfiguration' => ['positionData' => ['pageUid' => $pageUid, 'insertPosition' => 'after']], ]; } if (!empty($newPageLinks)) { $groupedLinksOnTop['pages'] = [ 'title' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:createNewPage'), 'icon' => $this->iconFactory->getIcon('actions-page-new', IconSize::SMALL), 'items' => $newPageLinks, ]; } } else { $nameParts = explode('_', $table); $groupName = $schema->getRawConfiguration()['groupName'] ?? ''; if (!isset($iconFile[$groupName]) || $nameParts[0] === 'tx' || $nameParts[0] === 'tt') { $groupName = $groupName ?: ($nameParts[1] ?? null); // Try to extract extension name if ($groupName) { $_EXTKEY = ''; $titleIsTranslatableLabel = str_starts_with($ctrlTitle, 'LLL:EXT:'); if ($titleIsTranslatableLabel) { // In case the title is a locallang reference, we can simply // extract the extension name from the given extension path. $_EXTKEY = substr($ctrlTitle, 8); $_EXTKEY = substr($_EXTKEY, 0, (int)strpos($_EXTKEY, '/')); } elseif (ExtensionManagementUtility::isLoaded($groupName)) { // In case $title is not a locallang reference, we check the groupName to // be a valid extension key. This most probably work since by convention the // first part after tx_ / tt_ is the extension key. $_EXTKEY = $groupName; } // Fetch the group title from the extension name if ($_EXTKEY !== '') { // Try to get the extension title $package = $this->packageManager->getPackage($_EXTKEY); $groupTitle = $lang->sL('LLL:EXT:' . $_EXTKEY . '/Resources/Private/Language/locallang_db.xlf:extension.title'); // If no localisation available, read title from the Package MetaData if (!$groupTitle) { $groupTitle = $package->getPackageMetaData()->getTitle(); } $extensionIcon = $package->getResources()->getPackageIcon(); if ($extensionIcon !== null) { $iconResource = $this->resourceFactory->createPublicResource($extensionIcon); $iconFile[$groupName] = '' . $groupTitle . ''; } if (!empty($groupTitle)) { $groupTitles[$groupName] = $groupTitle; } else { $groupTitles[$groupName] = ucwords($_EXTKEY); } } } else { // Fall back to "system" in case no $groupName could be found $groupName = 'system'; } } $tRows[$groupName]['title'] = $tRows[$groupName]['title'] ?? $groupTitles[$groupName] ?? $nameParts[1] ?? $ctrlTitle; $tRows[$groupName]['icon'] = $tRows[$groupName]['icon'] ?? $iconFile[$groupName] ?? $iconFile['system'] ?? ''; if ($schema->supportsSubSchema() && !$schema->getSubSchemaTypeInformation()->isPointerToForeignFieldInForeignSchema() && $this->hasRecordTypesForDirectCreation($schema) ) { $tRows[$groupName]['items'][$table]['label'] = $lang->sL($ctrlTitle); $tRows[$groupName]['items'][$table]['icon'] = $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL); $tRows[$groupName]['items'][$table]['types'] = $this->getRecordTypesForDirectCreation($request, $schema, $pageUid, $returnUrl); } else { $tRows[$groupName]['items'][$table] = [ 'url' => $this->renderLink($request, $table, $pageUid, [], $returnUrl), 'icon' => $this->iconFactory->getIconForRecord($table, [], IconSize::SMALL)->render(), 'label' => $lang->sL($ctrlTitle), ]; } } } // User sort $newRecordSortList = isset($pageTS['mod.']['wizards.']['newRecord.']['order']) ? GeneralUtility::trimExplode(',', $pageTS['mod.']['wizards.']['newRecord.']['order'], true) : []; uksort($tRows, static function (string $a, string $b) use ($newRecordSortList, $tRows): int { if ($newRecordSortList !== []) { if (in_array($a, $newRecordSortList) && in_array($b, $newRecordSortList)) { // Both are in the list, return relative to position in array $sub = array_search($a, $newRecordSortList) - array_search($b, $newRecordSortList); $ret = ($sub < 0 ? -1 : $sub == 0) ? 0 : 1; } elseif (in_array($a, $newRecordSortList)) { // First element is in array, put to top $ret = -1; } elseif (in_array($b, $newRecordSortList)) { // Second element is in array, put first to bottom $ret = 1; } else { // No element is in array, return alphabetic order $ret = strnatcasecmp($tRows[$a]['title'] ?? '', $tRows[$b]['title'] ?? ''); } return $ret; } // Return alphabetic order return strnatcasecmp($tRows[$a]['title'] ?? '', $tRows[$b]['title'] ?? ''); }); $tRows = array_merge($groupedLinksOnTop, $tRows); $tRows = $this->eventDispatcher->dispatch( new ModifyNewRecordCreationLinksEvent($tRows, $pageTS, $pageUid, $request) )->groupedCreationLinks; $recordControls = $tRows; if (count($recordControls) === 1) { $items = current($recordControls)['items'] ?? []; if (count($items) === 1) { $item = current($items); // Items for tables with sub-types carry a 'types' sub-array instead of a 'url' // and must fall through to render the selection wizard. if (isset($item['url'])) { return new RedirectResponse($item['url'], 301); } } } $view->assign('recordTypeGroups', $recordControls); // Setting up the buttons and markers for docheader (done after permissions are checked) // Back if ($returnUrl) { $view->addButtonToButtonBar($this->componentFactory->createBackButton($returnUrl), ButtonBar::BUTTON_POSITION_LEFT, 10); } if ($pageinfo['uid'] ?? false) { // View $previewUriBuilder = PreviewUriBuilder::create($pageinfo); if ($previewUriBuilder->isPreviewable()) { $view->addButtonToButtonBar( $this->componentFactory->createViewButton($previewUriBuilder ->withRootLine(BackendUtility::BEgetRootLine($pageinfo['uid'])) ->buildDispatcherDataAttributes() ?? []), ButtonBar::BUTTON_POSITION_LEFT, 30 ); } } return $view->renderResponse('NewRecord/NewRecord'); } /** * Links the string $code to a create-new form for a record in $table created on page $pid * * @param string $table Table name (in which to create new record) * @param int $pid PID value for the "&edit['.$table.']['.$pid.']=new" command (positive/negative) * @param array $additionalParams Additional params, such as "defVals" tp be added to the link * @param string $returnUrl Return URL, falls back to the current request URI when empty * @return string The link. */ private function renderLink(ServerRequestInterface $request, string $table, int $pid, array $additionalParams, string $returnUrl): string { $params = [ 'edit' => [ $table => [ $pid => 'new', ], ], 'returnUrl' => $returnUrl ?: $request->getAttribute('normalizedParams')->getRequestUri(), ]; if ($additionalParams) { $params = array_replace_recursive($params, $additionalParams); } return (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params); } /** * Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row * * @param TcaSchema $schema Table schema * @param array $page Potential parent page * @param int $pageUid Current page id * @return bool Returns TRUE if the tablename $table is allowed to be created on the $page */ private function isTableAllowedOnPage(TcaSchema $schema, array $page, int $pageUid): bool { $rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel); $rootLevelConstraintMatches = ($rootLevelCapability->canExistOnRootLevel() && $pageUid === 0) || ($pageUid && $rootLevelCapability->canExistOnPages()); if (empty($page)) { return $rootLevelConstraintMatches && $this->getBackendUserAuthentication()->isAdmin(); } if (!$this->getBackendUserAuthentication()->workspaceCanCreateNewRecord($schema->getName())) { return false; } // Checking doktype $isAllowed = $this->pageDoktypeRegistry->isRecordTypeAllowedForDoktype($schema->getName(), $page['doktype']); return $rootLevelConstraintMatches && $isAllowed; } /** * Returns whether the record link should be shown for a table * * Returns TRUE if: * - $allowedNewTables and $deniedNewTables are empty * - the table is not found in $deniedNewTables and $allowedNewTables is not set or the $table tablename is found in * $allowedNewTables * * If $table tablename is found in $allowedNewTables and $deniedNewTables, * $deniedNewTables has priority over $allowedNewTables. * * @param string $table Table name to test if in allowedTables * @param array $allowedNewTables Array of new tables that are allowed. * @param array $deniedNewTables Array of new tables that are not allowed. * @return bool Returns TRUE if a link for creating new records should be displayed for $table */ private function isRecordCreationAllowedForTable(string $table, array $allowedNewTables, array $deniedNewTables): bool { if (!$this->getBackendUserAuthentication()->check('tables_modify', $table)) { return false; } $schema = $this->tcaSchemaFactory->get($table); if ($schema->hasCapability(TcaSchemaCapability::AccessReadOnly) || $schema->hasCapability(TcaSchemaCapability::HideInUi) || ($schema->hasCapability(TcaSchemaCapability::AccessAdminOnly) && !$this->getBackendUserAuthentication()->isAdmin()) ) { return false; } // No deny/allow tables are set: if (empty($allowedNewTables) && empty($deniedNewTables)) { return true; } return !in_array($table, $deniedNewTables) && (empty($allowedNewTables) || in_array($table, $allowedNewTables)); } private function hasRecordTypesForDirectCreation(TcaSchema $schema): bool { if (count($schema->getSubSchemata()) <= 1) { return false; } foreach ($schema->getSubSchemata() as $subSchema) { if ((bool)($subSchema->getRawConfiguration()['creationOptions']['enableDirectRecordTypeCreation'] ?? true) === false) { continue; } return true; } return false; } private function getRecordTypesForDirectCreation(ServerRequestInterface $request, TcaSchema $schema, int $pageUid, string $returnUrl): array { $recordTypes = []; $lang = $this->getLanguageService(); $recordTypeField = $schema->getSubSchemaTypeInformation()->getFieldName(); foreach ($schema->getSubSchemata() as $subSchema) { $creationOptions = $subSchema->getRawConfiguration()['creationOptions'] ?? []; if ((bool)($creationOptions['enableDirectRecordTypeCreation'] ?? true) === false) { continue; } $recordTypeName = array_map(trim(...), explode('.', $subSchema->getName(), 2))[1] ?? ''; $recordTypes[$recordTypeName] = [ 'url' => $this->renderLink($request, $schema->getName(), $pageUid, [ 'defVals' => [ $schema->getName() => [ $recordTypeField => $recordTypeName, ], ], ], $returnUrl), 'icon' => $this->iconFactory->getIconForRecord($schema->getName(), [$recordTypeField => $recordTypeName], IconSize::SMALL), 'label' => $lang->sL($this->schemaLabelResolver->getLabelForFieldValue( $schema->getName(), $recordTypeField, $recordTypeName, [], BackendUtility::getPagesTSconfig($pageUid)['TCEFORM.'][$schema->getName() . '.'][$recordTypeField . '.'] ?? [], )) ?: $lang->sL($subSchema->getTitle()) ?: $recordTypeName, ]; } return $recordTypes; } private function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } private function getBackendUserAuthentication(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }