Commit c91e77ef authored by Christian Kuhn's avatar Christian Kuhn
Browse files

[TASK] Integrate "Functions" as context menu

The patch integrates ext:wizard_sortpages and ext:wizard_crpages
as context menu items in "more" of the page record context menus
and gets rid of the two default "Functions" main module entries,
effectively obsoleting ext:func, which will be extracted to TER
for b/w compat in a second step.

* Isolate ext:func by moving its language xml from ext:lang to ext:func
* Routing configuration for "sort pages" and "create pages" in ext:backend
* Context menu handling for "sort pages" and "create pages" in ext:backend
* Free "sort pages" and "create pages" from dependency to ext:frontend
  by not calling PageRepository->getMenu() anymore
* Proper use of request/response instead of _GP access
* Better controller / view separation

Change-Id: I2c7acbb79ddd8404fbef69a1c126d250b976629b
Resolves: #81768
Releases: master
Reviewed-on: https://review.typo3.org/53377

Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <typo3@scripting-base.de>
Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent e6878c49
......@@ -110,10 +110,6 @@
"../typo3/sysext/viewpage/Resources/Public/JavaScript/*",
"../typo3/sysext/viewpage/Resources/Private/TypeScript/*"
],
"TYPO3/CMS/WizardCrpages/*": [
"../typo3/sysext/wizard_crpages/Resources/Public/JavaScript/*",
"../typo3/sysext/wizard_crpages/Resources/Private/TypeScript/*"
],
"TYPO3/CMS/Workspaces/*": [
"../typo3/sysext/workspaces/Resources/Public/JavaScript/*",
"../typo3/sysext/workspaces/Resources/Private/TypeScript/*"
......@@ -129,4 +125,4 @@
"../typo3/sysext/*/Resources/Private/TypeScript/**/*.ts",
"../typo3/sysext/*/Tests/TypeScript/**/*.ts"
]
}
\ No newline at end of file
}
......@@ -133,8 +133,6 @@
"typo3/cms-typo3db-legacy": "self.version",
"typo3/cms-version": "self.version",
"typo3/cms-viewpage": "self.version",
"typo3/cms-wizard-crpages": "self.version",
"typo3/cms-wizard-sortpages": "self.version",
"typo3/cms-workspaces": "self.version"
},
"autoload": {
......@@ -182,8 +180,6 @@
"TYPO3\\CMS\\Tstemplate\\": "typo3/sysext/tstemplate/Classes/",
"TYPO3\\CMS\\Version\\": "typo3/sysext/version/Classes/",
"TYPO3\\CMS\\Viewpage\\": "typo3/sysext/viewpage/Classes/",
"TYPO3\\CMS\\WizardCrpages\\": "typo3/sysext/wizard_crpages/Classes/",
"TYPO3\\CMS\\WizardSortpages\\": "typo3/sysext/wizard_sortpages/Classes/",
"TYPO3\\CMS\\Workspaces\\": "typo3/sysext/workspaces/Classes/"
},
"classmap": [
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "546e2c9d87c42a0c785bb55ec036056a",
"content-hash": "ba4bebe7fcec50b689f276105864ae12",
"packages": [
{
"name": "cogpowered/finediff",
......
......@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
/**
......@@ -98,6 +99,16 @@ class PageProvider extends RecordProvider
'iconIdentifier' => 'actions-page-new',
'callbackAction' => 'newPageWizard',
],
'pagesSort' => [
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_sort.xlf:title',
'iconIdentifier' => 'actions-page-move',
'callbackAction' => 'pagesSort',
],
'pagesNewMultiple' => [
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_new.xlf:title',
'iconIdentifier' => 'apps-pagetree-drag-move-between',
'callbackAction' => 'pagesNewMultiple',
],
'openListModule' => [
'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
'iconIdentifier' => 'actions-system-list-open',
......@@ -181,6 +192,7 @@ class PageProvider extends RecordProvider
break;
case 'new':
case 'newWizard':
case 'pagesNewMultiple':
$canRender = $this->canBeCreated();
break;
case 'info':
......@@ -201,6 +213,9 @@ class PageProvider extends RecordProvider
case 'openListModule':
$canRender = $this->canOpenListModule();
break;
case 'pagesSort':
$canRender = $this->canBeSorted();
break;
case 'mountAsTreeRoot':
$canRender = !$this->isRoot();
break;
......@@ -332,6 +347,19 @@ class PageProvider extends RecordProvider
&& !$this->isDeletePlaceholder();
}
/**
* Check if sub pages of given page can be sorted
*
* @return bool
*/
protected function canBeSorted(): bool
{
return $this->backendUser->check('tables_modify', $this->table)
&& $this->hasPagePermission(Permission::CONTENT_EDIT)
&& !$this->isDeletePlaceholder()
&& $this->backendUser->workspace === 0;
}
/**
* Checks if the page is allowed to be removed
*
......@@ -424,6 +452,16 @@ class PageProvider extends RecordProvider
if ($itemName === 'pasteAfter') {
$attributes += $this->getPasteAdditionalAttributes('after');
}
if ($itemName === 'pagesSort') {
$attributes += [
'data-pages-sort-url' => BackendUtility::getModuleUrl('pages_sort', ['id' => $this->record['uid']]),
];
}
if ($itemName === 'pagesNewMultiple') {
$attributes += [
'data-pages-new-multiple-url' => BackendUtility::getModuleUrl('pages_new', ['id' => $this->record['uid']]),
];
}
return $attributes;
}
......
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Backend\Controller\Page;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* "Create multiple pages" controller
*/
class NewMultiplePagesController
{
/**
* ModuleTemplate object
*
* @var ModuleTemplate
*/
protected $moduleTemplate;
/**
* Constructor Method
*
* @var $moduleTemplate ModuleTemplate
*/
public function __construct(ModuleTemplate $moduleTemplate = null)
{
$this->moduleTemplate = $moduleTemplate ?? GeneralUtility::makeInstance(ModuleTemplate::class);
}
/**
* Main function Handling input variables and rendering main view
*
* @param $request ServerRequestInterface
* @param $response ResponseInterface
* @return ResponseInterface Response
*/
public function mainAction(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$backendUser = $this->getBackendUser();
$pageUid = (int)$request->getQueryParams()['id'];
// Show only if there is a valid page and if this page may be viewed by the user
$pageRecord = BackendUtility::readPageAccess($pageUid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW));
if (!is_array($pageRecord)) {
// User has no permission on parent page, should not happen, just render an empty page
$this->moduleTemplate->setContent('');
$response->getBody()->write($this->moduleTemplate->renderContent());
return $response;
}
// Doc header handling
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageRecord);
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
$cshButton = $buttonBar->makeHelpButton()
->setModuleName('pages_new')
->setFieldName('pages_new');
$viewButton = $buttonBar->makeLinkButton()
->setOnClick(BackendUtility::viewOnClick($pageUid, '', BackendUtility::BEgetRootLine($pageUid)))
->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
->setHref('#');
$shortcutButton = $buttonBar->makeShortcutButton()
->setModuleName('pages_new')
->setGetVariables(['id']);
$buttonBar->addButton($cshButton)->addButton($viewButton)->addButton($shortcutButton);
// Main view setup
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
'EXT:backend/Resources/Private/Templates/Page/NewPages.html'
));
$calculatedPermissions = $backendUser->calcPerms($pageRecord);
$canCreateNew = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_NEW;
$view->assign('canCreateNew', $canCreateNew);
$view->assign('maxTitleLength', $backendUser->uc['titleLen'] ?? 20);
$view->assign('pageUid', $pageUid);
if ($canCreateNew) {
$newPagesData = (array)$request->getParsedBody()['pages'];
if (!empty($newPagesData)) {
$hasNewPagesData = true;
$afterExisting = isset($request->getParsedBody()['createInListEnd']);
$hidePages = isset($request->getParsedBody()['hidePages']);
$hidePagesInMenu = isset($request->getParsedBody()['hidePagesInMenus']);
$pagesCreated = $this->createPages($newPagesData, $pageUid, $afterExisting, $hidePages, $hidePagesInMenu);
$view->assign('pagesCreated', $pagesCreated);
$subPages = $this->getSubPagesOfPage($pageUid);
$visiblePages = [];
foreach ($subPages as $page) {
$calculatedPermissions = $backendUser->calcPerms($page);
if ($backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_SHOW) {
$visiblePages[] = $page;
}
}
$view->assign('visiblePages', $visiblePages);
} else {
$hasNewPagesData = false;
$view->assign('pageTypes', $this->getTypeSelectData($pageUid));
}
$view->assign('hasNewPagesData', $hasNewPagesData);
}
$this->moduleTemplate->setContent($view->render());
$response->getBody()->write($this->moduleTemplate->renderContent());
return $response;
}
/**
* Persist new pages in DB
*
* @param array $newPagesData Data array with title and page type
* @param int $pageUid Uid of page new pages should be added in
* @param bool $afterExisting True if new pages should be created after existing pages
* @param bool $hidePages True if new pages should be set to hidden
* @param bool $hidePagesInMenu True if new pages should be set to hidden in menu
* @return bool TRUE if at least on pages has been added
*/
protected function createPages(array $newPagesData, int $pageUid, bool $afterExisting, bool $hidePages, bool $hidePagesInMenu): bool
{
$pagesCreated = false;
// Set first pid to "-1 * uid of last existing sub page" if pages should be created at end
$firstPid = $pageUid;
if ($afterExisting) {
$subPages = $this->getSubPagesOfPage($pageUid);
$lastPage = end($subPages);
if (isset($lastPage['uid']) && MathUtility::canBeInterpretedAsInteger($lastPage['uid'])) {
$firstPid = -(int)($lastPage['uid']);
}
}
$commandArray = [];
$firstRecord = true;
$previousIdentifier = '';
foreach ($newPagesData as $identifier => $data) {
if (!trim($data['title'])) {
continue;
} else {
$commandArray['pages'][$identifier]['hidden'] = $hidePages;
$commandArray['pages'][$identifier]['nav_hide'] = $hidePagesInMenu;
$commandArray['pages'][$identifier]['title'] = $data['title'];
$commandArray['pages'][$identifier]['doktype'] = $data['doktype'];
if ($firstRecord) {
$firstRecord = false;
$commandArray['pages'][$identifier]['pid'] = $firstPid;
} else {
$commandArray['pages'][$identifier]['pid'] = '-' . $previousIdentifier;
}
$previousIdentifier = $identifier;
}
}
if (!empty($commandArray)) {
$pagesCreated = true;
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
// Set default TCA values specific for the user
$backendUser = $this->getBackendUser();
$tcaDefaultOverride = $backendUser->getTSConfigProp('TCAdefaults');
if (is_array($tcaDefaultOverride)) {
$dataHandler->setDefaultsFromUserTS($tcaDefaultOverride);
}
$dataHandler->start($commandArray, []);
$dataHandler->process_datamap();
BackendUtility::setUpdateSignal('updatePageTree');
}
return $pagesCreated;
}
/**
* Page selector type data
*
* @param int $pageUid Page Uid
* @return array
*/
protected function getTypeSelectData(int $pageUid)
{
$tsConfig = BackendUtility::getPagesTSconfig($pageUid);
$pagesTsConfig = $tsConfig['TCEFORM.']['pages.'] ?? [];
// Find all available doktypes for the current user
$types = $GLOBALS['PAGES_TYPES'];
unset($types['default']);
$types = array_keys($types);
$types[] = 1; // default
$types[] = 3; // link
$types[] = 4; // shortcut
$types[] = 7; // mount point
$types[] = 199; // spacer
if (!$this->getBackendUser()->isAdmin() && isset($this->getBackendUser()->groupData['pagetypes_select'])) {
$types = GeneralUtility::trimExplode(',', $this->getBackendUser()->groupData['pagetypes_select'], true);
}
$removeItems = isset($pagesTsConfig['doktype.']['removeItems']) ? GeneralUtility::trimExplode(',', $pagesTsConfig['doktype.']['removeItems'], true) : [];
$allowedDoktypes = array_diff($types, $removeItems);
// All doktypes in the TCA
$availableDoktypes = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'];
// Sort by group and allowedDoktypes
$groupedData = [];
$groupLabel = '';
foreach ($availableDoktypes as $doktypeData) {
// If it is a group, save the group label for the children underneath
if ($doktypeData[1] === '--div--') {
$groupLabel = $doktypeData[0];
} else {
if (in_array($doktypeData[1], $allowedDoktypes)) {
$groupedData[$groupLabel][] = $doktypeData;
}
}
}
return $groupedData;
}
/**
* Get a list of sub pages with some all fields from given page.
* Fetch all data fields for full page icon display
*
* @param int $pageUid Get sub pages from this pages
* @return array
*/
protected function getSubPagesOfPage(int $pageUid): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
return $queryBuilder->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq(
'pid',
$queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)
)
)
->orderBy('sorting')
->execute()
->fetchAll();
}
/**
* Returns LanguageService
*
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* Returns current BE user
*
* @return BackendUserAuthentication
*/
protected function getBackendUser()
{
return $GLOBALS['BE_USER'];
}
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Backend\Controller\Page;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* "Sort sub pages" controller - reachable from context menu "more" on page records
*/
class SortSubPagesController
{
/**
* ModuleTemplate object
*
* @var ModuleTemplate
*/
protected $moduleTemplate;
/**
* Constructor Method
*
* @var $moduleTemplate ModuleTemplate
*/
public function __construct(ModuleTemplate $moduleTemplate = null)
{
$this->moduleTemplate = $moduleTemplate ?? GeneralUtility::makeInstance(ModuleTemplate::class);
}
/**
* Main function Handling input variables and rendering main view
*
* @param $request ServerRequestInterface
* @param $response ResponseInterface
* @return ResponseInterface Response
*/
public function mainAction(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$backendUser = $this->getBackendUser();
$parentPageUid = (int)$request->getQueryParams()['id'];
// Show only if there is a valid page and if this page may be viewed by the user
$pageInformation = BackendUtility::readPageAccess($parentPageUid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW));
if (!is_array($pageInformation)) {
// User has no permission on parent page, should not happen, just render an empty page
$this->moduleTemplate->setContent('');
$response->getBody()->write($this->moduleTemplate->renderContent());
return $response;
}
// Doc header handling
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageInformation);
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
$cshButton = $buttonBar->makeHelpButton()
->setModuleName('pages_sort')
->setFieldName('pages_sort');
$viewButton = $buttonBar->makeLinkButton()
->setOnClick(BackendUtility::viewOnClick($parentPageUid, '', BackendUtility::BEgetRootLine($parentPageUid)))
->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
->setIcon($iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
->setHref('#');
$shortcutButton = $buttonBar->makeShortcutButton()
->setModuleName('pages_sort')
->setGetVariables(['id']);
$buttonBar->addButton($cshButton)->addButton($viewButton)->addButton($shortcutButton);
// Main view setup
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
'EXT:backend/Resources/Private/Templates/Page/SortSubPages.html'
));
$isInWorkspace = $backendUser->workspace !== 0;
$view->assign('isInWorkspace', $isInWorkspace);
$view->assign('maxTitleLength', $backendUser->uc['titleLen'] ?? 20);
$view->assign('parentPageUid', $parentPageUid);
$view->assign('dateFormat', $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']);
$view->assign('timeFormat', $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']);
if (!$isInWorkspace) {
// Apply new sorting if given
$newSortBy = $request->getQueryParams()['newSortBy'] ?? null;
if ($newSortBy && in_array($newSortBy, ['title', 'subtitle', 'crdate', 'tstamp'], true)) {
$this->sortSubPagesByField($parentPageUid, (string)$newSortBy);
} elseif ($newSortBy && $newSortBy === 'reverseCurrentSorting') {
$this->reverseSortingOfPages($parentPageUid);
}
// Get sub pages, loop through them and add page/user specific permission details
$pageRecords = $this->getSubPagesOfPage($parentPageUid);
$hasInvisiblePage = false;
$subPages = [];
foreach ($pageRecords as $page) {
$pageWithPermissions = [];
$pageWithPermissions['record'] = $page;
$calculatedPermissions = $backendUser->calcPerms($page);
$pageWithPermissions['canEdit'] = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_EDIT;
$canSeePage = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_SHOW;
if ($canSeePage) {
$subPages[] = $pageWithPermissions;
} else {
$hasInvisiblePage = true;
}
}
$view->assign('subPages', $subPages);
$view->assign('hasInvisiblePage', $hasInvisiblePage);
}
$this->moduleTemplate->setContent($view->render());
$response->getBody()->write($this->moduleTemplate->renderContent());
return $response;
}
/**
* Sort sub pages of given uid by field name alphabetically
*
* @param int $parentPageUid Parent page uid
* @param string $newSortBy Field name to sort by
* @throws \RuntimeException If $newSortBy does not validate
*/
protected function sortSubPagesByField(int $parentPageUid, string $newSortBy)
{
if (!in_array($newSortBy, ['title', 'subtitle', 'crdate', 'tstamp'], true)) {
throw new \RuntimeException(
'New sort by must be one of "title", "subtitle", "crdate" or tstamp',
1498924810
);
}
$subPages = $this->getSubPagesOfPage($parentPageUid, $newSortBy);
if (!empty($subPages)) {
$subPages = array_reverse($subPages);
$this->persistNewSubPageOrder($parentPageUid, $subPages);
}