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;
19
20use Psr\EventDispatcher\EventDispatcherInterface;
21use Psr\Http\Message\ResponseInterface;
22use Psr\Http\Message\ServerRequestInterface;
23use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
24use TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent;
25use TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent;
26use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
27use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
28use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordWorkspaceDeletePlaceholderException;
29use TYPO3\CMS\Backend\Form\FormDataCompiler;
30use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
31use TYPO3\CMS\Backend\Form\FormResultCompiler;
32use TYPO3\CMS\Backend\Form\NodeFactory;
33use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
34use TYPO3\CMS\Backend\Routing\UriBuilder;
35use TYPO3\CMS\Backend\Template\Components\ButtonBar;
36use TYPO3\CMS\Backend\Template\ModuleTemplate;
37use TYPO3\CMS\Backend\Utility\BackendUtility;
38use TYPO3\CMS\Core\Database\ConnectionPool;
39use TYPO3\CMS\Core\Database\Query\QueryBuilder;
40use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
41use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
42use TYPO3\CMS\Core\Database\ReferenceIndex;
43use TYPO3\CMS\Core\DataHandling\DataHandler;
44use TYPO3\CMS\Core\Domain\Repository\PageRepository;
45use TYPO3\CMS\Core\Http\HtmlResponse;
46use TYPO3\CMS\Core\Http\RedirectResponse;
47use TYPO3\CMS\Core\Imaging\Icon;
48use TYPO3\CMS\Core\Messaging\FlashMessage;
49use TYPO3\CMS\Core\Messaging\FlashMessageService;
50use TYPO3\CMS\Core\Page\PageRenderer;
51use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
52use TYPO3\CMS\Core\Type\Bitmask\Permission;
53use TYPO3\CMS\Core\Utility\GeneralUtility;
54use TYPO3\CMS\Core\Utility\HttpUtility;
55use TYPO3\CMS\Core\Utility\MathUtility;
56use TYPO3\CMS\Core\Utility\PathUtility;
57use TYPO3\CMS\Core\Versioning\VersionState;
58
59/**
60 * Main backend controller almost always used if some database record is edited in the backend.
61 *
62 * Main job of this controller is to evaluate and sanitize $request parameters,
63 * call the DataHandler if records should be created or updated and
64 * execute FormEngine for record rendering.
65 */
66class EditDocumentController
67{
68    protected const DOCUMENT_CLOSE_MODE_DEFAULT = 0;
69    // works like DOCUMENT_CLOSE_MODE_DEFAULT
70    protected const DOCUMENT_CLOSE_MODE_REDIRECT = 1;
71    protected const DOCUMENT_CLOSE_MODE_CLEAR_ALL = 3;
72    protected const DOCUMENT_CLOSE_MODE_NO_REDIRECT = 4;
73
74    /**
75     * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
76     *
77     * @var array<string,array>
78     */
79    protected $editconf = [];
80
81    /**
82     * Comma list of field names to edit. If specified, only those fields will be rendered.
83     * Otherwise all (available) fields in the record are shown according to the TCA type.
84     *
85     * @var string|null
86     */
87    protected $columnsOnly;
88
89    /**
90     * Default values for fields
91     *
92     * @var array|null [table][field]
93     */
94    protected $defVals;
95
96    /**
97     * Array of values to force being set as hidden fields in FormEngine
98     *
99     * @var array|null [table][field]
100     */
101    protected $overrideVals;
102
103    /**
104     * If set, this value will be set in $this->retUrl as "returnUrl", if not,
105     * $this->retUrl will link to dummy controller
106     *
107     * @var string|null
108     */
109    protected $returnUrl;
110
111    /**
112     * Prepared return URL. Contains the URL that we should return to from FormEngine if
113     * close button is clicked. Usually passed along as 'returnUrl', but falls back to
114     * "dummy" controller.
115     *
116     * @var string
117     */
118    protected $retUrl;
119
120    /**
121     * Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
122     *
123     * @var int
124     */
125    protected $closeDoc;
126
127    /**
128     * If true, the processing of incoming data will be performed as if a save-button is pressed.
129     * Used in the forms as a hidden field which can be set through
130     * JavaScript if the form is somehow submitted by JavaScript.
131     *
132     * @var bool
133     */
134    protected $doSave;
135
136    /**
137     * Main DataHandler datamap array
138     *
139     * @var array
140     * @todo: Will be set protected later, still used by ConditionMatcher
141     * @internal Will be removed / protected in TYPO3 v10.x without further notice
142     */
143    public $data;
144
145    /**
146     * Main DataHandler cmdmap array
147     *
148     * @var array
149     */
150    protected $cmd;
151
152    /**
153     * DataHandler 'mirror' input
154     *
155     * @var array
156     */
157    protected $mirror;
158
159    /**
160     * Boolean: If set, then the GET var "&id=" will be added to the
161     * retUrl string so that the NEW id of something is returned to the script calling the form.
162     *
163     * @var bool
164     */
165    protected $returnNewPageId = false;
166
167    /**
168     * Updated values for backendUser->uc. Used for new inline records to mark them
169     * as expanded: uc[inlineView][...]
170     *
171     * @var array|null
172     */
173    protected $uc;
174
175    /**
176     * ID for displaying the page in the frontend, "save and view"
177     *
178     * @var int
179     */
180    protected $popViewId;
181
182    /**
183     * Alternative URL for viewing the frontend pages.
184     *
185     * @var string
186     */
187    protected $viewUrl;
188
189    /**
190     * Alternative title for the document handler.
191     *
192     * @var string
193     */
194    protected $recTitle;
195
196    /**
197     * If set, then no save & view button is printed
198     *
199     * @var bool
200     */
201    protected $noView;
202
203    /**
204     * @var string
205     */
206    protected $perms_clause;
207
208    /**
209     * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
210     *
211     * @var bool
212     */
213    protected $returnEditConf;
214
215    /**
216     * parse_url() of current requested URI, contains ['path'] and ['query'] parts.
217     *
218     * @var array
219     */
220    protected $R_URL_parts;
221
222    /**
223     * Contains $request query parameters. This array is the foundation for creating
224     * the R_URI internal var which becomes the url to which forms are submitted
225     *
226     * @var array
227     */
228    protected $R_URL_getvars;
229
230    /**
231     * Set to the URL of this script including variables which is needed to re-display the form.
232     *
233     * @var string
234     */
235    protected $R_URI;
236
237    /**
238     * @var array
239     */
240    protected $pageinfo;
241
242    /**
243     * Is loaded with the "title" of the currently "open document"
244     * used for the open document toolbar
245     *
246     * @var string
247     */
248    protected $storeTitle = '';
249
250    /**
251     * Contains an array with key/value pairs of GET parameters needed to reach the
252     * current document displayed - used in the 'open documents' toolbar.
253     *
254     * @var array
255     */
256    protected $storeArray;
257
258    /**
259     * $this->storeArray imploded to url
260     *
261     * @var string
262     */
263    protected $storeUrl;
264
265    /**
266     * md5 hash of storeURL, used to identify a single open document in backend user uc
267     *
268     * @var string
269     */
270    protected $storeUrlMd5;
271
272    /**
273     * Backend user session data of this module
274     *
275     * @var array
276     */
277    protected $docDat;
278
279    /**
280     * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
281     * the various documents on the GET parameter list needed to open it. The values are
282     * arrays with 0,1,2 keys with information about the document (see compileStoreData()).
283     * The docHandler variable is stored in the $docDat session data, key "0".
284     *
285     * @var array
286     */
287    protected $docHandler;
288
289    /**
290     * Array of the elements to create edit forms for.
291     *
292     * @var array
293     * @todo: Will be set protected later, still used by ConditionMatcher
294     * @internal Will be removed / protected in TYPO3 v10.x without further notice
295     */
296    public $elementsData;
297
298    /**
299     * Pointer to the first element in $elementsData
300     *
301     * @var array
302     */
303    protected $firstEl;
304
305    /**
306     * Counter, used to count the number of errors (when users do not have edit permissions)
307     *
308     * @var int
309     */
310    protected $errorC;
311
312    /**
313     * Counter, used to count the number of new record forms displayed
314     *
315     * @var int
316     */
317    protected $newC;
318
319    /**
320     * Is set to the pid value of the last shown record - thus indicating which page to
321     * show when clicking the SAVE/VIEW button
322     *
323     * @var int
324     */
325    protected $viewId;
326
327    /**
328     * Is set to additional parameters (like "&L=xxx") if the record supports it.
329     *
330     * @var string
331     */
332    protected $viewId_addParams;
333
334    /**
335     * @var FormResultCompiler
336     */
337    protected $formResultCompiler;
338
339    /**
340     * Used internally to disable the storage of the document reference (eg. new records)
341     *
342     * @var int
343     */
344    protected $dontStoreDocumentRef = 0;
345
346    /**
347     * Stores information needed to preview the currently saved record
348     *
349     * @var array
350     */
351    protected $previewData = [];
352
353    /**
354     * ModuleTemplate object
355     *
356     * @var ModuleTemplate
357     */
358    protected $moduleTemplate;
359
360    /**
361     * Check if a record has been saved
362     *
363     * @var bool
364     */
365    protected $isSavedRecord;
366
367    /**
368     * Check if a page in free translation mode
369     *
370     * @var bool
371     */
372    protected $isPageInFreeTranslationMode = false;
373
374    /**
375     * @var EventDispatcherInterface
376     */
377    protected $eventDispatcher;
378
379    /**
380     * @var UriBuilder
381     */
382    protected $uriBuilder;
383
384    public function __construct(EventDispatcherInterface $eventDispatcher)
385    {
386        $this->eventDispatcher = $eventDispatcher;
387        $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
388        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
389        $this->moduleTemplate->setUiBlock(true);
390        // @todo Used by TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching
391        $GLOBALS['SOBE'] = $this;
392        $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
393    }
394
395    /**
396     * Main dispatcher entry method registered as "record_edit" end point
397     *
398     * @param ServerRequestInterface $request the current request
399     * @return ResponseInterface the response with the content
400     */
401    public function mainAction(ServerRequestInterface $request): ResponseInterface
402    {
403        // Unlock all locked records
404        BackendUtility::lockRecords();
405        if ($response = $this->preInit($request)) {
406            return $response;
407        }
408
409        // Process incoming data via DataHandler?
410        $parsedBody = $request->getParsedBody();
411        if ($this->doSave
412            || isset($parsedBody['_savedok'])
413            || isset($parsedBody['_saveandclosedok'])
414            || isset($parsedBody['_savedokview'])
415            || isset($parsedBody['_savedoknew'])
416            || isset($parsedBody['_duplicatedoc'])
417        ) {
418            if ($response = $this->processData($request)) {
419                return $response;
420            }
421        }
422
423        $this->init($request);
424        $this->main($request);
425
426        return new HtmlResponse($this->moduleTemplate->renderContent());
427    }
428
429    /**
430     * First initialization, always called, even before processData() executes DataHandler processing.
431     *
432     * @param ServerRequestInterface $request
433     * @return ResponseInterface Possible redirect response
434     */
435    protected function preInit(ServerRequestInterface $request): ?ResponseInterface
436    {
437        if ($response = $this->localizationRedirect($request)) {
438            return $response;
439        }
440
441        $parsedBody = $request->getParsedBody();
442        $queryParams = $request->getQueryParams();
443
444        $this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
445        $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
446        $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
447        $this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
448        $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null);
449        $this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
450        $this->doSave = (bool)($parsedBody['doSave'] ?? $queryParams['doSave'] ?? false);
451        $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
452        $this->uc = $parsedBody['uc'] ?? $queryParams['uc'] ?? null;
453
454        // Set overrideVals as default values if defVals does not exist.
455        // @todo: Why?
456        if (!is_array($this->defVals) && is_array($this->overrideVals)) {
457            $this->defVals = $this->overrideVals;
458        }
459        $this->addSlugFieldsToColumnsOnly($queryParams);
460
461        // Set final return URL
462        $this->retUrl = $this->returnUrl ?: (string)$this->uriBuilder->buildUriFromRoute('dummy');
463
464        // Change $this->editconf if versioning applies to any of the records
465        $this->fixWSversioningInEditConf();
466
467        // Prepare R_URL (request url)
468        $this->R_URL_parts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
469        $this->R_URL_getvars = $queryParams;
470        $this->R_URL_getvars['edit'] = $this->editconf;
471
472        // Prepare 'open documents' url, this is later modified again various times
473        $this->compileStoreData();
474        // Backend user session data of this module
475        $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
476        $this->docHandler = $this->docDat[0];
477
478        // Close document if a request for closing the document has been sent
479        if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
480            if ($response = $this->closeDocument($this->closeDoc, $request)) {
481                return $response;
482            }
483        }
484
485        $event = new BeforeFormEnginePageInitializedEvent($this, $request);
486        $this->eventDispatcher->dispatch($event);
487        return null;
488    }
489
490    /**
491     * Always add required fields of slug field
492     *
493     * @param array $queryParams
494     */
495    protected function addSlugFieldsToColumnsOnly(array $queryParams): void
496    {
497        $data = $queryParams['edit'] ?? [];
498        $data = array_keys($data);
499        $table = reset($data);
500        if ($this->columnsOnly && $table !== false && isset($GLOBALS['TCA'][$table])) {
501            $fields = GeneralUtility::trimExplode(',', $this->columnsOnly, true);
502            foreach ($fields as $field) {
503                $postModifiers = $GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['postModifiers'] ?? [];
504                if (isset($GLOBALS['TCA'][$table]['columns'][$field])
505                    && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'slug'
506                    && (!is_array($postModifiers) || $postModifiers === [])
507                ) {
508                    foreach ($GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['fields'] ?? [] as $fields) {
509                        $this->columnsOnly .= ',' . (is_array($fields) ? implode(',', $fields) : $fields);
510                    }
511                }
512            }
513        }
514    }
515
516    /**
517     * Do processing of data, submitting it to DataHandler. May return a RedirectResponse
518     *
519     * @param ServerRequestInterface $request
520     * @return ResponseInterface|null
521     */
522    protected function processData(ServerRequestInterface $request): ?ResponseInterface
523    {
524        $parsedBody = $request->getParsedBody();
525        $queryParams = $request->getQueryParams();
526
527        $beUser = $this->getBackendUser();
528
529        // Processing related GET / POST vars
530        $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];
531        $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];
532        $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];
533        $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);
534
535        // Only options related to $this->data submission are included here
536        $tce = GeneralUtility::makeInstance(DataHandler::class);
537
538        $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);
539
540        // Set internal vars
541        if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {
542            $tce->neverHideAtCopy = 1;
543        }
544
545        // Set default values fetched previously from GET / POST vars
546        if (is_array($this->defVals) && $this->defVals !== [] && is_array($tce->defaultValues)) {
547            $tce->defaultValues = array_merge_recursive($this->defVals, $tce->defaultValues);
548        }
549
550        // Load DataHandler with data
551        $tce->start($this->data, $this->cmd);
552        if (is_array($this->mirror)) {
553            $tce->setMirror($this->mirror);
554        }
555
556        // Perform the saving operation with DataHandler:
557        if ($this->doSave === true) {
558            $tce->process_datamap();
559            $tce->process_cmdmap();
560        }
561        // If pages are being edited, we set an instruction about updating the page tree after this operation.
562        if ($tce->pagetreeNeedsRefresh
563            && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
564        ) {
565            BackendUtility::setUpdateSignal('updatePageTree');
566        }
567        // If there was saved any new items, load them:
568        if (!empty($tce->substNEWwithIDs_table)) {
569            // Save the expanded/collapsed states for new inline records, if any
570            FormEngineUtility::updateInlineView($this->uc, $tce);
571            $newEditConf = [];
572            foreach ($this->editconf as $tableName => $tableCmds) {
573                $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
574                if (!empty($keys)) {
575                    foreach ($keys as $key) {
576                        $editId = $tce->substNEWwithIDs[$key];
577                        // Check if the $editId isn't a child record of an IRRE action
578                        if (!(is_array($tce->newRelatedIDs[$tableName])
579                            && in_array($editId, $tce->newRelatedIDs[$tableName]))
580                        ) {
581                            // Translate new id to the workspace version
582                            if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
583                                $beUser->workspace,
584                                $tableName,
585                                $editId,
586                                'uid'
587                            )) {
588                                $editId = $versionRec['uid'];
589                            }
590                            $newEditConf[$tableName][$editId] = 'edit';
591                        }
592                        // Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
593                        if ($tableName === 'pages'
594                            && $this->retUrl !== (string)$this->uriBuilder->buildUriFromRoute('dummy')
595                            && $this->retUrl !== $this->getCloseUrl()
596                            && $this->returnNewPageId
597                        ) {
598                            $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
599                        }
600                    }
601                } else {
602                    $newEditConf[$tableName] = $tableCmds;
603                }
604            }
605            // Reset editconf if newEditConf has values
606            if (!empty($newEditConf)) {
607                $this->editconf = $newEditConf;
608            }
609            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
610            $this->R_URL_getvars['edit'] = $this->editconf;
611            // Unset default values since we don't need them anymore.
612            unset($this->R_URL_getvars['defVals']);
613            // Recompile the store* values since editconf changed
614            $this->compileStoreData();
615        }
616        // See if any records was auto-created as new versions?
617        if (!empty($tce->autoVersionIdMap)) {
618            $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
619        }
620        // If a document is saved and a new one is created right after.
621        if (isset($parsedBody['_savedoknew']) && is_array($this->editconf)) {
622            if ($redirect = $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request)) {
623                return $redirect;
624            }
625            // Find the current table
626            reset($this->editconf);
627            $nTable = (string)key($this->editconf);
628            // Finding the first id, getting the records pid+uid
629            reset($this->editconf[$nTable]);
630            $nUid = (int)key($this->editconf[$nTable]);
631            $recordFields = 'pid,uid';
632            if (BackendUtility::isTableWorkspaceEnabled($nTable)) {
633                $recordFields .= ',t3ver_oid';
634            }
635            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
636            // Determine insertion mode: 'top' is self-explaining,
637            // otherwise new elements are inserted after one using a negative uid
638            $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
639            // Setting a blank editconf array for a new record:
640            $this->editconf = [];
641            // Determine related page ID for regular live context
642            if ((int)($nRec['t3ver_oid'] ?? 0) === 0) {
643                if ($insertRecordOnTop) {
644                    $relatedPageId = $nRec['pid'];
645                } else {
646                    $relatedPageId = -$nRec['uid'];
647                }
648            } else {
649                // Determine related page ID for workspace context
650                if ($insertRecordOnTop) {
651                    // Fetch live version of workspace version since the pid value is always -1 in workspaces
652                    $liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
653                    $relatedPageId = $liveRecord['pid'];
654                } else {
655                    // Use uid of live version of workspace version
656                    $relatedPageId = -$nRec['t3ver_oid'];
657                }
658            }
659            $this->editconf[$nTable][$relatedPageId] = 'new';
660            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
661            $this->R_URL_getvars['edit'] = $this->editconf;
662            // Recompile the store* values since editconf changed...
663            $this->compileStoreData();
664        }
665        // If a document should be duplicated.
666        if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf)) {
667            $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request);
668            // Find current table
669            reset($this->editconf);
670            $nTable = (string)key($this->editconf);
671            // Find the first id, getting the records pid+uid
672            reset($this->editconf[$nTable]);
673            $nUid = key($this->editconf[$nTable]);
674            if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
675                $nUid = $tce->substNEWwithIDs[$nUid];
676            }
677
678            $recordFields = 'pid,uid';
679            if (BackendUtility::isTableWorkspaceEnabled($nTable)) {
680                $recordFields .= ',t3ver_oid';
681            }
682            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
683
684            // Setting a blank editconf array for a new record:
685            $this->editconf = [];
686
687            if ((int)($nRec['t3ver_oid'] ?? 0) === 0) {
688                $relatedPageId = -$nRec['uid'];
689            } else {
690                $relatedPageId = -$nRec['t3ver_oid'];
691            }
692
693            /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $duplicateTce */
694            $duplicateTce = GeneralUtility::makeInstance(DataHandler::class);
695
696            $duplicateCmd = [
697                $nTable => [
698                    $nUid => [
699                        'copy' => $relatedPageId
700                    ]
701                ]
702            ];
703
704            $duplicateTce->start([], $duplicateCmd);
705            $duplicateTce->process_cmdmap();
706
707            $duplicateMappingArray = $duplicateTce->copyMappingArray;
708            $duplicateUid = $duplicateMappingArray[$nTable][$nUid];
709
710            if ($nTable === 'pages') {
711                BackendUtility::setUpdateSignal('updatePageTree');
712            }
713
714            $this->editconf[$nTable][$duplicateUid] = 'edit';
715            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
716            $this->R_URL_getvars['edit'] = $this->editconf;
717            // Recompile the store* values since editconf changed...
718            $this->compileStoreData();
719
720            // Inform the user of the duplication
721            $flashMessage = GeneralUtility::makeInstance(
722                FlashMessage::class,
723                $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
724                '',
725                FlashMessage::OK
726            );
727            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
728            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
729            $defaultFlashMessageQueue->enqueue($flashMessage);
730        }
731        // If a preview is requested
732        if (isset($parsedBody['_savedokview'])) {
733            $array_keys = array_keys($this->data);
734            // Get the first table and id of the data array from DataHandler
735            $table = reset($array_keys);
736            $array_keys = array_keys($this->data[$table]);
737            $id = reset($array_keys);
738            if (!MathUtility::canBeInterpretedAsInteger($id)) {
739                $id = $tce->substNEWwithIDs[$id];
740            }
741            // Store this information for later use
742            $this->previewData['table'] = $table;
743            $this->previewData['id'] = $id;
744        }
745        $tce->printLogErrorMessages();
746
747        if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
748            || isset($parsedBody['_saveandclosedok'])
749        ) {
750            // Redirect if element should be closed after save
751            return $this->closeDocument((int)abs($this->closeDoc), $request);
752        }
753        return null;
754    }
755
756    /**
757     * Initialize the view part of the controller logic.
758     *
759     * @param ServerRequestInterface $request
760     */
761    protected function init(ServerRequestInterface $request): void
762    {
763        $parsedBody = $request->getParsedBody();
764        $queryParams = $request->getQueryParams();
765
766        $beUser = $this->getBackendUser();
767
768        $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);
769        $this->viewUrl = (string)($parsedBody['viewUrl'] ?? $queryParams['viewUrl'] ?? '');
770        $this->recTitle = (string)($parsedBody['recTitle'] ?? $queryParams['recTitle'] ?? '');
771        $this->noView = (bool)($parsedBody['noView'] ?? $queryParams['noView'] ?? false);
772        $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
773        // Set other internal variables:
774        $this->R_URL_getvars['returnUrl'] = $this->retUrl;
775        $this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
776
777        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
778        $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
779
780        $this->moduleTemplate->addJavaScriptCode(
781            'previewCode',
782            (isset($parsedBody['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '')
783        );
784        // Set context sensitive menu
785        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
786
787        $event = new AfterFormEnginePageInitializedEvent($this, $request);
788        $this->eventDispatcher->dispatch($event);
789    }
790
791    /**
792     * Generate the Javascript for opening the preview window
793     *
794     * @return string
795     */
796    protected function generatePreviewCode(): string
797    {
798        $previewPageId = $this->getPreviewPageId();
799        $previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
800        $anchorSection = $this->getPreviewUrlAnchorSection();
801
802        try {
803            $previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
804            return '
805            if (window.opener) {
806                '
807                . BackendUtility::viewOnClick(
808                    $previewPageId,
809                    '',
810                    $previewPageRootLine,
811                    $anchorSection,
812                    $this->viewUrl,
813                    $previewUrlParameters,
814                    false
815                )
816                . '
817            } else {
818            '
819                . BackendUtility::viewOnClick(
820                    $previewPageId,
821                    '',
822                    $previewPageRootLine,
823                    $anchorSection,
824                    $this->viewUrl,
825                    $previewUrlParameters
826                )
827                . '
828            }';
829        } catch (UnableToLinkToPageException $e) {
830            return '';
831        }
832    }
833
834    /**
835     * Returns the parameters for the preview URL
836     *
837     * @param int $previewPageId
838     * @return string
839     */
840    protected function getPreviewUrlParameters(int $previewPageId): string
841    {
842        $linkParameters = [];
843        $table = $this->previewData['table'] ?: $this->firstEl['table'];
844        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
845        $previewConfiguration = BackendUtility::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
846        $recordArray = BackendUtility::getRecord($table, $recordId);
847
848        // language handling
849        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
850        if ($languageField && !empty($recordArray[$languageField])) {
851            $recordId = $this->resolvePreviewRecordId($table, $recordArray, $previewConfiguration);
852            $language = $recordArray[$languageField];
853            if ($language > 0) {
854                $linkParameters['L'] = $language;
855            }
856        }
857
858        // Always use live workspace record uid for the preview
859        if (BackendUtility::isTableWorkspaceEnabled($table) && ($recordArray['t3ver_oid'] ?? 0) > 0) {
860            $recordId = $recordArray['t3ver_oid'];
861        }
862
863        // map record data to GET parameters
864        if (isset($previewConfiguration['fieldToParameterMap.'])) {
865            foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
866                $value = $recordArray[$field];
867                if ($field === 'uid') {
868                    $value = $recordId;
869                }
870                $linkParameters[$parameterName] = $value;
871            }
872        }
873
874        // add/override parameters by configuration
875        if (isset($previewConfiguration['additionalGetParameters.'])) {
876            $additionalGetParameters = [];
877            $this->parseAdditionalGetParameters(
878                $additionalGetParameters,
879                $previewConfiguration['additionalGetParameters.']
880            );
881            $linkParameters = array_replace($linkParameters, $additionalGetParameters);
882        }
883
884        return HttpUtility::buildQueryString($linkParameters, '&');
885    }
886
887    /**
888     * @param string $table
889     * @param array $recordArray
890     * @param array $previewConfiguration
891     *
892     * @return int
893     */
894    protected function resolvePreviewRecordId(string $table, array $recordArray, array $previewConfiguration): int
895    {
896        $l10nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
897        if ($l10nPointer
898            && !empty($recordArray[$l10nPointer])
899            && (
900                // not set -> default to true
901                !isset($previewConfiguration['useDefaultLanguageRecord'])
902                // or set -> use value
903                || $previewConfiguration['useDefaultLanguageRecord']
904            )
905        ) {
906            return (int)$recordArray[$l10nPointer];
907        }
908        return (int)$recordArray['uid'];
909    }
910
911    /**
912     * Returns the anchor section for the preview url
913     *
914     * @return string
915     */
916    protected function getPreviewUrlAnchorSection(): string
917    {
918        $table = $this->previewData['table'] ?: $this->firstEl['table'];
919        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
920
921        return $table === 'tt_content' ? '#c' . (int)$recordId : '';
922    }
923
924    /**
925     * Returns the preview page id
926     *
927     * @return int
928     */
929    protected function getPreviewPageId(): int
930    {
931        $previewPageId = 0;
932        $table = $this->previewData['table'] ?: $this->firstEl['table'];
933        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
934        $pageId = $this->popViewId ?: $this->viewId;
935
936        if ($table === 'pages') {
937            $currentPageId = (int)$recordId;
938        } else {
939            $currentPageId = MathUtility::convertToPositiveInteger($pageId);
940        }
941
942        $previewConfiguration = BackendUtility::getPagesTSconfig($currentPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
943
944        if (isset($previewConfiguration['previewPageId'])) {
945            $previewPageId = (int)$previewConfiguration['previewPageId'];
946        }
947        // if no preview page was configured
948        if (!$previewPageId) {
949            $rootPageData = null;
950            $rootLine = BackendUtility::BEgetRootLine($currentPageId);
951            $currentPage = (array)(reset($rootLine) ?: []);
952            if ($this->canViewDoktype($currentPage)) {
953                // try the current page
954                $previewPageId = $currentPageId;
955            } else {
956                // or search for the root page
957                foreach ($rootLine as $page) {
958                    if ($page['is_siteroot']) {
959                        $rootPageData = $page;
960                        break;
961                    }
962                }
963                $previewPageId = isset($rootPageData)
964                    ? (int)$rootPageData['uid']
965                    : $currentPageId;
966            }
967        }
968
969        $this->popViewId = $previewPageId;
970
971        return $previewPageId;
972    }
973
974    /**
975     * Check whether the current page has a "no view doktype" assigned
976     *
977     * @param array $currentPage
978     * @return bool
979     */
980    protected function canViewDoktype(array $currentPage): bool
981    {
982        if (!isset($currentPage['uid']) || !($currentPage['doktype'] ?? false)) {
983            // In case the current page record is invalid, the element can not be viewed
984            return false;
985        }
986
987        return !in_array((int)$currentPage['doktype'], [
988            PageRepository::DOKTYPE_SPACER,
989            PageRepository::DOKTYPE_SYSFOLDER,
990            PageRepository::DOKTYPE_RECYCLER,
991        ], true);
992    }
993
994    /**
995     * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
996     *
997     * This basically removes the trailing dots of sub-array keys in TypoScript.
998     * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
999     *
1000     * @param array $parameters Should be an empty array by default
1001     * @param array $typoScript The TypoScript configuration
1002     */
1003    protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
1004    {
1005        foreach ($typoScript as $key => $value) {
1006            if (is_array($value)) {
1007                $key = rtrim($key, '.');
1008                $parameters[$key] = [];
1009                $this->parseAdditionalGetParameters($parameters[$key], $value);
1010            } else {
1011                $parameters[$key] = $value;
1012            }
1013        }
1014    }
1015
1016    /**
1017     * Main module operation
1018     *
1019     * @param ServerRequestInterface $request
1020     */
1021    protected function main(ServerRequestInterface $request): void
1022    {
1023        $body = '';
1024        // Begin edit
1025        if (is_array($this->editconf)) {
1026            $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
1027
1028            // Creating the editing form, wrap it with buttons, document selector etc.
1029            $editForm = $this->makeEditForm();
1030            if ($editForm) {
1031                $this->firstEl = reset($this->elementsData);
1032                // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
1033                if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5]))
1034                    && !$this->dontStoreDocumentRef
1035                ) {
1036                    $this->docHandler[$this->storeUrlMd5] = [
1037                        $this->storeTitle,
1038                        $this->storeArray,
1039                        $this->storeUrl,
1040                        $this->firstEl
1041                    ];
1042                    $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
1043                    BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
1044                }
1045                $body = $this->formResultCompiler->addCssFiles();
1046                $body .= $this->compileForm($editForm);
1047                $body .= $this->formResultCompiler->printNeededJSFunctions();
1048                $body .= '</form>';
1049            }
1050        }
1051        // Access check...
1052        // The page will show only if there is a valid page and if this page may be viewed by the user
1053        $this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
1054        if ($this->pageinfo) {
1055            $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
1056        }
1057        // Setting up the buttons and markers for doc header
1058        $this->getButtons($request);
1059
1060        // Create language switch options if the record is already persisted
1061        if ($this->isSavedRecord) {
1062            $this->languageSwitch(
1063                (string)($this->firstEl['table'] ?? ''),
1064                (int)($this->firstEl['uid'] ?? 0),
1065                isset($this->firstEl['pid']) ? (int)$this->firstEl['pid'] : null
1066            );
1067        }
1068        $this->moduleTemplate->setContent($body);
1069    }
1070
1071    /**
1072     * Creates the editing form with FormEngine, based on the input from GPvars.
1073     *
1074     * @return string HTML form elements wrapped in tables
1075     */
1076    protected function makeEditForm(): string
1077    {
1078        // Initialize variables
1079        $this->elementsData = [];
1080        $this->errorC = 0;
1081        $this->newC = 0;
1082        $editForm = '';
1083        $beUser = $this->getBackendUser();
1084        // Traverse the GPvar edit array tables
1085        foreach ($this->editconf as $table => $conf) {
1086            if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
1087                // Traverse the keys/comments of each table (keys can be a comma list of uids)
1088                foreach ($conf as $cKey => $command) {
1089                    if ($command === 'edit' || $command === 'new') {
1090                        // Get the ids:
1091                        $ids = GeneralUtility::trimExplode(',', $cKey, true);
1092                        // Traverse the ids:
1093                        foreach ($ids as $theUid) {
1094                            // Don't save this document title in the document selector if the document is new.
1095                            if ($command === 'new') {
1096                                $this->dontStoreDocumentRef = 1;
1097                            }
1098
1099                            try {
1100                                $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
1101                                $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
1102                                $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
1103
1104                                // Reset viewId - it should hold data of last entry only
1105                                $this->viewId = 0;
1106                                $this->viewId_addParams = '';
1107
1108                                $formDataCompilerInput = [
1109                                    'tableName' => $table,
1110                                    'vanillaUid' => (int)$theUid,
1111                                    'command' => $command,
1112                                    'returnUrl' => $this->R_URI,
1113                                ];
1114                                if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
1115                                    $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
1116                                }
1117                                if (!empty($this->defVals) && is_array($this->defVals)) {
1118                                    $formDataCompilerInput['defaultValues'] = $this->defVals;
1119                                }
1120
1121                                $formData = $formDataCompiler->compile($formDataCompilerInput);
1122
1123                                // Set this->viewId if possible
1124                                if ($command === 'new'
1125                                    && $table !== 'pages'
1126                                    && !empty($formData['parentPageRow']['uid'])
1127                                ) {
1128                                    $this->viewId = $formData['parentPageRow']['uid'];
1129                                } else {
1130                                    if ($table === 'pages') {
1131                                        $this->viewId = $formData['databaseRow']['uid'];
1132                                    } elseif (!empty($formData['parentPageRow']['uid'])) {
1133                                        $this->viewId = $formData['parentPageRow']['uid'];
1134                                        // Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
1135                                        if (!empty($formData['processedTca']['ctrl']['languageField'])
1136                                            && is_array($formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']])
1137                                            && $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0] > 0
1138                                        ) {
1139                                            $this->viewId_addParams = '&L=' . $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0];
1140                                        }
1141                                    }
1142                                }
1143
1144                                // Determine if delete button can be shown
1145                                $deleteAccess = false;
1146                                if (
1147                                    $command === 'edit'
1148                                    || $command === 'new'
1149                                ) {
1150                                    $permission = $formData['userPermissionOnPage'];
1151                                    if ($formData['tableName'] === 'pages') {
1152                                        $deleteAccess = $permission & Permission::PAGE_DELETE ? true : false;
1153                                    } else {
1154                                        $deleteAccess = $permission & Permission::CONTENT_EDIT ? true : false;
1155                                    }
1156                                }
1157
1158                                // Display "is-locked" message
1159                                if ($command === 'edit') {
1160                                    $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
1161                                    if ($lockInfo) {
1162                                        $flashMessage = GeneralUtility::makeInstance(
1163                                            FlashMessage::class,
1164                                            $lockInfo['msg'],
1165                                            '',
1166                                            FlashMessage::WARNING
1167                                        );
1168                                        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1169                                        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1170                                        $defaultFlashMessageQueue->enqueue($flashMessage);
1171                                    }
1172                                }
1173
1174                                // Record title
1175                                if (!$this->storeTitle) {
1176                                    $this->storeTitle = htmlspecialchars($this->recTitle ?: ($formData['recordTitle'] ?? ''));
1177                                }
1178
1179                                $this->elementsData[] = [
1180                                    'table' => $table,
1181                                    'uid' => $formData['databaseRow']['uid'],
1182                                    'pid' => $formData['databaseRow']['pid'],
1183                                    'cmd' => $command,
1184                                    'deleteAccess' => $deleteAccess
1185                                ];
1186
1187                                if ($command !== 'new') {
1188                                    BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
1189                                }
1190
1191                                // Set list if only specific fields should be rendered. This will trigger
1192                                // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1193                                if ($this->columnsOnly) {
1194                                    if (is_array($this->columnsOnly)) {
1195                                        $formData['fieldListToRender'] = $this->columnsOnly[$table];
1196                                    } else {
1197                                        $formData['fieldListToRender'] = $this->columnsOnly;
1198                                    }
1199                                }
1200
1201                                $formData['renderType'] = 'outerWrapContainer';
1202                                $formResult = $nodeFactory->create($formData)->render();
1203
1204                                $html = $formResult['html'];
1205
1206                                $formResult['html'] = '';
1207                                $formResult['doSaveFieldName'] = 'doSave';
1208
1209                                // @todo: Put all the stuff into FormEngine as final "compiler" class
1210                                // @todo: This is done here for now to not rewrite addCssFiles()
1211                                // @todo: and printNeededJSFunctions() now
1212                                $this->formResultCompiler->mergeResult($formResult);
1213
1214                                // Seems the pid is set as hidden field (again) at end?!
1215                                if ($command === 'new') {
1216                                    // @todo: looks ugly
1217                                    $html .= LF
1218                                        . '<input type="hidden"'
1219                                        . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($formData['databaseRow']['uid']) . '][pid]"'
1220                                        . ' value="' . (int)$formData['databaseRow']['pid'] . '" />';
1221                                    $this->newC++;
1222                                }
1223
1224                                $editForm .= $html;
1225                            } catch (AccessDeniedException $e) {
1226                                $this->errorC++;
1227                                // Try to fetch error message from "recordInternals" be user object
1228                                // @todo: This construct should be logged and localized and de-uglified
1229                                $message = (!empty($beUser->errorMsg)) ? $beUser->errorMsg : $message = $e->getMessage() . ' ' . $e->getCode();
1230                                $title = $this->getLanguageService()
1231                                    ->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission');
1232                                $editForm .= $this->getInfobox($message, $title);
1233                            } catch (DatabaseRecordException | DatabaseRecordWorkspaceDeletePlaceholderException $e) {
1234                                $editForm .= $this->getInfobox($e->getMessage());
1235                            }
1236                        } // End of for each uid
1237                    }
1238                }
1239            }
1240        }
1241        return $editForm;
1242    }
1243
1244    /**
1245     * Helper function for rendering an Infobox
1246     *
1247     * @param string $message
1248     * @param string|null $title
1249     * @return string
1250     */
1251    protected function getInfobox(string $message, ?string $title = null): string
1252    {
1253        return '<div class="callout callout-danger">' .
1254                '<div class="media">' .
1255                    '<div class="media-left">' .
1256                        '<span class="fa-stack fa-lg callout-icon">' .
1257                            '<i class="fa fa-circle fa-stack-2x"></i>' .
1258                            '<i class="fa fa-times fa-stack-1x"></i>' .
1259                        '</span>' .
1260                    '</div>' .
1261                    '<div class="media-body">' .
1262                        ($title ? '<h4 class="callout-title">' . htmlspecialchars($title) . '</h4>' : '') .
1263                        '<div class="callout-body">' . htmlspecialchars($message) . '</div>' .
1264                    '</div>' .
1265                '</div>' .
1266            '</div>';
1267    }
1268
1269    /**
1270     * Create the panel of buttons for submitting the form or otherwise perform operations.
1271     *
1272     * @param ServerRequestInterface $request
1273     */
1274    protected function getButtons(ServerRequestInterface $request): void
1275    {
1276        $record = BackendUtility::getRecord($this->firstEl['table'], $this->firstEl['uid']);
1277        $TCActrl = $GLOBALS['TCA'][$this->firstEl['table']]['ctrl'];
1278
1279        $this->setIsSavedRecord();
1280
1281        $sysLanguageUid = 0;
1282        if (
1283            $this->isSavedRecord
1284            && isset($TCActrl['languageField'], $record[$TCActrl['languageField']])
1285        ) {
1286            $sysLanguageUid = (int)$record[$TCActrl['languageField']];
1287        } elseif (isset($this->defVals['sys_language_uid'])) {
1288            $sysLanguageUid = (int)$this->defVals['sys_language_uid'];
1289        }
1290
1291        $l18nParent = isset($TCActrl['transOrigPointerField'], $record[$TCActrl['transOrigPointerField']])
1292            ? (int)$record[$TCActrl['transOrigPointerField']]
1293            : 0;
1294
1295        $this->setIsPageInFreeTranslationMode($record, $sysLanguageUid);
1296
1297        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1298
1299        $this->registerCloseButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 1);
1300
1301        // Show buttons when table is not read-only
1302        if (
1303            !$this->errorC
1304            && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']
1305        ) {
1306            $this->registerSaveButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 2);
1307            $this->registerViewButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 3);
1308            if ($this->firstEl['cmd'] !== 'new') {
1309                $this->registerNewButtonToButtonBar(
1310                    $buttonBar,
1311                    ButtonBar::BUTTON_POSITION_LEFT,
1312                    4,
1313                    $sysLanguageUid,
1314                    $l18nParent
1315                );
1316                $this->registerDuplicationButtonToButtonBar(
1317                    $buttonBar,
1318                    ButtonBar::BUTTON_POSITION_LEFT,
1319                    5,
1320                    $sysLanguageUid,
1321                    $l18nParent
1322                );
1323            }
1324            $this->registerDeleteButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 6);
1325            $this->registerColumnsOnlyButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 7);
1326            $this->registerHistoryButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1327        }
1328
1329        $this->registerOpenInNewWindowButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 2);
1330        $this->registerShortcutButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 3);
1331        $this->registerCshButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 4);
1332    }
1333
1334    /**
1335     * Set the boolean to check if the record is saved
1336     */
1337    protected function setIsSavedRecord()
1338    {
1339        if (!is_bool($this->isSavedRecord)) {
1340            $this->isSavedRecord = (
1341                $this->firstEl['cmd'] !== 'new'
1342                && MathUtility::canBeInterpretedAsInteger($this->firstEl['uid'])
1343            );
1344        }
1345    }
1346
1347    /**
1348     * Returns if inconsistent language handling is allowed
1349     *
1350     * @return bool
1351     */
1352    protected function isInconsistentLanguageHandlingAllowed(): bool
1353    {
1354        $allowInconsistentLanguageHandling = BackendUtility::getPagesTSconfig(
1355            $this->pageinfo['uid']
1356        )['mod']['web_layout']['allowInconsistentLanguageHandling'];
1357
1358        return $allowInconsistentLanguageHandling['value'] === '1';
1359    }
1360
1361    /**
1362     * Set the boolean to check if the page is in free translation mode
1363     *
1364     * @param array|null $record
1365     * @param int $sysLanguageUid
1366     */
1367    protected function setIsPageInFreeTranslationMode($record, int $sysLanguageUid)
1368    {
1369        if ($this->firstEl['table'] === 'tt_content') {
1370            if (!$this->isSavedRecord) {
1371                $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1372                    (int)$this->pageinfo['uid'],
1373                    (int)$this->defVals['colPos'],
1374                    $sysLanguageUid
1375                );
1376            } else {
1377                $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1378                    (int)$this->pageinfo['uid'],
1379                    (int)$record['colPos'],
1380                    $sysLanguageUid
1381                );
1382            }
1383        }
1384    }
1385
1386    /**
1387     * Check if the page is in free translation mode
1388     *
1389     * @param int $page
1390     * @param int $column
1391     * @param int $language
1392     * @return bool
1393     */
1394    protected function getFreeTranslationMode(int $page, int $column, int $language): bool
1395    {
1396        $freeTranslationMode = false;
1397
1398        if (
1399            $this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0
1400            && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0
1401        ) {
1402            $freeTranslationMode = true;
1403        }
1404
1405        return $freeTranslationMode;
1406    }
1407
1408    /**
1409     * Register the close button to the button bar
1410     *
1411     * @param ButtonBar $buttonBar
1412     * @param string $position
1413     * @param int $group
1414     */
1415    protected function registerCloseButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1416    {
1417        $closeButton = $buttonBar->makeLinkButton()
1418            ->setHref('#')
1419            ->setClasses('t3js-editform-close')
1420            ->setTitle($this->getLanguageService()->sL(
1421                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'
1422            ))
1423            ->setShowLabelText(true)
1424            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1425                'actions-close',
1426                Icon::SIZE_SMALL
1427            ));
1428
1429        $buttonBar->addButton($closeButton, $position, $group);
1430    }
1431
1432    /**
1433     * Register the save button to the button bar
1434     *
1435     * @param ButtonBar $buttonBar
1436     * @param string $position
1437     * @param int $group
1438     */
1439    protected function registerSaveButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1440    {
1441        $saveButton = $buttonBar->makeInputButton()
1442            ->setForm('EditDocumentController')
1443            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1444            ->setName('_savedok')
1445            ->setShowLabelText(true)
1446            ->setTitle($this->getLanguageService()->sL(
1447                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'
1448            ))
1449            ->setValue('1');
1450
1451        $buttonBar->addButton($saveButton, $position, $group);
1452    }
1453
1454    /**
1455     * Register the view button to the button bar
1456     *
1457     * @param ButtonBar $buttonBar
1458     * @param string $position
1459     * @param int $group
1460     */
1461    protected function registerViewButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1462    {
1463        if (
1464            $this->viewId // Pid to show the record
1465            && !$this->noView // Passed parameter
1466            && !empty($this->firstEl['table']) // No table
1467
1468            // @TODO: TsConfig option should change to viewDoc
1469            && $this->getTsConfigOption($this->firstEl['table'], 'saveDocView')
1470        ) {
1471            $classNames = 't3js-editform-view';
1472
1473            $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
1474
1475            if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1476                $excludeDokTypes = GeneralUtility::intExplode(
1477                    ',',
1478                    $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
1479                    true
1480                );
1481            } else {
1482                // exclude sysfolders, spacers and recycler by default
1483                $excludeDokTypes = [
1484                    PageRepository::DOKTYPE_RECYCLER,
1485                    PageRepository::DOKTYPE_SYSFOLDER,
1486                    PageRepository::DOKTYPE_SPACER
1487                ];
1488            }
1489
1490            if (
1491                !in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)
1492                || isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl['table'] . '.']['previewPageId'])
1493            ) {
1494                $previewPageId = $this->getPreviewPageId();
1495                try {
1496                    $previewUrl = BackendUtility::getPreviewUrl(
1497                        $previewPageId,
1498                        '',
1499                        BackendUtility::BEgetRootLine($previewPageId),
1500                        $this->getPreviewUrlAnchorSection(),
1501                        $this->viewUrl,
1502                        $this->getPreviewUrlParameters($previewPageId)
1503                    );
1504
1505                    $viewButton = $buttonBar->makeLinkButton()
1506                        ->setHref($previewUrl)
1507                        ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1508                            'actions-view',
1509                            Icon::SIZE_SMALL
1510                        ))
1511                        ->setShowLabelText(true)
1512                        ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc'));
1513
1514                    if (!$this->isSavedRecord) {
1515                        if ($this->firstEl['table'] === 'pages') {
1516                            $viewButton->setDataAttributes(['is-new' => '']);
1517                        }
1518                    }
1519
1520                    if ($classNames !== '') {
1521                        $viewButton->setClasses($classNames);
1522                    }
1523
1524                    $buttonBar->addButton($viewButton, $position, $group);
1525                } catch (UnableToLinkToPageException $e) {
1526                    // Do not add any button
1527                }
1528            }
1529        }
1530    }
1531
1532    /**
1533     * Register the new button to the button bar
1534     *
1535     * @param ButtonBar $buttonBar
1536     * @param string $position
1537     * @param int $group
1538     * @param int $sysLanguageUid
1539     * @param int $l18nParent
1540     */
1541    protected function registerNewButtonToButtonBar(
1542        ButtonBar $buttonBar,
1543        string $position,
1544        int $group,
1545        int $sysLanguageUid,
1546        int $l18nParent
1547    ) {
1548        if (
1549            $this->firstEl['table'] !== 'sys_file_metadata'
1550            && !empty($this->firstEl['table'])
1551            && (
1552                (
1553                    (
1554                        $this->isInconsistentLanguageHandlingAllowed()
1555                        || $this->isPageInFreeTranslationMode
1556                    )
1557                    && $this->firstEl['table'] === 'tt_content'
1558                )
1559                || (
1560                    $this->firstEl['table'] !== 'tt_content'
1561                    && (
1562                        $sysLanguageUid === 0
1563                        || $l18nParent === 0
1564                    )
1565                )
1566            )
1567            && $this->getTsConfigOption($this->firstEl['table'], 'saveDocNew')
1568        ) {
1569            $classNames = 't3js-editform-new';
1570
1571            $newButton = $buttonBar->makeLinkButton()
1572                ->setHref('#')
1573                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1574                    'actions-add',
1575                    Icon::SIZE_SMALL
1576                ))
1577                ->setShowLabelText(true)
1578                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc'));
1579
1580            if (!$this->isSavedRecord) {
1581                $newButton->setDataAttributes(['is-new' => '']);
1582            }
1583
1584            if ($classNames !== '') {
1585                $newButton->setClasses($classNames);
1586            }
1587
1588            $buttonBar->addButton($newButton, $position, $group);
1589        }
1590    }
1591
1592    /**
1593     * Register the duplication button to the button bar
1594     *
1595     * @param ButtonBar $buttonBar
1596     * @param string $position
1597     * @param int $group
1598     * @param int $sysLanguageUid
1599     * @param int $l18nParent
1600     */
1601    protected function registerDuplicationButtonToButtonBar(
1602        ButtonBar $buttonBar,
1603        string $position,
1604        int $group,
1605        int $sysLanguageUid,
1606        int $l18nParent
1607    ) {
1608        if (
1609            $this->firstEl['table'] !== 'sys_file_metadata'
1610            && !empty($this->firstEl['table'])
1611            && (
1612                (
1613                    (
1614                        $this->isInconsistentLanguageHandlingAllowed()
1615                        || $this->isPageInFreeTranslationMode
1616                    )
1617                    && $this->firstEl['table'] === 'tt_content'
1618                )
1619                || (
1620                    $this->firstEl['table'] !== 'tt_content'
1621                    && (
1622                        $sysLanguageUid === 0
1623                        || $l18nParent === 0
1624                    )
1625                )
1626            )
1627            && $this->getTsConfigOption($this->firstEl['table'], 'showDuplicate')
1628        ) {
1629            $classNames = 't3js-editform-duplicate';
1630
1631            $duplicateButton = $buttonBar->makeLinkButton()
1632                ->setHref('#')
1633                ->setShowLabelText(true)
1634                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc'))
1635                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1636                    'actions-document-duplicates-select',
1637                    Icon::SIZE_SMALL
1638                ));
1639
1640            if (!$this->isSavedRecord) {
1641                $duplicateButton->setDataAttributes(['is-new' => '']);
1642            }
1643
1644            if ($classNames !== '') {
1645                $duplicateButton->setClasses($classNames);
1646            }
1647
1648            $buttonBar->addButton($duplicateButton, $position, $group);
1649        }
1650    }
1651
1652    /**
1653     * Register the delete button to the button bar
1654     *
1655     * @param ButtonBar $buttonBar
1656     * @param string $position
1657     * @param int $group
1658     */
1659    protected function registerDeleteButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1660    {
1661        if (
1662            $this->firstEl['deleteAccess']
1663            && !$this->getDisableDelete()
1664            && !$this->isRecordCurrentBackendUser()
1665            && $this->isSavedRecord
1666            && count($this->elementsData) === 1
1667        ) {
1668            $classNames = 't3js-editform-delete-record';
1669            $returnUrl = $this->retUrl;
1670            if ($this->firstEl['table'] === 'pages') {
1671                parse_str((string)parse_url($returnUrl, PHP_URL_QUERY), $queryParams);
1672                if (
1673                    isset($queryParams['route'], $queryParams['id'])
1674                    && (string)$this->firstEl['uid'] === (string)$queryParams['id']
1675                ) {
1676                    // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
1677                    // tree from the outside to be able to mark the pid as active
1678                    $returnUrl = (string)$this->uriBuilder->buildUriFromRoutePath($queryParams['route'], ['id' => 0]);
1679                }
1680            }
1681
1682            /** @var ReferenceIndex $referenceIndex */
1683            $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1684            $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords(
1685                $this->firstEl['table'],
1686                (int)$this->firstEl['uid']
1687            );
1688
1689            $referenceCountMessage = BackendUtility::referenceCount(
1690                $this->firstEl['table'],
1691                (string)(int)$this->firstEl['uid'],
1692                $this->getLanguageService()->sL(
1693                    'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'
1694                ),
1695                (string)$numberOfReferences
1696            );
1697            $translationCountMessage = BackendUtility::translationCount(
1698                $this->firstEl['table'],
1699                (string)(int)$this->firstEl['uid'],
1700                $this->getLanguageService()->sL(
1701                    'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'
1702                )
1703            );
1704
1705            $deleteUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [
1706                'cmd' => [
1707                    $this->firstEl['table'] => [
1708                        $this->firstEl['uid'] => [
1709                            'delete' => '1'
1710                        ]
1711                    ]
1712                ],
1713                'redirect' => $this->retUrl
1714            ]);
1715
1716            $deleteButton = $buttonBar->makeLinkButton()
1717                ->setClasses($classNames)
1718                ->setDataAttributes([
1719                    'return-url' => $returnUrl,
1720                    'uid' => $this->firstEl['uid'],
1721                    'table' => $this->firstEl['table'],
1722                    'reference-count-message' => $referenceCountMessage,
1723                    'translation-count-message' => $translationCountMessage
1724                ])
1725                ->setHref($deleteUrl)
1726                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1727                    'actions-edit-delete',
1728                    Icon::SIZE_SMALL
1729                ))
1730                ->setShowLabelText(true)
1731                ->setTitle($this->getLanguageService()->getLL('deleteItem'));
1732
1733            $buttonBar->addButton($deleteButton, $position, $group);
1734        }
1735    }
1736
1737    /**
1738     * Register the history button to the button bar
1739     *
1740     * @param ButtonBar $buttonBar
1741     * @param string $position
1742     * @param int $group
1743     */
1744    protected function registerHistoryButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1745    {
1746        if (
1747            count($this->elementsData) === 1
1748            && !empty($this->firstEl['table'])
1749            && $this->getTsConfigOption($this->firstEl['table'], 'showHistory')
1750        ) {
1751            $historyUrl = (string)$this->uriBuilder->buildUriFromRoute('record_history', [
1752                'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1753                'returnUrl' => $this->R_URI,
1754            ]);
1755            $historyButton = $buttonBar->makeLinkButton()
1756                ->setHref($historyUrl)
1757                ->setTitle('Open history of this record')
1758                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1759                    'actions-document-history-open',
1760                    Icon::SIZE_SMALL
1761                ));
1762
1763            $buttonBar->addButton($historyButton, $position, $group);
1764        }
1765    }
1766
1767    /**
1768     * Register the columns only button to the button bar
1769     *
1770     * @param ButtonBar $buttonBar
1771     * @param string $position
1772     * @param int $group
1773     */
1774    protected function registerColumnsOnlyButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1775    {
1776        if (
1777            $this->columnsOnly
1778            && count($this->elementsData) === 1
1779        ) {
1780            $columnsOnlyButton = $buttonBar->makeLinkButton()
1781                ->setHref($this->R_URI . '&columnsOnly=')
1782                ->setTitle($this->getLanguageService()->getLL('editWholeRecord'))
1783                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1784                    'actions-open',
1785                    Icon::SIZE_SMALL
1786                ));
1787
1788            $buttonBar->addButton($columnsOnlyButton, $position, $group);
1789        }
1790    }
1791
1792    /**
1793     * Register the open in new window button to the button bar
1794     *
1795     * @param ButtonBar $buttonBar
1796     * @param string $position
1797     * @param int $group
1798     */
1799    protected function registerOpenInNewWindowButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1800    {
1801        $closeUrl = $this->getCloseUrl();
1802        if ($this->returnUrl !== $closeUrl) {
1803            $requestUri = GeneralUtility::linkThisScript([
1804                'returnUrl' => $closeUrl,
1805            ]);
1806            $aOnClick = 'vHWin=window.open('
1807                . GeneralUtility::quoteJSvalue($requestUri) . ','
1808                . GeneralUtility::quoteJSvalue(md5($this->R_URI))
1809                . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1810
1811            $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
1812                ->makeLinkButton()
1813                ->setHref('#')
1814                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
1815                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL))
1816                ->setOnClick($aOnClick);
1817
1818            $buttonBar->addButton($openInNewWindowButton, $position, $group);
1819        }
1820    }
1821
1822    /**
1823     * Register the shortcut button to the button bar
1824     *
1825     * @param ButtonBar $buttonBar
1826     * @param string $position
1827     * @param int $group
1828     */
1829    protected function registerShortcutButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1830    {
1831        if ($this->returnUrl !== $this->getCloseUrl()) {
1832            $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
1833            $shortCutButton->setModuleName('xMOD_alt_doc.php')
1834                ->setGetVariables([
1835                    'returnUrl',
1836                    'edit',
1837                    'defVals',
1838                    'overrideVals',
1839                    'columnsOnly',
1840                    'returnNewPageId',
1841                    'noView']);
1842
1843            $buttonBar->addButton($shortCutButton, $position, $group);
1844        }
1845    }
1846
1847    /**
1848     * Register the CSH button to the button bar
1849     *
1850     * @param ButtonBar $buttonBar
1851     * @param string $position
1852     * @param int $group
1853     */
1854    protected function registerCshButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1855    {
1856        $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('TCEforms');
1857
1858        $buttonBar->addButton($cshButton, $position, $group);
1859    }
1860
1861    /**
1862     * Get the count of connected translated content elements
1863     *
1864     * @param int $page
1865     * @param int $column
1866     * @param int $language
1867     * @return int
1868     */
1869    protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int
1870    {
1871        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1872
1873        return (int)$queryBuilder
1874            ->andWhere(
1875                $queryBuilder->expr()->gt(
1876                    $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1877                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1878                )
1879            )
1880            ->execute()
1881            ->fetchColumn(0);
1882    }
1883
1884    /**
1885     * Get the count of standalone translated content elements
1886     *
1887     * @param int $page
1888     * @param int $column
1889     * @param int $language
1890     * @return int
1891     */
1892    protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int
1893    {
1894        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1895
1896        return (int)$queryBuilder
1897            ->andWhere(
1898                $queryBuilder->expr()->eq(
1899                    $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1900                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1901                )
1902            )
1903            ->execute()
1904            ->fetchColumn(0);
1905    }
1906
1907    /**
1908     * Get the query builder for the translation mode
1909     *
1910     * @param int $page
1911     * @param int $column
1912     * @param int $language
1913     * @return QueryBuilder
1914     */
1915    protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder
1916    {
1917        $languageField = $GLOBALS['TCA']['tt_content']['ctrl']['languageField'];
1918
1919        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1920            ->getQueryBuilderForTable('tt_content');
1921
1922        $queryBuilder->getRestrictions()
1923            ->removeAll()
1924            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1925            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
1926
1927        return $queryBuilder
1928            ->count('uid')
1929            ->from('tt_content')
1930            ->where(
1931                $queryBuilder->expr()->eq(
1932                    'pid',
1933                    $queryBuilder->createNamedParameter($page, \PDO::PARAM_INT)
1934                ),
1935                $queryBuilder->expr()->eq(
1936                    $languageField,
1937                    $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
1938                ),
1939                $queryBuilder->expr()->eq(
1940                    'colPos',
1941                    $queryBuilder->createNamedParameter($column, \PDO::PARAM_INT)
1942                )
1943            );
1944    }
1945
1946    /**
1947     * Put together the various elements (buttons, selectors, form) into a table
1948     *
1949     * @param string $editForm HTML form.
1950     * @return string Composite HTML
1951     */
1952    protected function compileForm(string $editForm): string
1953    {
1954        $formContent = '
1955            <form
1956                action="' . htmlspecialchars($this->R_URI) . '"
1957                method="post"
1958                enctype="multipart/form-data"
1959                name="editform"
1960                id="EditDocumentController"
1961            >
1962            ' . $editForm . '
1963            <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
1964            <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />
1965            <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$this->viewId) . '" />
1966            <input type="hidden" name="closeDoc" value="0" />
1967            <input type="hidden" name="doSave" value="0" />
1968            <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
1969            <input type="hidden" name="_scrollPosition" value="" />';
1970        if ($this->returnNewPageId) {
1971            $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
1972        }
1973        if ($this->viewId_addParams) {
1974            $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams) . '" />';
1975        }
1976        return $formContent;
1977    }
1978
1979    /**
1980     * Returns if delete for the current table is disabled by configuration.
1981     * For sys_file_metadata in default language delete is always disabled.
1982     *
1983     * @return bool
1984     */
1985    protected function getDisableDelete(): bool
1986    {
1987        $disableDelete = false;
1988        if ($this->firstEl['table'] === 'sys_file_metadata') {
1989            $row = BackendUtility::getRecord('sys_file_metadata', $this->firstEl['uid'], 'sys_language_uid');
1990            $languageUid = $row['sys_language_uid'];
1991            if ($languageUid === 0) {
1992                $disableDelete = true;
1993            }
1994        } else {
1995            $disableDelete = (bool)$this->getTsConfigOption($this->firstEl['table'] ?? '', 'disableDelete');
1996        }
1997        return $disableDelete;
1998    }
1999
2000    /**
2001     * Return true in case the current record is the current backend user
2002     *
2003     * @return bool
2004     */
2005    protected function isRecordCurrentBackendUser(): bool
2006    {
2007        $backendUser = $this->getBackendUser();
2008
2009        return $this->firstEl['table'] === 'be_users'
2010            && (int)($this->firstEl['uid'] ?? 0) === (int)$backendUser->user[$backendUser->userid_column];
2011    }
2012
2013    /**
2014     * Returns the URL (usually for the "returnUrl") which closes the current window.
2015     * Used when editing a record in a popup.
2016     *
2017     * @return string
2018     */
2019    protected function getCloseUrl(): string
2020    {
2021        $closeUrl = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
2022        return PathUtility::getAbsoluteWebPath($closeUrl);
2023    }
2024
2025    /***************************
2026     *
2027     * Localization stuff
2028     *
2029     ***************************/
2030    /**
2031     * Make selector box for creating new translation for a record or switching to edit the record
2032     * in an existing language. Displays only languages which are available for the current page.
2033     *
2034     * @param string $table Table name
2035     * @param int $uid Uid for which to create a new language
2036     * @param int|null $pid Pid of the record
2037     */
2038    protected function languageSwitch(string $table, int $uid, $pid = null)
2039    {
2040        $backendUser = $this->getBackendUser();
2041        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
2042        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2043        // Table editable and activated for languages?
2044        if ($backendUser->check('tables_modify', $table)
2045            && $languageField
2046            && $transOrigPointerField
2047        ) {
2048            if ($pid === null) {
2049                $row = BackendUtility::getRecord($table, $uid, 'pid');
2050                $pid = $row['pid'];
2051            }
2052            // Get all available languages for the page
2053            // If editing a page, the translations of the current UID need to be fetched
2054            if ($table === 'pages') {
2055                $row = BackendUtility::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
2056                // Ensure the check is always done against the default language page
2057                $availableLanguages = $this->getLanguages(
2058                    (int)$row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $uid,
2059                    $table
2060                );
2061            } else {
2062                $availableLanguages = $this->getLanguages((int)$pid, $table);
2063            }
2064            // Remove default language, if user does not have access. This is necessary, since
2065            // the default language is always added when fetching the system languages (#88504).
2066            if (isset($availableLanguages[0]) && !$this->getBackendUser()->checkLanguageAccess(0)) {
2067                unset($availableLanguages[0]);
2068            }
2069            // Page available in other languages than default language?
2070            if (count($availableLanguages) > 1) {
2071                $rowsByLang = [];
2072                $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
2073                // Get record in current language
2074                $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
2075                if (!is_array($rowCurrent)) {
2076                    $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
2077                }
2078                $currentLanguage = (int)$rowCurrent[$languageField];
2079                // Disabled for records with [all] language!
2080                if ($currentLanguage > -1) {
2081                    // Get record in default language if needed
2082                    if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
2083                        $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
2084                            $table,
2085                            $rowCurrent[$transOrigPointerField],
2086                            $fetchFields
2087                        );
2088                        if (!is_array($rowsByLang[0])) {
2089                            $rowsByLang[0] = BackendUtility::getRecord(
2090                                $table,
2091                                $rowCurrent[$transOrigPointerField],
2092                                $fetchFields
2093                            );
2094                        }
2095                    } else {
2096                        $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
2097                    }
2098                    // List of language id's that should not be added to the selector
2099                    $noAddOption = [];
2100                    if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
2101                        // Get record in other languages to see what's already available
2102
2103                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2104                            ->getQueryBuilderForTable($table);
2105
2106                        $queryBuilder->getRestrictions()
2107                            ->removeAll()
2108                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2109                            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace));
2110
2111                        $result = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fetchFields, true))
2112                            ->from($table)
2113                            ->where(
2114                                $queryBuilder->expr()->eq(
2115                                    'pid',
2116                                    $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
2117                                ),
2118                                $queryBuilder->expr()->gt(
2119                                    $languageField,
2120                                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2121                                ),
2122                                $queryBuilder->expr()->eq(
2123                                    $transOrigPointerField,
2124                                    $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO::PARAM_INT)
2125                                )
2126                            )
2127                            ->execute();
2128
2129                        while ($row = $result->fetch()) {
2130                            if ($backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
2131                                $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $table, $row['uid'], 'uid,t3ver_state');
2132                                if (!empty($workspaceVersion)) {
2133                                    $versionState = VersionState::cast($workspaceVersion['t3ver_state']);
2134                                    if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
2135                                        // If a workspace delete placeholder exists for this translation: Mark
2136                                        // this language as "don't add to selector" and continue with next row,
2137                                        // otherwise an edit link to a delete placeholder would be created, which
2138                                        // does not make sense.
2139                                        $noAddOption[] = (int)$row[$languageField];
2140                                        continue;
2141                                    }
2142                                }
2143                            }
2144                            $rowsByLang[$row[$languageField]] = $row;
2145                        }
2146                    }
2147                    $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
2148                    $languageMenu->setIdentifier('_langSelector');
2149                    foreach ($availableLanguages as $languageId => $language) {
2150                        $selectorOptionLabel = $language['title'];
2151                        // Create url for creating a localized record
2152                        $addOption = true;
2153                        $href = '';
2154                        if (!isset($rowsByLang[$languageId])) {
2155                            // Translation in this language does not exist
2156                            $selectorOptionLabel .= ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
2157                            $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
2158                                'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $languageId,
2159                                'returnUrl' => $this->retUrl
2160                            ]);
2161
2162                            if (array_key_exists(0, $rowsByLang)) {
2163                                $href = BackendUtility::getLinkToDataHandlerAction(
2164                                    '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $languageId,
2165                                    $redirectUrl
2166                                );
2167                            } else {
2168                                $addOption = false;
2169                            }
2170                        } else {
2171                            $params = [
2172                                'edit[' . $table . '][' . $rowsByLang[$languageId]['uid'] . ']' => 'edit',
2173                                'returnUrl' => $this->retUrl
2174                            ];
2175                            if ($table === 'pages') {
2176                                // Disallow manual adjustment of the language field for pages
2177                                $params['overrideVals'] = [
2178                                    'pages' => [
2179                                        'sys_language_uid' => $languageId
2180                                    ]
2181                                ];
2182                            }
2183                            $href = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params);
2184                        }
2185                        if ($addOption && !in_array($languageId, $noAddOption, true)) {
2186                            $menuItem = $languageMenu->makeMenuItem()
2187                                ->setTitle($selectorOptionLabel)
2188                                ->setHref($href);
2189                            if ($languageId === $currentLanguage) {
2190                                $menuItem->setActive(true);
2191                            }
2192                            $languageMenu->addMenuItem($menuItem);
2193                        }
2194                    }
2195                    $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
2196                }
2197            }
2198        }
2199    }
2200
2201    /**
2202     * Redirects to FormEngine with new parameters to edit a just created localized record
2203     *
2204     * @param ServerRequestInterface $request Incoming request object
2205     * @return ResponseInterface|null Possible redirect response
2206     */
2207    protected function localizationRedirect(ServerRequestInterface $request): ?ResponseInterface
2208    {
2209        $justLocalized = $request->getQueryParams()['justLocalized'];
2210
2211        if (empty($justLocalized)) {
2212            return null;
2213        }
2214
2215        [$table, $origUid, $language] = explode(':', $justLocalized);
2216
2217        if ($GLOBALS['TCA'][$table]
2218            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
2219            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
2220        ) {
2221            $parsedBody = $request->getParsedBody();
2222            $queryParams = $request->getQueryParams();
2223
2224            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2225            $queryBuilder->getRestrictions()
2226                ->removeAll()
2227                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2228                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
2229            $localizedRecord = $queryBuilder->select('uid')
2230                ->from($table)
2231                ->where(
2232                    $queryBuilder->expr()->eq(
2233                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
2234                        $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
2235                    ),
2236                    $queryBuilder->expr()->eq(
2237                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2238                        $queryBuilder->createNamedParameter($origUid, \PDO::PARAM_INT)
2239                    )
2240                )
2241                ->execute()
2242                ->fetch();
2243            $returnUrl = $parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '';
2244            if (is_array($localizedRecord)) {
2245                // Create redirect response to self to edit just created record
2246                return new RedirectResponse(
2247                    (string)$this->uriBuilder->buildUriFromRoute(
2248                        'record_edit',
2249                        [
2250                            'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2251                            'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2252                        ]
2253                    ),
2254                    303
2255                );
2256            }
2257        }
2258        return null;
2259    }
2260
2261    /**
2262     * Returns languages  available for record translations on given page.
2263     *
2264     * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
2265     *                hidden. If set to another value, the query will select all sys_language records that has a
2266     *                translation record on that page (and is not hidden, unless you are admin user)
2267     * @param string $table For pages we want all languages, for other records the languages of the page translations
2268     * @return array Array with languages (uid, title, ISOcode, flagIcon)
2269     */
2270    protected function getLanguages(int $id, string $table): array
2271    {
2272        // This usually happens when a non-pages record is added after another, so we are fetching the proper page ID
2273        if ($id < 0 && $table !== 'pages') {
2274            $pageId = $this->pageinfo['uid'] ?? null;
2275            if ($pageId !== null) {
2276                $pageId = (int)$pageId;
2277            } else {
2278                $fullRecord = BackendUtility::getRecord($table, abs($id));
2279                $pageId = (int)$fullRecord['pid'];
2280            }
2281        } else {
2282            if ($table === 'pages' && $id > 0) {
2283                $fullRecord = BackendUtility::getRecordWSOL('pages', $id);
2284                $id = (int)($fullRecord['t3ver_oid'] ?: $fullRecord['uid']);
2285            }
2286            $pageId = $id;
2287        }
2288        // Fetch the current translations of this page, to only show the ones where there is a page translation
2289        $allLanguages = array_filter(
2290            GeneralUtility::makeInstance(TranslationConfigurationProvider::class)->getSystemLanguages($pageId),
2291            static function (array $language): bool {
2292                return (int)$language['uid'] !== -1;
2293            }
2294        );
2295        if ($table !== 'pages' && $id > 0) {
2296            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
2297            $queryBuilder->getRestrictions()->removeAll()
2298                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2299                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
2300            $statement = $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
2301                ->from('pages')
2302                ->where(
2303                    $queryBuilder->expr()->eq(
2304                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2305                        $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
2306                    )
2307                )
2308                ->execute();
2309
2310            $availableLanguages = [];
2311
2312            if ($allLanguages[0] ?? false) {
2313                $availableLanguages = [
2314                    0 => $allLanguages[0]
2315                ];
2316            }
2317
2318            while ($row = $statement->fetch()) {
2319                $languageId = (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
2320                if (isset($allLanguages[$languageId])) {
2321                    $availableLanguages[$languageId] = $allLanguages[$languageId];
2322                }
2323            }
2324            return $availableLanguages;
2325        }
2326        return $allLanguages;
2327    }
2328
2329    /**
2330     * Fix $this->editconf if versioning applies to any of the records
2331     *
2332     * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
2333     */
2334    protected function fixWSversioningInEditConf($mapArray = false): void
2335    {
2336        // Traverse the editConf array
2337        if (is_array($this->editconf)) {
2338            // Tables:
2339            foreach ($this->editconf as $table => $conf) {
2340                if (is_array($conf) && $GLOBALS['TCA'][$table]) {
2341                    // Traverse the keys/comments of each table (keys can be a comma list of uids)
2342                    $newConf = [];
2343                    foreach ($conf as $cKey => $cmd) {
2344                        if ($cmd === 'edit') {
2345                            // Traverse the ids:
2346                            $ids = GeneralUtility::trimExplode(',', $cKey, true);
2347                            foreach ($ids as $idKey => $theUid) {
2348                                if (is_array($mapArray)) {
2349                                    if ($mapArray[$table][$theUid]) {
2350                                        $ids[$idKey] = $mapArray[$table][$theUid];
2351                                    }
2352                                } else {
2353                                    // Default, look for versions in workspace for record:
2354                                    $calcPRec = $this->getRecordForEdit((string)$table, (int)$theUid);
2355                                    if (is_array($calcPRec)) {
2356                                        // Setting UID again if it had changed, eg. due to workspace versioning.
2357                                        $ids[$idKey] = $calcPRec['uid'];
2358                                    }
2359                                }
2360                            }
2361                            // Add the possibly manipulated IDs to the new-build newConf array:
2362                            $newConf[implode(',', $ids)] = $cmd;
2363                        } else {
2364                            $newConf[$cKey] = $cmd;
2365                        }
2366                    }
2367                    // Store the new conf array:
2368                    $this->editconf[$table] = $newConf;
2369                }
2370            }
2371        }
2372    }
2373
2374    /**
2375     * Get record for editing.
2376     *
2377     * @param string $table Table name
2378     * @param int $theUid Record UID
2379     * @return array|false Returns record to edit, false if none
2380     */
2381    protected function getRecordForEdit(string $table, int $theUid)
2382    {
2383        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
2384        // Fetch requested record:
2385        $reqRecord = BackendUtility::getRecord($table, $theUid, 'uid,pid' . ($tableSupportsVersioning ? ',t3ver_oid' : ''));
2386        if (is_array($reqRecord)) {
2387            // If workspace is OFFLINE:
2388            if ($this->getBackendUser()->workspace != 0) {
2389                // Check for versioning support of the table:
2390                if ($tableSupportsVersioning) {
2391                    // If the record is already a version of "something" pass it by.
2392                    if ($reqRecord['t3ver_oid'] > 0) {
2393                        // (If it turns out not to be a version of the current workspace there will be trouble, but
2394                        // that is handled inside DataHandler then and in the interface it would clearly be an error of
2395                        // links if the user accesses such a scenario)
2396                        return $reqRecord;
2397                    }
2398                    // The input record was online and an offline version must be found or made:
2399                    // Look for version of this workspace:
2400                    $versionRec = BackendUtility::getWorkspaceVersionOfRecord(
2401                        $this->getBackendUser()->workspace,
2402                        $table,
2403                        $reqRecord['uid'],
2404                        'uid,pid,t3ver_oid'
2405                    );
2406                    return is_array($versionRec) ? $versionRec : $reqRecord;
2407                }
2408                // This means that editing cannot occur on this record because it was not supporting versioning
2409                // which is required inside an offline workspace.
2410                return false;
2411            }
2412            // In ONLINE workspace, just return the originally requested record:
2413            return $reqRecord;
2414        }
2415        // Return FALSE because the table/uid was not found anyway.
2416        return false;
2417    }
2418
2419    /**
2420     * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2421     * to prepare 'open documents' urls
2422     */
2423    protected function compileStoreData(): void
2424    {
2425        // @todo: Refactor in TYPO3 v10: This GeneralUtility method fiddles with _GP()
2426        $this->storeArray = GeneralUtility::compileSelectedGetVarsFromArray(
2427            'edit,defVals,overrideVals,columnsOnly,noView',
2428            $this->R_URL_getvars
2429        );
2430        $this->storeUrl = HttpUtility::buildQueryString($this->storeArray, '&');
2431        $this->storeUrlMd5 = md5($this->storeUrl);
2432    }
2433
2434    /**
2435     * Get a TSConfig 'option.' array, possibly for a specific table.
2436     *
2437     * @param string $table Table name
2438     * @param string $key Options key
2439     * @return string
2440     */
2441    protected function getTsConfigOption(string $table, string $key): string
2442    {
2443        return \trim((string)(
2444            $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table]
2445            ?? $this->getBackendUser()->getTSConfig()['options.'][$key]
2446            ?? ''
2447        ));
2448    }
2449
2450    /**
2451     * Handling the closing of a document
2452     * The argument $mode can be one of this values:
2453     * - 0/1 will redirect to $this->retUrl [self::DOCUMENT_CLOSE_MODE_DEFAULT || self::DOCUMENT_CLOSE_MODE_REDIRECT]
2454     * - 3 will clear the docHandler (thus closing all documents) [self::DOCUMENT_CLOSE_MODE_CLEAR_ALL]
2455     * - 4 will do no redirect [self::DOCUMENT_CLOSE_MODE_NO_REDIRECT]
2456     * - other values will call setDocument with ->retUrl
2457     *
2458     * @param int $mode the close mode: one of self::DOCUMENT_CLOSE_MODE_*
2459     * @param ServerRequestInterface $request Incoming request
2460     * @return ResponseInterface|null Redirect response if needed
2461     */
2462    protected function closeDocument($mode, ServerRequestInterface $request): ?ResponseInterface
2463    {
2464        $setupArr = [];
2465        $mode = (int)$mode;
2466        // If current document is found in docHandler,
2467        // then unset it, possibly unset it ALL and finally, write it to the session data
2468        if (isset($this->docHandler[$this->storeUrlMd5])) {
2469            // add the closing document to the recent documents
2470            $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
2471            if (!is_array($recentDocs)) {
2472                $recentDocs = [];
2473            }
2474            $closedDoc = $this->docHandler[$this->storeUrlMd5];
2475            $recentDocs = array_merge([$this->storeUrlMd5 => $closedDoc], $recentDocs);
2476            if (count($recentDocs) > 8) {
2477                $recentDocs = array_slice($recentDocs, 0, 8);
2478            }
2479            // remove it from the list of the open documents
2480            unset($this->docHandler[$this->storeUrlMd5]);
2481            if ($mode === self::DOCUMENT_CLOSE_MODE_CLEAR_ALL) {
2482                $recentDocs = array_merge($this->docHandler, $recentDocs);
2483                $this->docHandler = [];
2484            }
2485            $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
2486            $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->docDat[1]]);
2487            BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
2488        }
2489        if ($mode === self::DOCUMENT_CLOSE_MODE_NO_REDIRECT) {
2490            return null;
2491        }
2492        // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by
2493        // other scripts, like wizard_add, to know which records was created or so...
2494        if ($this->returnEditConf && $this->retUrl != (string)$this->uriBuilder->buildUriFromRoute('dummy')) {
2495            $this->retUrl .= '&returnEditConf=' . rawurlencode((string)json_encode($this->editconf));
2496        }
2497        // If mode is NOT set (means 0) OR set to 1, then make a header location redirect to $this->retUrl
2498        if ($mode === self::DOCUMENT_CLOSE_MODE_DEFAULT || $mode === self::DOCUMENT_CLOSE_MODE_REDIRECT) {
2499            return new RedirectResponse($this->retUrl, 303);
2500        }
2501        if ($this->retUrl === '') {
2502            return null;
2503        }
2504        $retUrl = (string)$this->returnUrl;
2505        if (is_array($this->docHandler) && !empty($this->docHandler)) {
2506            if (!empty($setupArr[2])) {
2507                $sParts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
2508                $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2509            }
2510        }
2511        return new RedirectResponse($retUrl, 303);
2512    }
2513
2514    /**
2515     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2516     */
2517    protected function getBackendUser()
2518    {
2519        return $GLOBALS['BE_USER'];
2520    }
2521
2522    /**
2523     * Returns LanguageService
2524     *
2525     * @return \TYPO3\CMS\Core\Localization\LanguageService
2526     */
2527    protected function getLanguageService()
2528    {
2529        return $GLOBALS['LANG'];
2530    }
2531}
2532