1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Backend\Controller\Page;
19
20use Psr\Http\Message\ResponseInterface;
21use Psr\Http\Message\ServerRequestInterface;
22use TYPO3\CMS\Backend\Template\ModuleTemplate;
23use TYPO3\CMS\Backend\Utility\BackendUtility;
24use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
25use TYPO3\CMS\Core\Database\ConnectionPool;
26use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27use TYPO3\CMS\Core\DataHandling\DataHandler;
28use TYPO3\CMS\Core\Http\HtmlResponse;
29use TYPO3\CMS\Core\Imaging\Icon;
30use TYPO3\CMS\Core\Imaging\IconFactory;
31use TYPO3\CMS\Core\Localization\LanguageService;
32use TYPO3\CMS\Core\Type\Bitmask\Permission;
33use TYPO3\CMS\Core\Utility\GeneralUtility;
34use TYPO3\CMS\Fluid\View\StandaloneView;
35
36/**
37 * "Sort sub pages" controller - reachable from context menu "more" on page records
38 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
39 */
40class SortSubPagesController
41{
42    /**
43     * ModuleTemplate object
44     *
45     * @var ModuleTemplate
46     */
47    protected $moduleTemplate;
48
49    /**
50     * Constructor Method
51     *
52     * @var ModuleTemplate $moduleTemplate
53     */
54    public function __construct(ModuleTemplate $moduleTemplate = null)
55    {
56        $this->moduleTemplate = $moduleTemplate ?? GeneralUtility::makeInstance(ModuleTemplate::class);
57    }
58
59    /**
60     * Main function Handling input variables and rendering main view
61     *
62     * @param ServerRequestInterface $request
63     * @return ResponseInterface Response
64     */
65    public function mainAction(ServerRequestInterface $request): ResponseInterface
66    {
67        $backendUser = $this->getBackendUser();
68        $parentPageUid = (int)$request->getQueryParams()['id'];
69
70        // Show only if there is a valid page and if this page may be viewed by the user
71        $pageInformation = BackendUtility::readPageAccess($parentPageUid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW));
72        if (!is_array($pageInformation)) {
73            // User has no permission on parent page, should not happen, just render an empty page
74            $this->moduleTemplate->setContent('');
75            return new HtmlResponse($this->moduleTemplate->renderContent());
76        }
77
78        // Doc header handling
79        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
80        $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageInformation);
81        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
82        $cshButton = $buttonBar->makeHelpButton()
83            ->setModuleName('pages_sort')
84            ->setFieldName('pages_sort');
85        $viewButton = $buttonBar->makeLinkButton()
86            ->setOnClick(BackendUtility::viewOnClick($parentPageUid, '', BackendUtility::BEgetRootLine($parentPageUid)))
87            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
88            ->setIcon($iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
89            ->setHref('#');
90        $shortcutButton = $buttonBar->makeShortcutButton()
91            ->setModuleName('pages_sort')
92            ->setGetVariables(['id']);
93        $buttonBar->addButton($cshButton)->addButton($viewButton)->addButton($shortcutButton);
94
95        // Main view setup
96        $view = GeneralUtility::makeInstance(StandaloneView::class);
97        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
98            'EXT:backend/Resources/Private/Templates/Page/SortSubPages.html'
99        ));
100
101        $isInWorkspace = $backendUser->workspace !== 0;
102        $view->assign('isInWorkspace', $isInWorkspace);
103        $view->assign('maxTitleLength', $backendUser->uc['titleLen'] ?? 20);
104        $view->assign('parentPageUid', $parentPageUid);
105        $view->assign('dateFormat', $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']);
106        $view->assign('timeFormat', $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']);
107
108        if (!$isInWorkspace) {
109            // Apply new sorting if given
110            $newSortBy = $request->getQueryParams()['newSortBy'] ?? null;
111            if ($newSortBy && in_array($newSortBy, ['title', 'subtitle', 'nav_title', 'crdate', 'tstamp'], true)) {
112                $this->sortSubPagesByField($parentPageUid, (string)$newSortBy);
113            } elseif ($newSortBy && $newSortBy === 'reverseCurrentSorting') {
114                $this->reverseSortingOfPages($parentPageUid);
115            }
116
117            // Get sub pages, loop through them and add page/user specific permission details
118            $pageRecords = $this->getSubPagesOfPage($parentPageUid);
119            $hasInvisiblePage = false;
120            $subPages = [];
121            foreach ($pageRecords as $page) {
122                $pageWithPermissions = [];
123                $pageWithPermissions['record'] = $page;
124                $calculatedPermissions = $backendUser->calcPerms($page);
125                $pageWithPermissions['canEdit'] = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_EDIT;
126                $canSeePage = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_SHOW;
127                if ($canSeePage) {
128                    $subPages[] = $pageWithPermissions;
129                } else {
130                    $hasInvisiblePage = true;
131                }
132            }
133            $view->assign('subPages', $subPages);
134            $view->assign('hasInvisiblePage', $hasInvisiblePage);
135        }
136
137        $this->moduleTemplate->setContent($view->render());
138        return new HtmlResponse($this->moduleTemplate->renderContent());
139    }
140
141    /**
142     * Sort sub pages of given uid by field name alphabetically
143     *
144     * @param int $parentPageUid Parent page uid
145     * @param string $newSortBy Field name to sort by
146     * @throws \RuntimeException If $newSortBy does not validate
147     */
148    protected function sortSubPagesByField(int $parentPageUid, string $newSortBy)
149    {
150        if (!in_array($newSortBy, ['title', 'subtitle', 'nav_title', 'crdate', 'tstamp'], true)) {
151            throw new \RuntimeException(
152                'New sort by must be one of "title", "subtitle", "nav_title", "crdate" or tstamp',
153                1498924810
154            );
155        }
156        $subPages = $this->getSubPagesOfPage($parentPageUid, $newSortBy);
157        if (!empty($subPages)) {
158            $subPages = array_reverse($subPages);
159            $this->persistNewSubPageOrder($parentPageUid, $subPages);
160        }
161    }
162
163    /**
164     * Reverse current sorting of sub pages
165     *
166     * @param int $parentPageUid Parent page uid
167     */
168    protected function reverseSortingOfPages(int $parentPageUid)
169    {
170        $subPages = $this->getSubPagesOfPage($parentPageUid);
171        if (!empty($subPages)) {
172            $this->persistNewSubPageOrder($parentPageUid, $subPages);
173        }
174    }
175
176    /**
177     * Store new sub page order
178     *
179     * @param int $parentPageUid Parent page uid
180     * @param array $subPages List of sub pages in new order
181     */
182    protected function persistNewSubPageOrder(int $parentPageUid, array $subPages)
183    {
184        $commandArray = [];
185        foreach ($subPages as $subPage) {
186            $commandArray['pages'][$subPage['uid']]['move'] = $parentPageUid;
187        }
188        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
189        $dataHandler->start([], $commandArray);
190        $dataHandler->process_cmdmap();
191        BackendUtility::setUpdateSignal('updatePageTree');
192    }
193
194    /**
195     * Get a list of sub pages with some all fields from given page.
196     * Fetch all data fields for full page icon display
197     *
198     * @param int $parentPageUid Get sub pages from this pages
199     * @param string $orderBy Order pages by this field
200     * @return array
201     */
202    protected function getSubPagesOfPage(int $parentPageUid, string $orderBy = 'sorting'): array
203    {
204        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
205        $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
206        return $queryBuilder->select('*')
207            ->from('pages')
208            ->where(
209                $queryBuilder->expr()->eq('sys_language_uid', 0),
210                $queryBuilder->expr()->eq(
211                    'pid',
212                    $queryBuilder->createNamedParameter($parentPageUid, \PDO::PARAM_INT)
213                )
214            )
215            ->orderBy($orderBy)
216            ->execute()
217            ->fetchAll();
218    }
219
220    /**
221     * Returns LanguageService
222     *
223     * @return LanguageService
224     */
225    protected function getLanguageService()
226    {
227        return $GLOBALS['LANG'];
228    }
229
230    /**
231     * Returns current BE user
232     *
233     * @return BackendUserAuthentication
234     */
235    protected function getBackendUser()
236    {
237        return $GLOBALS['BE_USER'];
238    }
239}
240