[BUGFIX] Omit `overrideVals` in NewRecordController
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / NewRecordController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Backend\Controller;
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
18 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Backend\Routing\UriBuilder;
21 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
22 use TYPO3\CMS\Backend\Template\ModuleTemplate;
23 use TYPO3\CMS\Backend\Tree\View\NewRecordPageTreeView;
24 use TYPO3\CMS\Backend\Tree\View\PagePositionMap;
25 use TYPO3\CMS\Backend\Utility\BackendUtility;
26 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27 use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
28 use TYPO3\CMS\Core\Database\ConnectionPool;
29 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
30 use TYPO3\CMS\Core\Http\HtmlResponse;
31 use TYPO3\CMS\Core\Http\RedirectResponse;
32 use TYPO3\CMS\Core\Imaging\Icon;
33 use TYPO3\CMS\Core\Localization\LanguageService;
34 use TYPO3\CMS\Core\Type\Bitmask\Permission;
35 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
36 use TYPO3\CMS\Core\Utility\GeneralUtility;
37 use TYPO3\CMS\Core\Utility\HttpUtility;
38 use TYPO3\CMS\Core\Utility\PathUtility;
39 use TYPO3\CMS\Frontend\Page\PageRepository;
40
41 /**
42 * Script class for 'db_new'
43 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
44 */
45 class NewRecordController
46 {
47 use PublicPropertyDeprecationTrait;
48
49 /**
50 * @var array
51 */
52 protected $deprecatedPublicProperties = [
53 'pageinfo' => 'Using $pageinfo of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
54 'pidInfo' => 'Using $pidInfo of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
55 'newPagesInto' => 'Using $newPagesInto of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
56 'newContentInto' => 'Using $newContentInto of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
57 'newPagesAfter' => 'Using $newPagesAfter of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
58 'web_list_modTSconfig' => 'Using $web_list_modTSconfig of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
59 'allowedNewTables' => 'Using $allowedNewTables of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
60 'deniedNewTables' => 'Using $deniedNewTables of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
61 'web_list_modTSconfig_pid' => 'Using $web_list_modTSconfig_pid of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
62 'allowedNewTables_pid' => 'Using $allowedNewTables_pid of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
63 'deniedNewTables_pid' => 'Using $deniedNewTables_pid of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
64 'code' => 'Using $code of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
65 'R_URI' => 'Using $R_URI of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
66 'returnUrl' => 'Using $returnUrl of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
67 'pagesOnly' => 'Using $pagesOnly of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
68 'perms_clause' => 'Using $perms_clause of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
69 'content' => 'Using $content of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
70 'tRows' => 'Using $tRows of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
71 ];
72
73 /**
74 * @var array
75 */
76 protected $pageinfo = [];
77
78 /**
79 * @var array
80 */
81 protected $pidInfo = [];
82
83 /**
84 * @var array
85 */
86 protected $newRecordSortList;
87
88 /**
89 * @var int
90 */
91 protected $newPagesInto;
92
93 /**
94 * @var int
95 */
96 protected $newContentInto;
97
98 /**
99 * @var int
100 */
101 protected $newPagesAfter;
102
103 /**
104 * Determines, whether "Select Position" for new page should be shown
105 *
106 * @var bool
107 */
108 protected $newPagesSelectPosition = true;
109
110 /**
111 * @var array
112 */
113 protected $web_list_modTSconfig;
114
115 /**
116 * @var array
117 */
118 protected $allowedNewTables;
119
120 /**
121 * @var array
122 */
123 protected $deniedNewTables;
124
125 /**
126 * @var array
127 */
128 protected $web_list_modTSconfig_pid;
129
130 /**
131 * @var array
132 */
133 protected $allowedNewTables_pid;
134
135 /**
136 * @var array
137 */
138 protected $deniedNewTables_pid;
139
140 /**
141 * @var string
142 */
143 protected $code;
144
145 /**
146 * @var string
147 */
148 protected $R_URI;
149
150 /**
151 * @var int
152 *
153 * @see \TYPO3\CMS\Backend\Tree\View\NewRecordPageTreeView::expandNext()
154 * @internal
155 */
156 public $id;
157
158 /**
159 * @var string
160 */
161 protected $returnUrl;
162
163 /**
164 * pagesOnly flag.
165 *
166 * @var int
167 */
168 protected $pagesOnly;
169
170 /**
171 * @var string
172 */
173 protected $perms_clause;
174
175 /**
176 * Accumulated HTML output
177 *
178 * @var string
179 */
180 protected $content;
181
182 /**
183 * @var array
184 */
185 protected $tRows;
186
187 /**
188 * ModuleTemplate object
189 *
190 * @var ModuleTemplate
191 */
192 protected $moduleTemplate;
193
194 /**
195 * Constructor
196 */
197 public function __construct()
198 {
199 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
200 $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
201
202 // @see \TYPO3\CMS\Backend\Tree\View\NewRecordPageTreeView::expandNext()
203 $GLOBALS['SOBE'] = $this;
204
205 // @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
206 $this->init($GLOBALS['TYPO3_REQUEST']);
207 }
208
209 /**
210 * Injects the request object for the current request or subrequest
211 * As this controller goes only through the main() method, it is rather simple for now
212 *
213 * @param ServerRequestInterface $request the current request
214 * @return ResponseInterface the response with the content
215 */
216 public function mainAction(ServerRequestInterface $request): ResponseInterface
217 {
218 $response = $this->renderContent($request);
219
220 if (empty($response)) {
221 $response = new HtmlResponse($this->moduleTemplate->renderContent());
222 }
223
224 return $response;
225 }
226
227 /**
228 * Main processing, creating the list of new record tables to select from
229 *
230 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
231 */
232 public function main()
233 {
234 trigger_error('NewRecordController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
235
236 $response = $this->renderContent($GLOBALS['TYPO3_REQUEST']);
237
238 if ($response instanceof RedirectResponse) {
239 HttpUtility::redirect($response->getHeaders()['location'][0]);
240 }
241 }
242
243 /**
244 * Creates the position map for pages wizard
245 *
246 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
247 */
248 public function pagesOnly()
249 {
250 trigger_error('NewRecordController->pagesOnly() will be replaced by protected method renderPositionTree() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
251 $this->renderPositionTree();
252 }
253
254 /**
255 * Create a regular new element (pages and records)
256 *
257 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
258 */
259 public function regularNew()
260 {
261 trigger_error('NewRecordController->regularNew() will be replaced by protected method renderNewRecordControls() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
262 $this->renderNewRecordControls($GLOBALS['TYPO3_REQUEST']);
263 }
264
265 /**
266 * User array sort function used by renderNewRecordControls
267 *
268 * @param string $a First array element for compare
269 * @param string $b First array element for compare
270 * @return int -1 for lower, 0 for equal, 1 for greater
271 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
272 */
273 public function sortNewRecordsByConfig($a, $b)
274 {
275 trigger_error('NewRecordController->sortNewRecordsByConfig() will be replaced by protected method sortTableRows() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
276 return $this->sortTableRows($a, $b);
277 }
278
279 /**
280 * Links the string $code to a create-new form for a record in $table created on page $pid
281 *
282 * @param string $linkText Link text
283 * @param string $table Table name (in which to create new record)
284 * @param int $pid PID value for the "&edit['.$table.']['.$pid.']=new" command (positive/negative)
285 * @param bool $addContentTable If $addContentTable is set, then a new tt_content record is created together with pages
286 * @return string The link.
287 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
288 */
289 public function linkWrap($linkText, $table, $pid, $addContentTable = false)
290 {
291 trigger_error('NewRecordController->linkWrap() will be replaced by protected method renderLink() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
292 return $this->renderLink($linkText, $table, $pid, $addContentTable);
293 }
294
295 /**
296 * Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row
297 *
298 * @param array $pid_row Record for parent page.
299 * @param string $checkTable Table name to check
300 * @return bool Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row
301 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
302 */
303 public function isTableAllowedForThisPage($pid_row, $checkTable)
304 {
305 trigger_error('NewRecordController->isTableAllowedForThisPage() will be replaced by protected method isTableAllowedOnPage() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
306 return $this->isTableAllowedOnPage($checkTable, $pid_row);
307 }
308
309 /**
310 * Returns TRUE if:
311 * - $allowedNewTables and $deniedNewTables are empty
312 * - the table is not found in $deniedNewTables and $allowedNewTables is not set or the $table tablename is found in
313 * $allowedNewTables
314 *
315 * If $table tablename is found in $allowedNewTables and $deniedNewTables, $deniedNewTables
316 * has priority over $allowedNewTables.
317 *
318 * @param string $table Table name to test if in allowedTables
319 * @param array $allowedNewTables Array of new tables that are allowed.
320 * @param array $deniedNewTables Array of new tables that are not allowed.
321 * @return bool Returns TRUE if a link for creating new records should be displayed for $table
322 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
323 */
324 public function showNewRecLink($table, array $allowedNewTables = [], array $deniedNewTables = [])
325 {
326 trigger_error('NewRecordController->showNewRecLink() will be replaced by protected method isRecordCreationAllowedForTable() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
327 return $this->isRecordCreationAllowedForTable($table, $allowedNewTables, $deniedNewTables);
328 }
329
330 /**
331 * Constructor function for the class
332 *
333 * @param ServerRequestInterface $request
334 */
335 protected function init(ServerRequestInterface $request): void
336 {
337 $beUser = $this->getBackendUserAuthentication();
338 // Page-selection permission clause (reading)
339 $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
340 // This will hide records from display - it has nothing to do with user rights!!
341 $pidList = $beUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? '';
342 if (!empty($pidList)) {
343 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
344 ->getQueryBuilderForTable('pages');
345 $this->perms_clause .= ' AND ' . $queryBuilder->expr()->notIn(
346 'pages.uid',
347 GeneralUtility::intExplode(',', $pidList)
348 );
349 }
350 // Setting GPvars:
351 $parsedBody = $request->getParsedBody();
352 $queryParams = $request->getQueryParams();
353 // The page id to operate from
354 $this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
355 $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
356 $this->pagesOnly = $parsedBody['pagesOnly'] ?? $queryParams['pagesOnly'] ?? null;
357 // Setting up the context sensitive menu:
358 $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
359 $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
360 $this->moduleTemplate->getPageRenderer()->loadRequireJsModule(
361 'TYPO3/CMS/Backend/Wizard/NewContentElement',
362 'function(NewContentElement) {
363 require([\'jquery\'], function($) {
364 $(function() {
365 $(\'.t3js-toggle-new-content-element-wizard\').click(function() {
366 var $me = $(this);
367 NewContentElement.wizard($me.data(\'url\'), $me.data(\'title\'));
368 });
369 });
370 });
371 }'
372 );
373 // Creating content
374 $this->content = '';
375 $this->content .= '<h1>'
376 . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.pagetitle')
377 . '</h1>';
378 // Id a positive id is supplied, ask for the page record with permission information contained:
379 if ($this->id > 0) {
380 $this->pageinfo = BackendUtility::readPageAccess($this->id, $this->perms_clause);
381 }
382 // If a page-record was returned, the user had read-access to the page.
383 if ($this->pageinfo['uid']) {
384 // Get record of parent page
385 $this->pidInfo = BackendUtility::getRecord('pages', $this->pageinfo['pid']) ?: [];
386 // Checking the permissions for the user with regard to the parent page: Can he create new pages, new
387 // content record, new page after?
388 if ($beUser->doesUserHaveAccess($this->pageinfo, 8)) {
389 $this->newPagesInto = 1;
390 }
391 if ($beUser->doesUserHaveAccess($this->pageinfo, 16)) {
392 $this->newContentInto = 1;
393 }
394 if (($beUser->isAdmin() || !empty($this->pidInfo)) && $beUser->doesUserHaveAccess($this->pidInfo, 8)) {
395 $this->newPagesAfter = 1;
396 }
397 } elseif ($beUser->isAdmin()) {
398 // Admins can do it all
399 $this->newPagesInto = 1;
400 $this->newContentInto = 1;
401 $this->newPagesAfter = 0;
402 } else {
403 // People with no permission can do nothing
404 $this->newPagesInto = 0;
405 $this->newContentInto = 0;
406 $this->newPagesAfter = 0;
407 }
408 }
409
410 /**
411 * Main processing, creating the list of new record tables to select from
412 *
413 * @param ServerRequestInterface $request
414 * @return ResponseInterface|null
415 */
416 protected function renderContent(ServerRequestInterface $request): ?ResponseInterface
417 {
418 // If there was a page - or if the user is admin (admins has access to the root) we proceed:
419 if (!empty($this->pageinfo['uid']) || $this->getBackendUserAuthentication()->isAdmin()) {
420 if (empty($this->pageinfo)) {
421 // Explicitly pass an empty array to the docHeader
422 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation([]);
423 } else {
424 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
425 }
426 // Acquiring TSconfig for this module/current page:
427 $this->web_list_modTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid'])['mod.']['web_list.'] ?? [];
428 $this->allowedNewTables = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig['allowedNewTables'] ?? '', true);
429 $this->deniedNewTables = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig['deniedNewTables'] ?? '', true);
430 // Acquiring TSconfig for this module/parent page:
431 $this->web_list_modTSconfig_pid = BackendUtility::getPagesTSconfig($this->pageinfo['pid'])['mod.']['web_list.'] ?? [];
432 $this->allowedNewTables_pid = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig_pid['allowedNewTables'] ?? '', true);
433 $this->deniedNewTables_pid = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig_pid['deniedNewTables'] ?? '', true);
434 // More init:
435 if (!$this->isRecordCreationAllowedForTable('pages')) {
436 $this->newPagesInto = 0;
437 }
438 if (!$this->isRecordCreationAllowedForTable('pages', $this->allowedNewTables_pid, $this->deniedNewTables_pid)) {
439 $this->newPagesAfter = 0;
440 }
441 // Set header-HTML and return_url
442 if (is_array($this->pageinfo) && $this->pageinfo['uid']) {
443 $title = strip_tags($this->pageinfo[$GLOBALS['TCA']['pages']['ctrl']['label']]);
444 } else {
445 $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
446 }
447 $this->moduleTemplate->setTitle($title);
448 // GENERATE the HTML-output depending on mode (pagesOnly is the page wizard)
449 // Regular new element:
450 if (!$this->pagesOnly) {
451 $this->renderNewRecordControls($request);
452 } elseif ($this->isRecordCreationAllowedForTable('pages')) {
453 // Pages only wizard
454 $response = $this->renderPositionTree();
455
456 if (!empty($response)) {
457 return $response;
458 }
459 }
460 // Add all the content to an output section
461 $this->content .= '<div>' . $this->code . '</div>';
462 // Setting up the buttons and markers for docheader
463 $this->getButtons();
464 // Build the <body> for the module
465 $this->moduleTemplate->setContent($this->content);
466 }
467
468 return null;
469 }
470
471 /**
472 * Create the panel of buttons for submitting the form or otherwise perform operations.
473 */
474 protected function getButtons(): void
475 {
476 $lang = $this->getLanguageService();
477 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
478 // Regular new element:
479 if (!$this->pagesOnly) {
480 // New page
481 if ($this->isRecordCreationAllowedForTable('pages')) {
482 $newPageButton = $buttonBar->makeLinkButton()
483 ->setHref(GeneralUtility::linkThisScript(['pagesOnly' => '1']))
484 ->setTitle($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:newPage'))
485 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL));
486 $buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
487 }
488 // CSH
489 $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('new_regular');
490 $buttonBar->addButton($cshButton);
491 } elseif ($this->isRecordCreationAllowedForTable('pages')) {
492 // Pages only wizard
493 // CSH
494 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'new_pages');
495 $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('new_pages');
496 $buttonBar->addButton($cshButton);
497 }
498 // Back
499 if ($this->returnUrl) {
500 $returnButton = $buttonBar->makeLinkButton()
501 ->setHref($this->returnUrl)
502 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
503 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
504 $buttonBar->addButton($returnButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
505 }
506
507 if (is_array($this->pageinfo) && $this->pageinfo['uid']) {
508 // View
509 $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
510 if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
511 $excludeDokTypes = GeneralUtility::intExplode(
512 ',',
513 $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
514 true
515 );
516 } else {
517 // exclude sysfolders and recycler by default
518 $excludeDokTypes = [
519 PageRepository::DOKTYPE_RECYCLER,
520 PageRepository::DOKTYPE_SYSFOLDER,
521 PageRepository::DOKTYPE_SPACER
522 ];
523 }
524 if (!in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)) {
525 $viewButton = $buttonBar->makeLinkButton()
526 ->setHref('#')
527 ->setOnClick(BackendUtility::viewOnClick(
528 $this->pageinfo['uid'],
529 '',
530 BackendUtility::BEgetRootLine($this->pageinfo['uid'])
531 ))
532 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
533 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
534 'actions-view-page',
535 Icon::SIZE_SMALL
536 ));
537 $buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 30);
538 }
539 }
540 }
541
542 /**
543 * Renders the position map for pages wizard
544 *
545 * @return ResponseInterface|null
546 */
547 protected function renderPositionTree(): ?ResponseInterface
548 {
549 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
550 ->getQueryBuilderForTable('sys_language');
551 $queryBuilder->getRestrictions()
552 ->removeAll()
553 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
554 $numberOfPages = $queryBuilder
555 ->count('*')
556 ->from('pages')
557 ->execute()
558 ->fetchColumn(0);
559
560 if ($numberOfPages > 0) {
561 $this->code .= '<h3>' . htmlspecialchars($this->getLanguageService()->getLL('selectPosition')) . ':</h3>';
562 /** @var \TYPO3\CMS\Backend\Tree\View\PagePositionMap $positionMap */
563 $positionMap = GeneralUtility::makeInstance(PagePositionMap::class, NewRecordPageTreeView::class);
564 $this->code .= $positionMap->positionTree(
565 $this->id,
566 $this->pageinfo,
567 $this->perms_clause,
568 $this->returnUrl
569 );
570 } else {
571 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
572 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
573 // No pages yet, no need to prompt for position, redirect to page creation.
574 $urlParameters = [
575 'edit' => [
576 'pages' => [
577 0 => 'new'
578 ]
579 ],
580 'returnNewPageId' => 1,
581 'returnUrl' => (string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id, 'pagesOnly' => '1'])
582 ];
583 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
584 @ob_end_clean();
585
586 return new RedirectResponse($url);
587 }
588
589 return null;
590 }
591
592 /**
593 * Render controls for creating a regular new element (pages or records)
594 *
595 * @param ServerRequestInterface $request
596 */
597 protected function renderNewRecordControls(ServerRequestInterface $request): void
598 {
599 $lang = $this->getLanguageService();
600 // Initialize array for accumulating table rows:
601 $this->tRows = [];
602 // Get TSconfig for current page
603 $pageTS = BackendUtility::getPagesTSconfig($this->id);
604 // Finish initializing new pages options with TSconfig
605 // Each new page option may be hidden by TSconfig
606 // Enabled option for the position of a new page
607 $this->newPagesSelectPosition = !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageSelectPosition']);
608 // Pseudo-boolean (0/1) for backward compatibility
609 $displayNewPagesIntoLink = $this->newPagesInto && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageInside']);
610 $displayNewPagesAfterLink = $this->newPagesAfter && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageAfter']);
611 // Slight spacer from header:
612 $this->code .= '';
613 // New Page
614 $table = 'pages';
615 $v = $GLOBALS['TCA'][$table];
616 $pageIcon = $this->moduleTemplate->getIconFactory()->getIconForRecord(
617 $table,
618 [],
619 Icon::SIZE_SMALL
620 )->render();
621 $newPageIcon = $this->moduleTemplate->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL)->render();
622 $rowContent = '';
623 // New pages INSIDE this pages
624 $newPageLinks = [];
625 if ($displayNewPagesIntoLink
626 && $this->isTableAllowedOnPage('pages', $this->pageinfo)
627 && $this->getBackendUserAuthentication()->check('tables_modify', 'pages')
628 && $this->getBackendUserAuthentication()->workspaceCreateNewRecord(($this->pageinfo['_ORIG_uid'] ?: $this->id), 'pages')
629 ) {
630 // Create link to new page inside:
631 $recordIcon = $this->moduleTemplate->getIconFactory()->getIconForRecord($table, [], Icon::SIZE_SMALL)->render();
632 $newPageLinks[] = $this->renderLink(
633 $recordIcon . htmlspecialchars($lang->sL($v['ctrl']['title'])) . ' (' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.inside')) . ')',
634 $table,
635 $this->id
636 );
637 }
638 // New pages AFTER this pages
639 if ($displayNewPagesAfterLink
640 && $this->isTableAllowedOnPage('pages', $this->pidInfo)
641 && $this->getBackendUserAuthentication()->check('tables_modify', 'pages')
642 && $this->getBackendUserAuthentication()->workspaceCreateNewRecord($this->pidInfo['uid'], 'pages')
643 ) {
644 $newPageLinks[] = $this->renderLink(
645 $pageIcon . htmlspecialchars($lang->sL($v['ctrl']['title'])) . ' (' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.after')) . ')',
646 'pages',
647 -$this->id
648 );
649 }
650 // New pages at selection position
651 if ($this->newPagesSelectPosition && $this->isRecordCreationAllowedForTable('pages')) {
652 // Link to page-wizard:
653 $newPageLinks[] = '<a href="' . htmlspecialchars(GeneralUtility::linkThisScript(['pagesOnly' => 1])) . '">' . $pageIcon . htmlspecialchars($lang->getLL('pageSelectPosition')) . '</a>';
654 }
655 // Assemble all new page links
656 $numPageLinks = count($newPageLinks);
657 for ($i = 0; $i < $numPageLinks; $i++) {
658 $rowContent .= '<li>' . $newPageLinks[$i] . '</li>';
659 }
660 if ($this->isRecordCreationAllowedForTable('pages')) {
661 $rowContent = '<ul class="list-tree"><li>' . $newPageIcon . '<strong>' .
662 $lang->getLL('createNewPage') . '</strong><ul>' . $rowContent . '</ul></li>';
663 } else {
664 $rowContent = '<ul class="list-tree"><li><ul>' . $rowContent . '</li></ul>';
665 }
666 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
667 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
668 // Compile table row
669 $startRows = [$rowContent];
670 $iconFile = [];
671 // New tables (but not pages) INSIDE this pages
672 $isAdmin = $this->getBackendUserAuthentication()->isAdmin();
673 $newContentIcon = $this->moduleTemplate->getIconFactory()->getIcon('actions-document-new', Icon::SIZE_SMALL)->render();
674 if ($this->newContentInto) {
675 if (is_array($GLOBALS['TCA'])) {
676 $groupName = '';
677 foreach ($GLOBALS['TCA'] as $table => $v) {
678 $rootLevelConfiguration = isset($v['ctrl']['rootLevel']) ? (int)$v['ctrl']['rootLevel'] : 0;
679 if ($table !== 'pages'
680 && $this->isRecordCreationAllowedForTable($table)
681 && $this->isTableAllowedOnPage($table, $this->pageinfo)
682 && $this->getBackendUserAuthentication()->check('tables_modify', $table)
683 && ($rootLevelConfiguration === -1 || ($this->id xor $rootLevelConfiguration))
684 && $this->getBackendUserAuthentication()->workspaceCreateNewRecord(($this->pageinfo['_ORIG_uid'] ? $this->pageinfo['_ORIG_uid'] : $this->id), $table)
685 ) {
686 $newRecordIcon = $this->moduleTemplate->getIconFactory()->getIconForRecord($table, [], Icon::SIZE_SMALL)->render();
687 $rowContent = '';
688 $thisTitle = '';
689 // Create new link for record:
690 $newLink = $this->renderLink(
691 $newRecordIcon . htmlspecialchars($lang->sL($v['ctrl']['title'])),
692 $table,
693 $this->id
694 );
695 // If the table is 'tt_content', add link to wizard
696 if ($table === 'tt_content') {
697 $groupName = $lang->getLL('createNewContent');
698 $rowContent = $newContentIcon
699 . '<strong>' . $lang->getLL('createNewContent') . '</strong>'
700 . '<ul>';
701 // If mod.newContentElementWizard.override is set, use that extension's wizard instead:
702 $moduleName = BackendUtility::getPagesTSconfig($this->id)['mod.']['newContentElementWizard.']['override']
703 ?? 'new_content_element_wizard';
704 /** @var \TYPO3\CMS\Core\Http\NormalizedParams */
705 $normalizedParams = $request->getAttribute('normalizedParams');
706 $url = (string)$uriBuilder->buildUriFromRoute($moduleName, ['id' => $this->id, 'returnUrl' => $normalizedParams->getRequestUri()]);
707 $rowContent .= '<li>' . $newLink . ' ' . BackendUtility::wrapInHelp($table, '') . '</li>'
708 . '<li>'
709 . '<a href="#" data-url="' . htmlspecialchars($url) . '" data-title="' . htmlspecialchars($this->getLanguageService()->getLL('newContentElement')) . '" class="t3js-toggle-new-content-element-wizard">'
710 . $newContentIcon . htmlspecialchars($lang->getLL('clickForWizard'))
711 . '</a>'
712 . '</li>'
713 . '</ul>';
714 } else {
715 // Get the title
716 if ($v['ctrl']['readOnly'] || $v['ctrl']['hideTable'] || $v['ctrl']['is_static']) {
717 continue;
718 }
719 if ($v['ctrl']['adminOnly'] && !$isAdmin) {
720 continue;
721 }
722 $nameParts = explode('_', $table);
723 $thisTitle = '';
724 $_EXTKEY = '';
725 if ($nameParts[0] === 'tx' || $nameParts[0] === 'tt') {
726 // Try to extract extension name
727 if (strpos($v['ctrl']['title'], 'LLL:EXT:') === 0) {
728 $_EXTKEY = substr($v['ctrl']['title'], 8);
729 $_EXTKEY = substr($_EXTKEY, 0, strpos($_EXTKEY, '/'));
730 if ($_EXTKEY !== '') {
731 // First try to get localisation of extension title
732 $temp = explode(':', substr($v['ctrl']['title'], 9 + strlen($_EXTKEY)));
733 $langFile = $temp[0];
734 $thisTitle = $lang->sL('LLL:EXT:' . $_EXTKEY . '/' . $langFile . ':extension.title');
735 // If no localisation available, read title from ext_emconf.php
736 $extPath = ExtensionManagementUtility::extPath($_EXTKEY);
737 $extEmConfFile = $extPath . 'ext_emconf.php';
738 if (!$thisTitle && is_file($extEmConfFile)) {
739 $EM_CONF = [];
740 include $extEmConfFile;
741 $thisTitle = $EM_CONF[$_EXTKEY]['title'];
742 }
743 $iconFile[$_EXTKEY] = '<img src="' . PathUtility::getAbsoluteWebPath(ExtensionManagementUtility::getExtensionIcon($extPath, true)) . '" ' . 'width="16" height="16" ' . 'alt="' . $thisTitle . '" />';
744 }
745 }
746 if (empty($thisTitle)) {
747 $_EXTKEY = $nameParts[1];
748 $thisTitle = $nameParts[1];
749 $iconFile[$_EXTKEY] = '';
750 }
751 } else {
752 $_EXTKEY = 'system';
753 $thisTitle = $lang->getLL('system_records');
754 $iconFile['system'] = $this->moduleTemplate->getIconFactory()->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render();
755 }
756
757 if ($groupName === '' || $groupName !== $_EXTKEY) {
758 $groupName = empty($v['ctrl']['groupName']) ? $_EXTKEY : $v['ctrl']['groupName'];
759 }
760 $rowContent .= $newLink;
761 }
762 // Compile table row:
763 if ($table === 'tt_content') {
764 $startRows[] = '<li>' . $rowContent . '</li>';
765 } else {
766 $this->tRows[$groupName]['title'] = $thisTitle;
767 $this->tRows[$groupName]['html'][] = $rowContent;
768 $this->tRows[$groupName]['table'][] = $table;
769 }
770 }
771 }
772 }
773 }
774 // User sort
775 if (isset($pageTS['mod.']['wizards.']['newRecord.']['order'])) {
776 $this->newRecordSortList = GeneralUtility::trimExplode(',', $pageTS['mod.']['wizards.']['newRecord.']['order'], true);
777 }
778 uksort($this->tRows, [$this, 'sortTableRows']);
779 // Compile table row:
780 $finalRows = [];
781 $finalRows[] = implode('', $startRows);
782 foreach ($this->tRows as $key => $value) {
783 $row = '<li>' . $iconFile[$key] . ' <strong>' . $value['title'] . '</strong><ul>';
784 foreach ($value['html'] as $recordKey => $record) {
785 $row .= '<li>' . $record . ' ' . BackendUtility::wrapInHelp($value['table'][$recordKey], '') . '</li>';
786 }
787 $row .= '</ul></li>';
788 $finalRows[] = $row;
789 }
790
791 $finalRows[] = '</ul>';
792 // Make table:
793 $this->code .= implode('', $finalRows);
794 }
795
796 /**
797 * User array sort function used by renderNewRecordControls
798 *
799 * @param string $a First array element for compare
800 * @param string $b First array element for compare
801 * @return int -1 for lower, 0 for equal, 1 for greater
802 */
803 protected function sortTableRows(string $a, string $b): int
804 {
805 if (!empty($this->newRecordSortList)) {
806 if (in_array($a, $this->newRecordSortList) && in_array($b, $this->newRecordSortList)) {
807 // Both are in the list, return relative to position in array
808 $sub = array_search($a, $this->newRecordSortList) - array_search($b, $this->newRecordSortList);
809 $ret = ($sub < 0 ? -1 : $sub == 0) ? 0 : 1;
810 } elseif (in_array($a, $this->newRecordSortList)) {
811 // First element is in array, put to top
812 $ret = -1;
813 } elseif (in_array($b, $this->newRecordSortList)) {
814 // Second element is in array, put first to bottom
815 $ret = 1;
816 } else {
817 // No element is in array, return alphabetic order
818 $ret = strnatcasecmp($this->tRows[$a]['title'], $this->tRows[$b]['title']);
819 }
820 return $ret;
821 }
822 // Return alphabetic order
823 return strnatcasecmp($this->tRows[$a]['title'], $this->tRows[$b]['title']);
824 }
825
826 /**
827 * Links the string $code to a create-new form for a record in $table created on page $pid
828 *
829 * @param string $linkText Link text
830 * @param string $table Table name (in which to create new record)
831 * @param int $pid PID value for the "&edit['.$table.']['.$pid.']=new" command (positive/negative)
832 * @param bool $addContentTable If $addContentTable is set, then a new tt_content record is created together with pages
833 * @return string The link.
834 */
835 protected function renderLink(string $linkText, string $table, int $pid, bool $addContentTable = false): string
836 {
837 $urlParameters = [
838 'edit' => [
839 $table => [
840 $pid => 'new'
841 ]
842 ],
843 'returnUrl' => $this->returnUrl
844 ];
845
846 if ($table === 'pages' && $addContentTable) {
847 $urlParameters['tt_content']['prev'] = 'new';
848 $urlParameters['returnNewPageId'] = 1;
849 }
850
851 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
852 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
853
854 return '<a href="' . htmlspecialchars($url) . '">' . $linkText . '</a>';
855 }
856
857 /**
858 * Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row
859 *
860 * @param string $table Table name to check
861 * @param array $page Potential parent page
862 * @return bool Returns TRUE if the tablename $table is allowed to be created on the $page
863 */
864 protected function isTableAllowedOnPage(string $table, array $page): bool
865 {
866 if (empty($page)) {
867 return $this->getBackendUserAuthentication()->isAdmin();
868 }
869 // be_users and be_groups may not be created anywhere but in the root.
870 if ($table === 'be_users' || $table === 'be_groups') {
871 return false;
872 }
873 // Checking doktype:
874 $doktype = (int)$page['doktype'];
875 if (!($allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'])) {
876 $allowedTableList = $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
877 }
878 // If all tables or the table is listed as an allowed type, return TRUE
879 if (strstr($allowedTableList, '*') || GeneralUtility::inList($allowedTableList, $table)) {
880 return true;
881 }
882
883 return false;
884 }
885
886 /**
887 * Returns whether the record link should be shown for a table
888 *
889 * Returns TRUE if:
890 * - $allowedNewTables and $deniedNewTables are empty
891 * - the table is not found in $deniedNewTables and $allowedNewTables is not set or the $table tablename is found in
892 * $allowedNewTables
893 *
894 * If $table tablename is found in $allowedNewTables and $deniedNewTables,
895 * $deniedNewTables has priority over $allowedNewTables.
896 *
897 * @param string $table Table name to test if in allowedTables
898 * @param array $allowedNewTables Array of new tables that are allowed.
899 * @param array $deniedNewTables Array of new tables that are not allowed.
900 * @return bool Returns TRUE if a link for creating new records should be displayed for $table
901 */
902 protected function isRecordCreationAllowedForTable(string $table, array $allowedNewTables = [], array $deniedNewTables = []): bool
903 {
904 if (!$this->getBackendUserAuthentication()->check('tables_modify', $table)) {
905 return false;
906 }
907
908 $allowedNewTables = $allowedNewTables ?: $this->allowedNewTables;
909 $deniedNewTables = $deniedNewTables ?: $this->deniedNewTables;
910 // No deny/allow tables are set:
911 if (empty($allowedNewTables) && empty($deniedNewTables)) {
912 return true;
913 }
914
915 return !in_array($table, $deniedNewTables) && (empty($allowedNewTables) || in_array($table, $allowedNewTables));
916 }
917
918 /**
919 * Checks if sys_language records are present
920 *
921 * @return bool
922 */
923 protected function checkIfLanguagesExist(): bool
924 {
925 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
926 ->getQueryBuilderForTable('sys_language');
927 $queryBuilder->getRestrictions()->removeAll();
928
929 $count = $queryBuilder
930 ->count('uid')
931 ->from('sys_language')
932 ->execute()
933 ->fetchColumn(0);
934 return (bool)$count;
935 }
936
937 /**
938 * @return LanguageService
939 */
940 protected function getLanguageService(): LanguageService
941 {
942 return $GLOBALS['LANG'];
943 }
944
945 /**
946 * @return BackendUserAuthentication
947 */
948 protected function getBackendUserAuthentication(): BackendUserAuthentication
949 {
950 return $GLOBALS['BE_USER'];
951 }
952 }