2 declare(strict_types
= 1);
3 namespace TYPO3\CMS\Backend\Controller
;
6 * This file is part of the TYPO3 CMS project.
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.
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
15 * The TYPO3 project - inspiring people to share!
18 use Psr\Http\Message\ResponseInterface
;
19 use Psr\Http\Message\ServerRequestInterface
;
20 use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException
;
21 use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException
;
22 use TYPO3\CMS\Backend\Form\FormDataCompiler
;
23 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord
;
24 use TYPO3\CMS\Backend\Form\FormResultCompiler
;
25 use TYPO3\CMS\Backend\Form\NodeFactory
;
26 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility
;
27 use TYPO3\CMS\Backend\Routing\UriBuilder
;
28 use TYPO3\CMS\Backend\Template\Components\ButtonBar
;
29 use TYPO3\CMS\Backend\Template\ModuleTemplate
;
30 use TYPO3\CMS\Backend\Utility\BackendUtility
;
31 use TYPO3\CMS\Core\Database\ConnectionPool
;
32 use TYPO3\CMS\Core\Database\Query\QueryBuilder
;
33 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction
;
34 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
;
35 use TYPO3\CMS\Core\Database\ReferenceIndex
;
36 use TYPO3\CMS\Core\DataHandling\DataHandler
;
37 use TYPO3\CMS\Core\Http\HtmlResponse
;
38 use TYPO3\CMS\Core\Http\RedirectResponse
;
39 use TYPO3\CMS\Core\Imaging\Icon
;
40 use TYPO3\CMS\Core\Messaging\FlashMessage
;
41 use TYPO3\CMS\Core\Messaging\FlashMessageService
;
42 use TYPO3\CMS\Core\Page\PageRenderer
;
43 use TYPO3\CMS\Core\Routing\SiteMatcher
;
44 use TYPO3\CMS\Core\Site\Entity\SiteLanguage
;
45 use TYPO3\CMS\Core\Type\Bitmask\Permission
;
46 use TYPO3\CMS\Core\Utility\GeneralUtility
;
47 use TYPO3\CMS\Core\Utility\HttpUtility
;
48 use TYPO3\CMS\Core\Utility\MathUtility
;
49 use TYPO3\CMS\Core\Utility\PathUtility
;
50 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher
;
51 use TYPO3\CMS\Frontend\Page\CacheHashCalculator
;
52 use TYPO3\CMS\Frontend\Page\PageRepository
;
55 * Main backend controller almost always used if some database record is edited in the backend.
57 * Main job of this controller is to evaluate and sanitize $request parameters,
58 * call the DataHandler if records should be created or updated and
59 * execute FormEngine for record rendering.
61 class EditDocumentController
63 protected const DOCUMENT_CLOSE_MODE_DEFAULT
= 0;
64 // works like DOCUMENT_CLOSE_MODE_DEFAULT
65 protected const DOCUMENT_CLOSE_MODE_REDIRECT
= 1;
66 protected const DOCUMENT_CLOSE_MODE_CLEAR_ALL
= 3;
67 protected const DOCUMENT_CLOSE_MODE_NO_REDIRECT
= 4;
70 * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
72 * @see \TYPO3\CMS\Backend\Utility\BackendUtility::editOnClick()
75 protected $editconf = [];
78 * Comma list of field names to edit. If specified, only those fields will be rendered.
79 * Otherwise all (available) fields in the record are shown according to the TCA type.
83 protected $columnsOnly;
86 * Default values for fields
88 * @var array|null [table][field]
93 * Array of values to force being set as hidden fields in FormEngine
95 * @var array|null [table][field]
97 protected $overrideVals;
100 * If set, this value will be set in $this->retUrl as "returnUrl", if not,
101 * $this->retUrl will link to dummy controller
105 protected $returnUrl;
108 * Prepared return URL. Contains the URL that we should return to from FormEngine if
109 * close button is clicked. Usually passed along as 'returnUrl', but falls back to
110 * "dummy" controller.
117 * Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
124 * If true, the processing of incoming data will be performed as if a save-button is pressed.
125 * Used in the forms as a hidden field which can be set through
126 * JavaScript if the form is somehow submitted by JavaScript.
133 * Main DataHandler datamap array
136 * @todo: Will be set protected later, still used by ConditionMatcher
137 * @internal Will be removed / protected in TYPO3 v10.0 without further notice
142 * Main DataHandler cmdmap array
149 * DataHandler 'mirror' input
156 * Boolean: If set, then the GET var "&id=" will be added to the
157 * retUrl string so that the NEW id of something is returned to the script calling the form.
161 protected $returnNewPageId = false;
164 * Updated values for backendUser->uc. Used for new inline records to mark them
165 * as expanded: uc[inlineView][...]
172 * ID for displaying the page in the frontend, "save and view"
176 protected $popViewId;
179 * Alternative URL for viewing the frontend pages.
186 * Alternative title for the document handler.
193 * If set, then no save & view button is printed
202 protected $perms_clause;
205 * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
209 protected $returnEditConf;
212 * Workspace used for the editing action.
216 protected $workspace;
219 * parse_url() of current requested URI, contains ['path'] and ['query'] parts.
223 protected $R_URL_parts;
226 * Contains $request query parameters. This array is the foundation for creating
227 * the R_URI internal var which becomes the url to which forms are submitted
231 protected $R_URL_getvars;
234 * Set to the URL of this script including variables which is needed to re-display the form.
246 * Is loaded with the "title" of the currently "open document"
247 * used for the open document toolbar
251 protected $storeTitle = '';
254 * Contains an array with key/value pairs of GET parameters needed to reach the
255 * current document displayed - used in the 'open documents' toolbar.
259 protected $storeArray;
262 * $this->storeArray imploded to url
269 * md5 hash of storeURL, used to identify a single open document in backend user uc
273 protected $storeUrlMd5;
276 * Backend user session data of this module
283 * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
284 * the various documents on the GET parameter list needed to open it. The values are
285 * arrays with 0,1,2 keys with information about the document (see compileStoreData()).
286 * The docHandler variable is stored in the $docDat session data, key "0".
290 protected $docHandler;
293 * Array of the elements to create edit forms for.
296 * @todo: Will be set protected later, still used by ConditionMatcher
297 * @internal Will be removed / protected in TYPO3 v10.0 without further notice
299 public $elementsData;
302 * Pointer to the first element in $elementsData
309 * Counter, used to count the number of errors (when users do not have edit permissions)
316 * Counter, used to count the number of new record forms displayed
323 * Is set to the pid value of the last shown record - thus indicating which page to
324 * show when clicking the SAVE/VIEW button
331 * Is set to additional parameters (like "&L=xxx") if the record supports it.
335 protected $viewId_addParams;
338 * @var FormResultCompiler
340 protected $formResultCompiler;
343 * Used internally to disable the storage of the document reference (eg. new records)
347 protected $dontStoreDocumentRef = 0;
350 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
352 protected $signalSlotDispatcher;
355 * Stores information needed to preview the currently saved record
359 protected $previewData = [];
362 * ModuleTemplate object
364 * @var ModuleTemplate
366 protected $moduleTemplate;
369 * Check if a record has been saved
373 protected $isSavedRecord;
376 * Check if a page in free translation mode
380 protected $isPageInFreeTranslationMode = false;
385 public function __construct()
387 $this->moduleTemplate
= GeneralUtility
::makeInstance(ModuleTemplate
::class);
388 $this->moduleTemplate
->setUiBlock(true);
389 // @todo Used by TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching
390 $GLOBALS['SOBE'] = $this;
391 $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
395 * Main dispatcher entry method registered as "record_edit" end point
397 * @param ServerRequestInterface $request the current request
398 * @return ResponseInterface the response with the content
400 public function mainAction(ServerRequestInterface
$request): ResponseInterface
402 // Unlock all locked records
403 BackendUtility
::lockRecords();
404 if ($response = $this->preInit($request)) {
408 // Process incoming data via DataHandler?
409 $parsedBody = $request->getParsedBody();
411 ||
isset($parsedBody['_savedok'])
412 ||
isset($parsedBody['_saveandclosedok'])
413 ||
isset($parsedBody['_savedokview'])
414 ||
isset($parsedBody['_savedoknew'])
415 ||
isset($parsedBody['_duplicatedoc'])
417 if ($response = $this->processData($request)) {
422 $this->init($request);
423 $this->main($request);
425 return new HtmlResponse($this->moduleTemplate
->renderContent());
429 * First initialization, always called, even before processData() executes DataHandler processing.
431 * @param ServerRequestInterface $request
432 * @return ResponseInterface Possible redirect response
434 protected function preInit(ServerRequestInterface
$request): ?ResponseInterface
436 if ($response = $this->localizationRedirect($request)) {
440 $parsedBody = $request->getParsedBody();
441 $queryParams = $request->getQueryParams();
443 $this->editconf
= $parsedBody['edit'] ??
$queryParams['edit'] ??
[];
444 $this->defVals
= $parsedBody['defVals'] ??
$queryParams['defVals'] ??
null;
445 $this->overrideVals
= $parsedBody['overrideVals'] ??
$queryParams['overrideVals'] ??
null;
446 $this->columnsOnly
= $parsedBody['columnsOnly'] ??
$queryParams['columnsOnly'] ??
null;
447 $this->returnUrl
= GeneralUtility
::sanitizeLocalUrl($parsedBody['returnUrl'] ??
$queryParams['returnUrl'] ??
null);
448 $this->closeDoc
= (int)($parsedBody['closeDoc'] ??
$queryParams['closeDoc'] ?? self
::DOCUMENT_CLOSE_MODE_DEFAULT
);
449 $this->doSave
= (bool)($parsedBody['doSave'] ??
$queryParams['doSave'] ??
false);
450 $this->returnEditConf
= (bool)($parsedBody['returnEditConf'] ??
$queryParams['returnEditConf'] ??
false);
451 $this->workspace
= $parsedBody['workspace'] ??
$queryParams['workspace'] ??
null;
452 $this->uc
= $parsedBody['uc'] ??
$queryParams['uc'] ??
null;
454 // Set overrideVals as default values if defVals does not exist.
456 if (!is_array($this->defVals
) && is_array($this->overrideVals
)) {
457 $this->defVals
= $this->overrideVals
;
460 // Set final return URL
461 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
462 $this->retUrl
= $this->returnUrl ?
: (string)$uriBuilder->buildUriFromRoute('dummy');
464 // Change $this->editconf if versioning applies to any of the records
465 $this->fixWSversioningInEditConf();
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
;
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];
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)) {
485 // Sets a temporary workspace, this request is based on
486 if ($this->workspace
!== null) {
487 $this->getBackendUser()->setTemporaryWorkspace($this->workspace
);
490 $this->emitFunctionAfterSignal('preInit', $request);
496 * Do processing of data, submitting it to DataHandler. May return a RedirectResponse
498 * @param ServerRequestInterface $request
499 * @return ResponseInterface|null
501 protected function processData(ServerRequestInterface
$request): ?ResponseInterface
503 $parsedBody = $request->getParsedBody();
504 $queryParams = $request->getQueryParams();
506 $beUser = $this->getBackendUser();
508 // Processing related GET / POST vars
509 $this->data
= $parsedBody['data'] ??
$queryParams['data'] ??
[];
510 $this->cmd
= $parsedBody['cmd'] ??
$queryParams['cmd'] ??
[];
511 $this->mirror
= $parsedBody['mirror'] ??
$queryParams['mirror'] ??
[];
512 $this->returnNewPageId
= (bool)($parsedBody['returnNewPageId'] ??
$queryParams['returnNewPageId'] ??
false);
514 // Only options related to $this->data submission are included here
515 $tce = GeneralUtility
::makeInstance(DataHandler
::class);
517 $tce->setControl($parsedBody['control'] ??
$queryParams['control'] ??
[]);
519 // Set default values specific for the user
520 $TCAdefaultOverride = $beUser->getTSConfig()['TCAdefaults.'] ??
null;
521 if (is_array($TCAdefaultOverride)) {
522 $tce->setDefaultsFromUserTS($TCAdefaultOverride);
525 if (isset($beUser->uc
['neverHideAtCopy']) && $beUser->uc
['neverHideAtCopy']) {
526 $tce->neverHideAtCopy
= 1;
528 // Load DataHandler with data
529 $tce->start($this->data
, $this->cmd
);
530 if (is_array($this->mirror
)) {
531 $tce->setMirror($this->mirror
);
534 // Perform the saving operation with DataHandler:
535 if ($this->doSave
=== true) {
536 // @todo: Make DataHandler understand UploadedFileInterface and submit $request->getUploadedFiles() instead of $_FILES here
537 $tce->process_uploads($_FILES);
538 $tce->process_datamap();
539 $tce->process_cmdmap();
541 // If pages are being edited, we set an instruction about updating the page tree after this operation.
542 if ($tce->pagetreeNeedsRefresh
543 && (isset($this->data
['pages']) ||
$beUser->workspace
!= 0 && !empty($this->data
))
545 BackendUtility
::setUpdateSignal('updatePageTree');
547 // If there was saved any new items, load them:
548 if (!empty($tce->substNEWwithIDs_table
)) {
549 // Save the expanded/collapsed states for new inline records, if any
550 FormEngineUtility
::updateInlineView($this->uc
, $tce);
552 foreach ($this->editconf
as $tableName => $tableCmds) {
553 $keys = array_keys($tce->substNEWwithIDs_table
, $tableName);
555 foreach ($keys as $key) {
556 $editId = $tce->substNEWwithIDs
[$key];
557 // Check if the $editId isn't a child record of an IRRE action
558 if (!(is_array($tce->newRelatedIDs
[$tableName])
559 && in_array($editId, $tce->newRelatedIDs
[$tableName]))
561 // Translate new id to the workspace version
562 if ($versionRec = BackendUtility
::getWorkspaceVersionOfRecord(
568 $editId = $versionRec['uid'];
570 $newEditConf[$tableName][$editId] = 'edit';
572 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
573 // Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
574 if ($tableName === 'pages'
575 && $this->retUrl
!= (string)$uriBuilder->buildUriFromRoute('dummy')
576 && $this->returnNewPageId
578 $this->retUrl
.= '&id=' . $tce->substNEWwithIDs
[$key];
582 $newEditConf[$tableName] = $tableCmds;
585 // Reset editconf if newEditConf has values
586 if (!empty($newEditConf)) {
587 $this->editconf
= $newEditConf;
589 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
590 $this->R_URL_getvars
['edit'] = $this->editconf
;
591 // Unset default values since we don't need them anymore.
592 unset($this->R_URL_getvars
['defVals']);
593 // Recompile the store* values since editconf changed
594 $this->compileStoreData();
596 // See if any records was auto-created as new versions?
597 if (!empty($tce->autoVersionIdMap
)) {
598 $this->fixWSversioningInEditConf($tce->autoVersionIdMap
);
600 // If a document is saved and a new one is created right after.
601 if (isset($parsedBody['_savedoknew']) && is_array($this->editconf
)) {
602 if ($redirect = $this->closeDocument(self
::DOCUMENT_CLOSE_MODE_NO_REDIRECT
, $request)) {
605 // Find the current table
606 reset($this->editconf
);
607 $nTable = key($this->editconf
);
608 // Finding the first id, getting the records pid+uid
609 reset($this->editconf
[$nTable]);
610 $nUid = key($this->editconf
[$nTable]);
611 $recordFields = 'pid,uid';
612 if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
613 $recordFields .= ',t3ver_oid';
615 $nRec = BackendUtility
::getRecord($nTable, $nUid, $recordFields);
616 // Determine insertion mode: 'top' is self-explaining,
617 // otherwise new elements are inserted after one using a negative uid
618 $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
619 // Setting a blank editconf array for a new record:
620 $this->editconf
= [];
621 // Determine related page ID for regular live context
622 if ($nRec['pid'] != -1) {
623 if ($insertRecordOnTop) {
624 $relatedPageId = $nRec['pid'];
626 $relatedPageId = -$nRec['uid'];
629 // Determine related page ID for workspace context
630 if ($insertRecordOnTop) {
631 // Fetch live version of workspace version since the pid value is always -1 in workspaces
632 $liveRecord = BackendUtility
::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
633 $relatedPageId = $liveRecord['pid'];
635 // Use uid of live version of workspace version
636 $relatedPageId = -$nRec['t3ver_oid'];
639 $this->editconf
[$nTable][$relatedPageId] = 'new';
640 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
641 $this->R_URL_getvars
['edit'] = $this->editconf
;
642 // Recompile the store* values since editconf changed...
643 $this->compileStoreData();
645 // If a document should be duplicated.
646 if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf
)) {
647 $this->closeDocument(self
::DOCUMENT_CLOSE_MODE_NO_REDIRECT
, $request);
648 // Find current table
649 reset($this->editconf
);
650 $nTable = key($this->editconf
);
651 // Find the first id, getting the records pid+uid
652 reset($this->editconf
[$nTable]);
653 $nUid = key($this->editconf
[$nTable]);
654 if (!MathUtility
::canBeInterpretedAsInteger($nUid)) {
655 $nUid = $tce->substNEWwithIDs
[$nUid];
658 $recordFields = 'pid,uid';
659 if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
660 $recordFields .= ',t3ver_oid';
662 $nRec = BackendUtility
::getRecord($nTable, $nUid, $recordFields);
664 // Setting a blank editconf array for a new record:
665 $this->editconf
= [];
667 if ($nRec['pid'] != -1) {
668 $relatedPageId = -$nRec['uid'];
670 $relatedPageId = -$nRec['t3ver_oid'];
673 /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $duplicateTce */
674 $duplicateTce = GeneralUtility
::makeInstance(DataHandler
::class);
679 'copy' => $relatedPageId
684 $duplicateTce->start([], $duplicateCmd);
685 $duplicateTce->process_cmdmap();
687 $duplicateMappingArray = $duplicateTce->copyMappingArray
;
688 $duplicateUid = $duplicateMappingArray[$nTable][$nUid];
690 if ($nTable === 'pages') {
691 BackendUtility
::setUpdateSignal('updatePageTree');
694 $this->editconf
[$nTable][$duplicateUid] = 'edit';
695 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
696 $this->R_URL_getvars
['edit'] = $this->editconf
;
697 // Recompile the store* values since editconf changed...
698 $this->compileStoreData();
700 // Inform the user of the duplication
701 $flashMessage = GeneralUtility
::makeInstance(
703 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
707 $flashMessageService = GeneralUtility
::makeInstance(FlashMessageService
::class);
708 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
709 $defaultFlashMessageQueue->enqueue($flashMessage);
711 // If a preview is requested
712 if (isset($parsedBody['_savedokview'])) {
713 // Get the first table and id of the data array from DataHandler
714 $table = reset(array_keys($this->data
));
715 $id = reset(array_keys($this->data
[$table]));
716 if (!MathUtility
::canBeInterpretedAsInteger($id)) {
717 $id = $tce->substNEWwithIDs
[$id];
719 // Store this information for later use
720 $this->previewData
['table'] = $table;
721 $this->previewData
['id'] = $id;
723 $tce->printLogErrorMessages();
725 if ((int)$this->closeDoc
< self
::DOCUMENT_CLOSE_MODE_DEFAULT
726 ||
isset($parsedBody['_saveandclosedok'])
728 // Redirect if element should be closed after save
729 return $this->closeDocument(abs($this->closeDoc
), $request);
735 * Initialize the view part of the controller logic.
737 * @param ServerRequestInterface $request
739 protected function init(ServerRequestInterface
$request): void
741 $parsedBody = $request->getParsedBody();
742 $queryParams = $request->getQueryParams();
744 $beUser = $this->getBackendUser();
746 $this->popViewId
= (int)($parsedBody['popViewId'] ??
$queryParams['popViewId'] ??
0);
747 $this->viewUrl
= (string)($parsedBody['viewUrl'] ??
$queryParams['viewUrl'] ??
'');
748 $this->recTitle
= (string)($parsedBody['recTitle'] ??
$queryParams['recTitle'] ??
'');
749 $this->noView
= (bool)($parsedBody['noView'] ??
$queryParams['noView'] ??
false);
750 $this->perms_clause
= $beUser->getPagePermsClause(Permission
::PAGE_SHOW
);
751 // Set other internal variables:
752 $this->R_URL_getvars
['returnUrl'] = $this->retUrl
;
753 $this->R_URI
= $this->R_URL_parts
['path'] . HttpUtility
::buildQueryString($this->R_URL_getvars
, '?');
755 $pageRenderer = GeneralUtility
::makeInstance(PageRenderer
::class);
756 $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
758 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
759 // override the default jumpToUrl
760 $this->moduleTemplate
->addJavaScriptCode(
763 function deleteRecord(table,id,url) {
764 window.location.href = ' . GeneralUtility
::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('tce_db') . '&cmd[') . '+table+"]["+id+"][delete]=1&redirect="+escape(url);
766 ' . (isset($parsedBody['_savedokview']) && $this->popViewId ?
$this->generatePreviewCode() : '')
768 // Set context sensitive menu
769 $this->moduleTemplate
->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
771 $this->emitFunctionAfterSignal('init', $request);
775 * Generate the Javascript for opening the preview window
779 protected function generatePreviewCode(): string
781 $previewPageId = $this->getPreviewPageId();
782 $previewPageRootLine = BackendUtility
::BEgetRootLine($previewPageId);
783 $anchorSection = $this->getPreviewUrlAnchorSection();
784 $previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
789 . BackendUtility
::viewOnClick(
792 $previewPageRootLine,
795 $previewUrlParameters,
801 . BackendUtility
::viewOnClick(
804 $previewPageRootLine,
807 $previewUrlParameters
814 * Returns the parameters for the preview URL
816 * @param int $previewPageId
819 protected function getPreviewUrlParameters(int $previewPageId): string
821 $linkParameters = [];
822 $table = $this->previewData
['table'] ?
: $this->firstEl
['table'];
823 $recordId = $this->previewData
['id'] ?
: $this->firstEl
['uid'];
824 $previewConfiguration = BackendUtility
::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ??
[];
825 $recordArray = BackendUtility
::getRecord($table, $recordId);
828 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ??
'';
829 if ($languageField && !empty($recordArray[$languageField])) {
830 $l18nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ??
'';
831 if ($l18nPointer && !empty($recordArray[$l18nPointer])
832 && isset($previewConfiguration['useDefaultLanguageRecord'])
833 && !$previewConfiguration['useDefaultLanguageRecord']
836 $recordId = $recordArray[$l18nPointer];
838 $language = $recordArray[$languageField];
840 $linkParameters['L'] = $language;
844 // map record data to GET parameters
845 if (isset($previewConfiguration['fieldToParameterMap.'])) {
846 foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
847 $value = $recordArray[$field];
848 if ($field === 'uid') {
851 $linkParameters[$parameterName] = $value;
855 // add/override parameters by configuration
856 if (isset($previewConfiguration['additionalGetParameters.'])) {
857 $additionalGetParameters = [];
858 $this->parseAdditionalGetParameters(
859 $additionalGetParameters,
860 $previewConfiguration['additionalGetParameters.']
862 $linkParameters = array_replace($linkParameters, $additionalGetParameters);
865 if (!empty($previewConfiguration['useCacheHash'])) {
866 $cacheHashCalculator = GeneralUtility
::makeInstance(CacheHashCalculator
::class);
867 $fullLinkParameters = HttpUtility
::buildQueryString(array_merge($linkParameters, ['id' => $previewPageId]), '&');
868 $cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters);
869 $linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters);
871 $linkParameters['no_cache'] = 1;
874 return HttpUtility
::buildQueryString($linkParameters, '&');
878 * Returns the anchor section for the preview url
882 protected function getPreviewUrlAnchorSection(): string
884 $table = $this->previewData
['table'] ?
: $this->firstEl
['table'];
885 $recordId = $this->previewData
['id'] ?
: $this->firstEl
['uid'];
887 return $table === 'tt_content' ?
'#c' . (int)$recordId : '';
891 * Returns the preview page id
895 protected function getPreviewPageId(): int
898 $table = $this->previewData
['table'] ?
: $this->firstEl
['table'];
899 $recordId = $this->previewData
['id'] ?
: $this->firstEl
['uid'];
900 $pageId = $this->popViewId ?
: $this->viewId
;
902 if ($table === 'pages') {
903 $currentPageId = (int)$recordId;
905 $currentPageId = MathUtility
::convertToPositiveInteger($pageId);
908 $previewConfiguration = BackendUtility
::getPagesTSconfig($currentPageId)['TCEMAIN.']['preview.'][$table . '.'] ??
[];
910 if (isset($previewConfiguration['previewPageId'])) {
911 $previewPageId = (int)$previewConfiguration['previewPageId'];
913 // if no preview page was configured
914 if (!$previewPageId) {
915 $rootPageData = null;
916 $rootLine = BackendUtility
::BEgetRootLine($currentPageId);
917 $currentPage = reset($rootLine);
918 // Allow all doktypes below 200
919 // This makes custom doktype work as well with opening a frontend page.
920 if ((int)$currentPage['doktype'] <= PageRepository
::DOKTYPE_SPACER
) {
921 // try the current page
922 $previewPageId = $currentPageId;
924 // or search for the root page
925 foreach ($rootLine as $page) {
926 if ($page['is_siteroot']) {
927 $rootPageData = $page;
931 $previewPageId = isset($rootPageData)
932 ?
(int)$rootPageData['uid']
937 $this->popViewId
= $previewPageId;
939 return $previewPageId;
943 * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
945 * This basically removes the trailing dots of sub-array keys in TypoScript.
946 * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
948 * @param array $parameters Should be an empty array by default
949 * @param array $typoScript The TypoScript configuration
951 protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
953 foreach ($typoScript as $key => $value) {
954 if (is_array($value)) {
955 $key = rtrim($key, '.');
956 $parameters[$key] = [];
957 $this->parseAdditionalGetParameters($parameters[$key], $value);
959 $parameters[$key] = $value;
965 * Main module operation
967 * @param ServerRequestInterface $request
969 protected function main(ServerRequestInterface
$request): void
973 if (is_array($this->editconf
)) {
974 $this->formResultCompiler
= GeneralUtility
::makeInstance(FormResultCompiler
::class);
976 // Creating the editing form, wrap it with buttons, document selector etc.
977 $editForm = $this->makeEditForm();
979 $this->firstEl
= reset($this->elementsData
);
980 // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
981 if (($this->docDat
[1] !== $this->storeUrlMd5 ||
!isset($this->docHandler
[$this->storeUrlMd5
]))
982 && !$this->dontStoreDocumentRef
984 $this->docHandler
[$this->storeUrlMd5
] = [
990 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler
, $this->storeUrlMd5
]);
991 BackendUtility
::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler
));
993 $body = $this->formResultCompiler
->addCssFiles();
994 $body .= $this->compileForm($editForm);
995 $body .= $this->formResultCompiler
->printNeededJSFunctions();
1000 // The page will show only if there is a valid page and if this page may be viewed by the user
1001 $this->pageinfo
= BackendUtility
::readPageAccess($this->viewId
, $this->perms_clause
);
1002 if ($this->pageinfo
) {
1003 $this->moduleTemplate
->getDocHeaderComponent()->setMetaInformation($this->pageinfo
);
1005 // Setting up the buttons and markers for doc header
1006 $this->getButtons($request);
1007 $this->languageSwitch(
1008 (string)($this->firstEl
['table'] ??
''),
1009 (int)($this->firstEl
['uid'] ??
0),
1010 isset($this->firstEl
['pid']) ?
(int)$this->firstEl
['pid'] : null
1012 $this->moduleTemplate
->setContent($body);
1016 * Creates the editing form with FormEngine, based on the input from GPvars.
1018 * @return string HTML form elements wrapped in tables
1020 protected function makeEditForm(): string
1022 // Initialize variables
1023 $this->elementsData
= [];
1027 $beUser = $this->getBackendUser();
1028 // Traverse the GPvar edit array tables
1029 foreach ($this->editconf
as $table => $conf) {
1030 if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
1031 // Traverse the keys/comments of each table (keys can be a comma list of uids)
1032 foreach ($conf as $cKey => $command) {
1033 if ($command === 'edit' ||
$command === 'new') {
1035 $ids = GeneralUtility
::trimExplode(',', $cKey, true);
1036 // Traverse the ids:
1037 foreach ($ids as $theUid) {
1038 // Don't save this document title in the document selector if the document is new.
1039 if ($command === 'new') {
1040 $this->dontStoreDocumentRef
= 1;
1044 $formDataGroup = GeneralUtility
::makeInstance(TcaDatabaseRecord
::class);
1045 $formDataCompiler = GeneralUtility
::makeInstance(FormDataCompiler
::class, $formDataGroup);
1046 $nodeFactory = GeneralUtility
::makeInstance(NodeFactory
::class);
1048 // Reset viewId - it should hold data of last entry only
1050 $this->viewId_addParams
= '';
1052 $formDataCompilerInput = [
1053 'tableName' => $table,
1054 'vanillaUid' => (int)$theUid,
1055 'command' => $command,
1056 'returnUrl' => $this->R_URI
,
1058 if (is_array($this->overrideVals
) && is_array($this->overrideVals
[$table])) {
1059 $formDataCompilerInput['overrideValues'] = $this->overrideVals
[$table];
1061 if (!empty($this->defVals
) && is_array($this->defVals
)) {
1062 $formDataCompilerInput['defaultValues'] = $this->defVals
;
1065 $formData = $formDataCompiler->compile($formDataCompilerInput);
1067 // Set this->viewId if possible
1068 if ($command === 'new'
1069 && $table !== 'pages'
1070 && !empty($formData['parentPageRow']['uid'])
1072 $this->viewId
= $formData['parentPageRow']['uid'];
1074 if ($table === 'pages') {
1075 $this->viewId
= $formData['databaseRow']['uid'];
1076 } elseif (!empty($formData['parentPageRow']['uid'])) {
1077 $this->viewId
= $formData['parentPageRow']['uid'];
1078 // Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
1079 if (!empty($formData['processedTca']['ctrl']['languageField'])
1080 && is_array($formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']])
1081 && $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0] > 0
1083 $this->viewId_addParams
= '&L=' . $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0];
1088 // Determine if delete button can be shown
1089 $deleteAccess = false;
1092 ||
$command === 'new'
1094 $permission = $formData['userPermissionOnPage'];
1095 if ($formData['tableName'] === 'pages') {
1096 $deleteAccess = $permission & Permission
::PAGE_DELETE ?
true : false;
1098 $deleteAccess = $permission & Permission
::CONTENT_EDIT ?
true : false;
1102 // Display "is-locked" message
1103 if ($command === 'edit') {
1104 $lockInfo = BackendUtility
::isRecordLocked($table, $formData['databaseRow']['uid']);
1106 $flashMessage = GeneralUtility
::makeInstance(
1107 FlashMessage
::class,
1110 FlashMessage
::WARNING
1112 $flashMessageService = GeneralUtility
::makeInstance(FlashMessageService
::class);
1113 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1114 $defaultFlashMessageQueue->enqueue($flashMessage);
1119 if (!$this->storeTitle
) {
1120 $this->storeTitle
= $this->recTitle
1121 ?
htmlspecialchars($this->recTitle
)
1122 : BackendUtility
::getRecordTitle($table, FormEngineUtility
::databaseRowCompatibility($formData['databaseRow']), true);
1125 $this->elementsData
[] = [
1127 'uid' => $formData['databaseRow']['uid'],
1128 'pid' => $formData['databaseRow']['pid'],
1130 'deleteAccess' => $deleteAccess
1133 if ($command !== 'new') {
1134 BackendUtility
::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ?
$formData['databaseRow']['pid'] : 0);
1137 // Set list if only specific fields should be rendered. This will trigger
1138 // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1139 if ($this->columnsOnly
) {
1140 if (is_array($this->columnsOnly
)) {
1141 $formData['fieldListToRender'] = $this->columnsOnly
[$table];
1143 $formData['fieldListToRender'] = $this->columnsOnly
;
1147 $formData['renderType'] = 'outerWrapContainer';
1148 $formResult = $nodeFactory->create($formData)->render();
1150 $html = $formResult['html'];
1152 $formResult['html'] = '';
1153 $formResult['doSaveFieldName'] = 'doSave';
1155 // @todo: Put all the stuff into FormEngine as final "compiler" class
1156 // @todo: This is done here for now to not rewrite addCssFiles()
1157 // @todo: and printNeededJSFunctions() now
1158 $this->formResultCompiler
->mergeResult($formResult);
1160 // Seems the pid is set as hidden field (again) at end?!
1161 if ($command === 'new') {
1162 // @todo: looks ugly
1164 . '<input type="hidden"'
1165 . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($formData['databaseRow']['uid']) . '][pid]"'
1166 . ' value="' . (int)$formData['databaseRow']['pid'] . '" />';
1171 } catch (AccessDeniedException
$e) {
1173 // Try to fetch error message from "recordInternals" be user object
1174 // @todo: This construct should be logged and localized and de-uglified
1175 $message = $beUser->errorMsg
;
1176 if (empty($message)) {
1177 // Create message from exception.
1178 $message = $e->getMessage() . ' ' . $e->getCode();
1180 $editForm .= htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission'))
1181 . '<br /><br />' . htmlspecialchars($message) . '<br /><br />';
1182 } catch (DatabaseRecordException
$e) {
1183 $editForm = '<div class="alert alert-warning">' . htmlspecialchars($e->getMessage()) . '</div>';
1185 } // End of for each uid
1194 * Create the panel of buttons for submitting the form or otherwise perform operations.
1196 * @param ServerRequestInterface $request
1198 protected function getButtons(ServerRequestInterface
$request): void
1200 $record = BackendUtility
::getRecord($this->firstEl
['table'], $this->firstEl
['uid']);
1201 $TCActrl = $GLOBALS['TCA'][$this->firstEl
['table']]['ctrl'];
1203 $this->setIsSavedRecord();
1205 $sysLanguageUid = 0;
1207 $this->isSavedRecord
1208 && isset($TCActrl['languageField'], $record[$TCActrl['languageField']])
1210 $sysLanguageUid = (int)$record[$TCActrl['languageField']];
1211 } elseif (isset($this->defVals
['sys_language_uid'])) {
1212 $sysLanguageUid = (int)$this->defVals
['sys_language_uid'];
1215 $l18nParent = isset($TCActrl['transOrigPointerField'], $record[$TCActrl['transOrigPointerField']])
1216 ?
(int)$record[$TCActrl['transOrigPointerField']]
1219 $this->setIsPageInFreeTranslationMode($record, $sysLanguageUid);
1221 $buttonBar = $this->moduleTemplate
->getDocHeaderComponent()->getButtonBar();
1223 $this->registerCloseButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_LEFT
, 1);
1225 // Show buttons when table is not read-only
1228 && !$GLOBALS['TCA'][$this->firstEl
['table']]['ctrl']['readOnly']
1230 $this->registerSaveButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_LEFT
, 2);
1231 $this->registerViewButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_LEFT
, 3);
1232 $this->registerNewButtonToButtonBar(
1234 ButtonBar
::BUTTON_POSITION_LEFT
,
1239 $this->registerDuplicationButtonToButtonBar(
1241 ButtonBar
::BUTTON_POSITION_LEFT
,
1246 $this->registerDeleteButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_LEFT
, 6);
1247 $this->registerColumnsOnlyButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_LEFT
, 7);
1248 $this->registerHistoryButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_RIGHT
, 1);
1251 $this->registerOpenInNewWindowButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_RIGHT
, 2);
1252 $this->registerShortcutButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_RIGHT
, 3);
1253 $this->registerCshButtonToButtonBar($buttonBar, ButtonBar
::BUTTON_POSITION_RIGHT
, 4);
1257 * Set the boolean to check if the record is saved
1259 protected function setIsSavedRecord()
1261 if (!is_bool($this->isSavedRecord
)) {
1262 $this->isSavedRecord
= (
1263 $this->firstEl
['cmd'] !== 'new'
1264 && MathUtility
::canBeInterpretedAsInteger($this->firstEl
['uid'])
1270 * Returns if inconsistent language handling is allowed
1274 protected function isInconsistentLanguageHandlingAllowed(): bool
1276 $allowInconsistentLanguageHandling = BackendUtility
::getPagesTSconfig(
1277 $this->pageinfo
['uid']
1278 )['mod']['web_layout']['allowInconsistentLanguageHandling'];
1280 return $allowInconsistentLanguageHandling['value'] === '1';
1284 * Set the boolean to check if the page is in free translation mode
1286 * @param array|null $record
1287 * @param int $sysLanguageUid
1289 protected function setIsPageInFreeTranslationMode($record, int $sysLanguageUid)
1291 if ($this->firstEl
['table'] === 'tt_content') {
1292 if (!$this->isSavedRecord
) {
1293 $this->isPageInFreeTranslationMode
= $this->getFreeTranslationMode(
1294 (int)$this->pageinfo
['uid'],
1295 (int)$this->defVals
['colPos'],
1299 $this->isPageInFreeTranslationMode
= $this->getFreeTranslationMode(
1300 (int)$this->pageinfo
['uid'],
1301 (int)$record['colPos'],
1309 * Check if the page is in free translation mode
1312 * @param int $column
1313 * @param int $language
1316 protected function getFreeTranslationMode(int $page, int $column, int $language): bool
1318 $freeTranslationMode = false;
1321 $this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0
1322 && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0
1324 $freeTranslationMode = true;
1327 return $freeTranslationMode;
1331 * Register the close button to the button bar
1333 * @param ButtonBar $buttonBar
1334 * @param string $position
1337 protected function registerCloseButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1339 $closeButton = $buttonBar->makeLinkButton()
1341 ->setClasses('t3js-editform-close')
1342 ->setTitle($this->getLanguageService()->sL(
1343 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'
1345 ->setShowLabelText(true)
1346 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon(
1351 $buttonBar->addButton($closeButton, $position, $group);
1355 * Register the save button to the button bar
1357 * @param ButtonBar $buttonBar
1358 * @param string $position
1361 protected function registerSaveButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1363 $saveButton = $buttonBar->makeInputButton()
1364 ->setForm('EditDocumentController')
1365 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon('actions-document-save', Icon
::SIZE_SMALL
))
1366 ->setName('_savedok')
1367 ->setShowLabelText(true)
1368 ->setTitle($this->getLanguageService()->sL(
1369 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'
1373 $buttonBar->addButton($saveButton, $position, $group);
1377 * Register the view button to the button bar
1379 * @param ButtonBar $buttonBar
1380 * @param string $position
1383 protected function registerViewButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1386 $this->viewId
// Pid to show the record
1387 && !$this->noView
// Passed parameter
1388 && !empty($this->firstEl
['table']) // No table
1390 // @TODO: TsConfig option should change to viewDoc
1391 && $this->getTsConfigOption($this->firstEl
['table'], 'saveDocView')
1393 $classNames = 't3js-editform-view';
1395 $pagesTSconfig = BackendUtility
::getPagesTSconfig($this->pageinfo
['uid']);
1397 if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1398 $excludeDokTypes = GeneralUtility
::intExplode(
1400 $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
1404 // exclude sysfolders, spacers and recycler by default
1405 $excludeDokTypes = [
1406 PageRepository
::DOKTYPE_RECYCLER
,
1407 PageRepository
::DOKTYPE_SYSFOLDER
,
1408 PageRepository
::DOKTYPE_SPACER
1413 !in_array((int)$this->pageinfo
['doktype'], $excludeDokTypes, true)
1414 ||
isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl
['table'] . '.']['previewPageId'])
1416 $previewPageId = $this->getPreviewPageId();
1417 $previewUrl = BackendUtility
::getPreviewUrl(
1420 BackendUtility
::BEgetRootLine($previewPageId),
1421 $this->getPreviewUrlAnchorSection(),
1423 $this->getPreviewUrlParameters($previewPageId)
1426 $viewButton = $buttonBar->makeLinkButton()
1427 ->setHref($previewUrl)
1428 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon(
1432 ->setShowLabelText(true)
1433 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc'));
1435 if (!$this->isSavedRecord
) {
1436 if ($this->firstEl
['table'] === 'pages') {
1437 $viewButton->setDataAttributes(['is-new' => '']);
1441 if ($classNames !== '') {
1442 $viewButton->setClasses($classNames);
1445 $buttonBar->addButton($viewButton, $position, $group);
1451 * Register the new button to the button bar
1453 * @param ButtonBar $buttonBar
1454 * @param string $position
1456 * @param int $sysLanguageUid
1457 * @param int $l18nParent
1459 protected function registerNewButtonToButtonBar(
1460 ButtonBar
$buttonBar,
1463 int $sysLanguageUid,
1467 $this->firstEl
['table'] !== 'sys_file_metadata'
1468 && !empty($this->firstEl
['table'])
1472 $this->isInconsistentLanguageHandlingAllowed()
1473 ||
$this->isPageInFreeTranslationMode
1475 && $this->firstEl
['table'] === 'tt_content'
1478 $this->firstEl
['table'] !== 'tt_content'
1480 $sysLanguageUid === 0
1481 ||
$l18nParent === 0
1486 $classNames = 't3js-editform-new';
1488 $newButton = $buttonBar->makeLinkButton()
1490 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon(
1494 ->setShowLabelText(true)
1495 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc'));
1497 if (!$this->isSavedRecord
) {
1498 $newButton->setDataAttributes(['is-new' => '']);
1501 if ($classNames !== '') {
1502 $newButton->setClasses($classNames);
1505 $buttonBar->addButton($newButton, $position, $group);
1510 * Register the duplication button to the button bar
1512 * @param ButtonBar $buttonBar
1513 * @param string $position
1515 * @param int $sysLanguageUid
1516 * @param int $l18nParent
1518 protected function registerDuplicationButtonToButtonBar(
1519 ButtonBar
$buttonBar,
1522 int $sysLanguageUid,
1526 $this->firstEl
['table'] !== 'sys_file_metadata'
1527 && !empty($this->firstEl
['table'])
1531 $this->isInconsistentLanguageHandlingAllowed()
1532 ||
$this->isPageInFreeTranslationMode
1534 && $this->firstEl
['table'] === 'tt_content'
1537 $this->firstEl
['table'] !== 'tt_content'
1539 $sysLanguageUid === 0
1540 ||
$l18nParent === 0
1544 && $this->getTsConfigOption($this->firstEl
['table'], 'showDuplicate')
1546 $classNames = 't3js-editform-duplicate';
1548 $duplicateButton = $buttonBar->makeLinkButton()
1550 ->setShowLabelText(true)
1551 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc'))
1552 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon(
1553 'actions-document-duplicates-select',
1557 if (!$this->isSavedRecord
) {
1558 $duplicateButton->setDataAttributes(['is-new' => '']);
1561 if ($classNames !== '') {
1562 $duplicateButton->setClasses($classNames);
1565 $buttonBar->addButton($duplicateButton, $position, $group);
1570 * Register the delete button to the button bar
1572 * @param ButtonBar $buttonBar
1573 * @param string $position
1576 protected function registerDeleteButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1579 $this->firstEl
['deleteAccess']
1580 && !$this->getDisableDelete()
1581 && $this->isSavedRecord
1582 && count($this->elementsData
) === 1
1584 $classNames = 't3js-editform-delete-record';
1586 $returnUrl = $this->retUrl
;
1587 if ($this->firstEl
['table'] === 'pages') {
1588 parse_str((string)parse_url($returnUrl, PHP_URL_QUERY
), $queryParams);
1590 isset($queryParams['route'], $queryParams['id'])
1591 && (string)$this->firstEl
['uid'] === (string)$queryParams['id']
1594 /** @var UriBuilder $uriBuilder */
1595 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
1597 // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
1598 // tree from the outside to be able to mark the pid as active
1599 $returnUrl = (string)$uriBuilder->buildUriFromRoutePath($queryParams['route'], ['id' => 0]);
1603 /** @var ReferenceIndex $referenceIndex */
1604 $referenceIndex = GeneralUtility
::makeInstance(ReferenceIndex
::class);
1605 $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords(
1606 $this->firstEl
['table'],
1607 (int)$this->firstEl
['uid']
1610 $referenceCountMessage = BackendUtility
::referenceCount(
1611 $this->firstEl
['table'],
1612 (int)$this->firstEl
['uid'],
1613 $this->getLanguageService()->sL(
1614 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'
1618 $translationCountMessage = BackendUtility
::translationCount(
1619 $this->firstEl
['table'],
1620 (int)$this->firstEl
['uid'],
1621 $this->getLanguageService()->sL(
1622 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'
1626 $deleteButton = $buttonBar->makeLinkButton()
1627 ->setClasses($classNames)
1628 ->setDataAttributes([
1629 'return-url' => $returnUrl,
1630 'uid' => $this->firstEl
['uid'],
1631 'table' => $this->firstEl
['table'],
1632 'reference-count-message' => $referenceCountMessage,
1633 'translation-count-message' => $translationCountMessage
1636 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon(
1637 'actions-edit-delete',
1640 ->setShowLabelText(true)
1641 ->setTitle($this->getLanguageService()->getLL('deleteItem'));
1643 $buttonBar->addButton($deleteButton, $position, $group);
1648 * Register the history button to the button bar
1650 * @param ButtonBar $buttonBar
1651 * @param string $position
1654 protected function registerHistoryButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1657 count($this->elementsData
) === 1
1658 && !empty($this->firstEl
['table'])
1659 && $this->getTsConfigOption($this->firstEl
['table'], 'showHistory')
1661 /** @var UriBuilder $uriBuilder */
1662 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
1664 $historyButtonOnClick = 'window.location.href=' .
1665 GeneralUtility
::quoteJSvalue(
1666 (string)$uriBuilder->buildUriFromRoute(
1669 'element' => $this->firstEl
['table'] . ':' . $this->firstEl
['uid'],
1670 'returnUrl' => $this->R_URI
,
1673 ) . '; return false;';
1675 $historyButton = $buttonBar->makeLinkButton()
1677 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon(
1678 'actions-document-history-open',
1681 ->setOnClick($historyButtonOnClick)
1682 ->setTitle('Open history of this record')
1685 $buttonBar->addButton($historyButton, $position, $group);
1690 * Register the columns only button to the button bar
1692 * @param ButtonBar $buttonBar
1693 * @param string $position
1696 protected function registerColumnsOnlyButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1700 && count($this->elementsData
) === 1
1702 $columnsOnlyButton = $buttonBar->makeLinkButton()
1703 ->setHref($this->R_URI
. '&columnsOnly=')
1704 ->setTitle($this->getLanguageService()->getLL('editWholeRecord'))
1705 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon(
1710 $buttonBar->addButton($columnsOnlyButton, $position, $group);
1715 * Register the open in new window button to the button bar
1717 * @param ButtonBar $buttonBar
1718 * @param string $position
1721 protected function registerOpenInNewWindowButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1723 $closeUrl = $this->getCloseUrl();
1724 if ($this->returnUrl
!== $closeUrl) {
1725 $requestUri = GeneralUtility
::linkThisScript([
1726 'returnUrl' => $closeUrl,
1728 $aOnClick = 'vHWin=window.open('
1729 . GeneralUtility
::quoteJSvalue($requestUri) . ','
1730 . GeneralUtility
::quoteJSvalue(md5($this->R_URI
))
1731 . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1733 $openInNewWindowButton = $this->moduleTemplate
->getDocHeaderComponent()->getButtonBar()
1736 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
1737 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon('actions-window-open', Icon
::SIZE_SMALL
))
1738 ->setOnClick($aOnClick);
1740 $buttonBar->addButton($openInNewWindowButton, $position, $group);
1745 * Register the shortcut button to the button bar
1747 * @param ButtonBar $buttonBar
1748 * @param string $position
1751 protected function registerShortcutButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1753 if ($this->returnUrl
!== $this->getCloseUrl()) {
1754 $shortCutButton = $this->moduleTemplate
->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
1755 $shortCutButton->setModuleName('xMOD_alt_doc.php')
1765 $buttonBar->addButton($shortCutButton, $position, $group);
1770 * Register the CSH button to the button bar
1772 * @param ButtonBar $buttonBar
1773 * @param string $position
1776 protected function registerCshButtonToButtonBar(ButtonBar
$buttonBar, string $position, int $group)
1778 $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('TCEforms');
1780 $buttonBar->addButton($cshButton, $position, $group);
1784 * Get the count of connected translated content elements
1787 * @param int $column
1788 * @param int $language
1791 protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int
1793 $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1795 return (int)$queryBuilder
1797 $queryBuilder->expr()->gt(
1798 $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1799 $queryBuilder->createNamedParameter(0, \PDO
::PARAM_INT
)
1807 * Get the count of standalone translated content elements
1810 * @param int $column
1811 * @param int $language
1814 protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int
1816 $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1818 return (int)$queryBuilder
1820 $queryBuilder->expr()->eq(
1821 $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1822 $queryBuilder->createNamedParameter(0, \PDO
::PARAM_INT
)
1830 * Get the query builder for the translation mode
1833 * @param int $column
1834 * @param int $language
1835 * @return QueryBuilder
1837 protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder
1839 $languageField = $GLOBALS['TCA']['tt_content']['ctrl']['languageField'];
1841 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)
1842 ->getQueryBuilderForTable('tt_content');
1844 $queryBuilder->getRestrictions()
1846 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class))
1847 ->add(GeneralUtility
::makeInstance(BackendWorkspaceRestriction
::class));
1849 return $queryBuilder
1851 ->from('tt_content')
1853 $queryBuilder->expr()->eq(
1855 $queryBuilder->createNamedParameter($page, \PDO
::PARAM_INT
)
1857 $queryBuilder->expr()->eq(
1859 $queryBuilder->createNamedParameter($language, \PDO
::PARAM_INT
)
1861 $queryBuilder->expr()->eq(
1863 $queryBuilder->createNamedParameter($column, \PDO
::PARAM_INT
)
1869 * Put together the various elements (buttons, selectors, form) into a table
1871 * @param string $editForm HTML form.
1872 * @return string Composite HTML
1874 protected function compileForm(string $editForm): string
1878 action="' . htmlspecialchars($this->R_URI
) . '"
1880 enctype="multipart/form-data"
1882 id="EditDocumentController"
1883 onsubmit="TBE_EDITOR.checkAndDoSubmit(1); return false;"
1886 <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl
) . '" />
1887 <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl
) . '" />
1888 <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$this->viewId
) . '" />
1889 <input type="hidden" name="closeDoc" value="0" />
1890 <input type="hidden" name="doSave" value="0" />
1891 <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
1892 <input type="hidden" name="_scrollPosition" value="" />';
1893 if ($this->returnNewPageId
) {
1894 $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
1896 if ($this->viewId_addParams
) {
1897 $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams
) . '" />';
1899 return $formContent;
1903 * Returns if delete for the current table is disabled by configuration.
1904 * For sys_file_metadata in default language delete is always disabled.
1908 protected function getDisableDelete(): bool
1910 $disableDelete = false;
1911 if ($this->firstEl
['table'] === 'sys_file_metadata') {
1912 $row = BackendUtility
::getRecord('sys_file_metadata', $this->firstEl
['uid'], 'sys_language_uid');
1913 $languageUid = $row['sys_language_uid'];
1914 if ($languageUid === 0) {
1915 $disableDelete = true;
1918 $disableDelete = (bool)$this->getTsConfigOption($this->firstEl
['table'] ??
'', 'disableDelete');
1920 return $disableDelete;
1924 * Returns the URL (usually for the "returnUrl") which closes the current window.
1925 * Used when editing a record in a popup.
1929 protected function getCloseUrl(): string
1931 $closeUrl = GeneralUtility
::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
1932 return PathUtility
::getAbsoluteWebPath($closeUrl);
1935 /***************************
1937 * Localization stuff
1939 ***************************/
1941 * Make selector box for creating new translation for a record or switching to edit the record in an existing
1943 * Displays only languages which are available for the current page.
1945 * @param string $table Table name
1946 * @param int $uid Uid for which to create a new language
1947 * @param int $pid|null Pid of the record
1949 protected function languageSwitch(string $table, int $uid, $pid = null)
1951 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1952 $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1953 /** @var UriBuilder $uriBuilder */
1954 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
1956 // Table editable and activated for languages?
1957 if ($this->getBackendUser()->check('tables_modify', $table)
1959 && $transOrigPointerField
1961 if ($pid === null) {
1962 $row = BackendUtility
::getRecord($table, $uid, 'pid');
1965 // Get all available languages for the page
1966 // If editing a page, the translations of the current UID need to be fetched
1967 if ($table === 'pages') {
1968 $row = BackendUtility
::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
1969 // Ensure the check is always done against the default language page
1970 $availableLanguages = $this->getLanguages(
1971 (int)$row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?
: $uid,
1975 $availableLanguages = $this->getLanguages((int)$pid, $table);
1977 // Page available in other languages than default language?
1978 if (count($availableLanguages) > 1) {
1980 $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
1981 // Get record in current language
1982 $rowCurrent = BackendUtility
::getLiveVersionOfRecord($table, $uid, $fetchFields);
1983 if (!is_array($rowCurrent)) {
1984 $rowCurrent = BackendUtility
::getRecord($table, $uid, $fetchFields);
1986 $currentLanguage = (int)$rowCurrent[$languageField];
1987 // Disabled for records with [all] language!
1988 if ($currentLanguage > -1) {
1989 // Get record in default language if needed
1990 if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
1991 $rowsByLang[0] = BackendUtility
::getLiveVersionOfRecord(
1993 $rowCurrent[$transOrigPointerField],
1996 if (!is_array($rowsByLang[0])) {
1997 $rowsByLang[0] = BackendUtility
::getRecord(
1999 $rowCurrent[$transOrigPointerField],
2004 $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
2006 if ($rowCurrent[$transOrigPointerField] ||
$currentLanguage === 0) {
2007 // Get record in other languages to see what's already available
2009 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)
2010 ->getQueryBuilderForTable($table);
2012 $queryBuilder->getRestrictions()
2014 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class))
2015 ->add(GeneralUtility
::makeInstance(BackendWorkspaceRestriction
::class));
2017 $result = $queryBuilder->select(...GeneralUtility
::trimExplode(',', $fetchFields, true))
2020 $queryBuilder->expr()->eq(
2022 $queryBuilder->createNamedParameter($pid, \PDO
::PARAM_INT
)
2024 $queryBuilder->expr()->gt(
2026 $queryBuilder->createNamedParameter(0, \PDO
::PARAM_INT
)
2028 $queryBuilder->expr()->eq(
2029 $transOrigPointerField,
2030 $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO
::PARAM_INT
)
2035 while ($row = $result->fetch()) {
2036 $rowsByLang[$row[$languageField]] = $row;
2039 $languageMenu = $this->moduleTemplate
->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
2040 $languageMenu->setIdentifier('_langSelector');
2041 foreach ($availableLanguages as $language) {
2042 $languageId = $language->getLanguageId();
2043 $selectorOptionLabel = $language->getTitle();
2044 // Create url for creating a localized record
2047 if (!isset($rowsByLang[$languageId])) {
2048 // Translation in this language does not exist
2049 $selectorOptionLabel .= ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
2050 $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2051 'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $languageId,
2052 'returnUrl' => $this->retUrl
2055 if (array_key_exists(0, $rowsByLang)) {
2056 $href = BackendUtility
::getLinkToDataHandlerAction(
2057 '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $languageId,
2065 'edit[' . $table . '][' . $rowsByLang[$languageId]['uid'] . ']' => 'edit',
2066 'returnUrl' => $this->retUrl
2068 if ($table === 'pages') {
2069 // Disallow manual adjustment of the language field for pages
2070 $params['overrideVals'] = [
2072 'sys_language_uid' => $languageId
2076 $href = (string)$uriBuilder->buildUriFromRoute('record_edit', $params);
2079 $menuItem = $languageMenu->makeMenuItem()
2080 ->setTitle($selectorOptionLabel)
2082 if ($languageId === $currentLanguage) {
2083 $menuItem->setActive(true);
2085 $languageMenu->addMenuItem($menuItem);
2088 $this->moduleTemplate
->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
2095 * Redirects to FormEngine with new parameters to edit a just created localized record
2097 * @param ServerRequestInterface $request Incoming request object
2098 * @return ResponseInterface|null Possible redirect response
2100 protected function localizationRedirect(ServerRequestInterface
$request): ?ResponseInterface
2102 $justLocalized = $request->getQueryParams()['justLocalized'];
2104 if (empty($justLocalized)) {
2108 list($table, $origUid, $language) = explode(':', $justLocalized);
2110 if ($GLOBALS['TCA'][$table]
2111 && $GLOBALS['TCA'][$table]['ctrl']['languageField']
2112 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
2114 $parsedBody = $request->getParsedBody();
2115 $queryParams = $request->getQueryParams();
2117 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable($table);
2118 $queryBuilder->getRestrictions()
2120 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class))
2121 ->add(GeneralUtility
::makeInstance(BackendWorkspaceRestriction
::class));
2122 $localizedRecord = $queryBuilder->select('uid')
2125 $queryBuilder->expr()->eq(
2126 $GLOBALS['TCA'][$table]['ctrl']['languageField'],
2127 $queryBuilder->createNamedParameter($language, \PDO
::PARAM_INT
)
2129 $queryBuilder->expr()->eq(
2130 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2131 $queryBuilder->createNamedParameter($origUid, \PDO
::PARAM_INT
)
2136 $returnUrl = $parsedBody['returnUrl'] ??
$queryParams['returnUrl'] ??
'';
2137 if (is_array($localizedRecord)) {
2138 // Create redirect response to self to edit just created record
2139 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
2140 return new RedirectResponse(
2141 (string)$uriBuilder->buildUriFromRoute(
2144 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2145 'returnUrl' => GeneralUtility
::sanitizeLocalUrl($returnUrl)
2156 * Returns languages available for record translations on given page.
2158 * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
2159 * hidden. If set to another value, the query will select all sys_language records that has a
2160 * translation record on that page (and is not hidden, unless you are admin user)
2161 * @param string $table For pages we want all languages, for other records the languages of the page translations
2162 * @return SiteLanguage[] Language
2164 protected function getLanguages(int $id, string $table): array
2166 // This usually happens when a non-pages record is added after another, so we are fetching the proper page ID
2167 if ($id < 0 && $table !== 'pages') {
2168 $pageId = $this->pageinfo
['uid'] ??
null;
2169 if ($pageId !== null) {
2170 $pageId = (int)$pageId;
2172 $fullRecord = BackendUtility
::getRecord($table, abs($id));
2173 $pageId = (int)$fullRecord['pid'];
2178 $site = GeneralUtility
::makeInstance(SiteMatcher
::class)->matchByPageId($pageId);
2180 // Fetch the current translations of this page, to only show the ones where there is a page translation
2181 $allLanguages = $site->getAvailableLanguages($this->getBackendUser(), false, $pageId);
2182 if ($table !== 'pages' && $id > 0) {
2183 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable('pages');
2184 $queryBuilder->getRestrictions()->removeAll()
2185 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class))
2186 ->add(GeneralUtility
::makeInstance(BackendWorkspaceRestriction
::class));
2187 $statement = $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
2190 $queryBuilder->expr()->eq(
2191 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2192 $queryBuilder->createNamedParameter($pageId, \PDO
::PARAM_INT
)
2197 $availableLanguages = [];
2199 if ($allLanguages[0] ??
false) {
2200 $availableLanguages = [
2201 0 => $allLanguages[0]
2205 while ($row = $statement->fetch()) {
2206 $languageId = (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
2207 if (isset($allLanguages[$languageId])) {
2208 $availableLanguages[$languageId] = $allLanguages[$languageId];
2211 return $availableLanguages;
2213 return $allLanguages;
2217 * Fix $this->editconf if versioning applies to any of the records
2219 * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
2221 protected function fixWSversioningInEditConf($mapArray = false): void
2223 // Traverse the editConf array
2224 if (is_array($this->editconf
)) {
2226 foreach ($this->editconf
as $table => $conf) {
2227 if (is_array($conf) && $GLOBALS['TCA'][$table]) {
2228 // Traverse the keys/comments of each table (keys can be a comma list of uids)
2230 foreach ($conf as $cKey => $cmd) {
2231 if ($cmd === 'edit') {
2232 // Traverse the ids:
2233 $ids = GeneralUtility
::trimExplode(',', $cKey, true);
2234 foreach ($ids as $idKey => $theUid) {
2235 if (is_array($mapArray)) {
2236 if ($mapArray[$table][$theUid]) {
2237 $ids[$idKey] = $mapArray[$table][$theUid];
2240 // Default, look for versions in workspace for record:
2241 $calcPRec = $this->getRecordForEdit((string)$table, (int)$theUid);
2242 if (is_array($calcPRec)) {
2243 // Setting UID again if it had changed, eg. due to workspace versioning.
2244 $ids[$idKey] = $calcPRec['uid'];
2248 // Add the possibly manipulated IDs to the new-build newConf array:
2249 $newConf[implode(',', $ids)] = $cmd;
2251 $newConf[$cKey] = $cmd;
2254 // Store the new conf array:
2255 $this->editconf
[$table] = $newConf;
2262 * Get record for editing.
2264 * @param string $table Table name
2265 * @param int $theUid Record UID
2266 * @return array|false Returns record to edit, false if none
2268 protected function getRecordForEdit(string $table, int $theUid)
2270 // Fetch requested record:
2271 $reqRecord = BackendUtility
::getRecord($table, $theUid, 'uid,pid');
2272 if (is_array($reqRecord)) {
2273 // If workspace is OFFLINE:
2274 if ($this->getBackendUser()->workspace
!= 0) {
2275 // Check for versioning support of the table:
2276 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
2277 // If the record is already a version of "something" pass it by.
2278 if ($reqRecord['pid'] == -1) {
2279 // (If it turns out not to be a version of the current workspace there will be trouble, but
2280 // that is handled inside DataHandler then and in the interface it would clearly be an error of
2281 // links if the user accesses such a scenario)
2284 // The input record was online and an offline version must be found or made:
2285 // Look for version of this workspace:
2286 $versionRec = BackendUtility
::getWorkspaceVersionOfRecord(
2287 $this->getBackendUser()->workspace
,
2292 return is_array($versionRec) ?
$versionRec : $reqRecord;
2294 // This means that editing cannot occur on this record because it was not supporting versioning
2295 // which is required inside an offline workspace.
2298 // In ONLINE workspace, just return the originally requested record:
2301 // Return FALSE because the table/uid was not found anyway.
2306 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2307 * to prepare 'open documents' urls
2309 protected function compileStoreData(): void
2311 // @todo: Refactor in TYPO3 v10.0: This GeneralUtility method fiddles with _GP()
2312 $this->storeArray
= GeneralUtility
::compileSelectedGetVarsFromArray(
2313 'edit,defVals,overrideVals,columnsOnly,noView,workspace',
2314 $this->R_URL_getvars
2316 $this->storeUrl
= HttpUtility
::buildQueryString($this->storeArray
, '&');
2317 $this->storeUrlMd5
= md5($this->storeUrl
);
2321 * Get a TSConfig 'option.' array, possibly for a specific table.
2323 * @param string $table Table name
2324 * @param string $key Options key
2327 protected function getTsConfigOption(string $table, string $key): string
2329 return \trim
((string)(
2330 $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table]
2331 ??
$this->getBackendUser()->getTSConfig()['options.'][$key]
2337 * Handling the closing of a document
2338 * The argument $mode can be one of this values:
2339 * - 0/1 will redirect to $this->retUrl [self::DOCUMENT_CLOSE_MODE_DEFAULT || self::DOCUMENT_CLOSE_MODE_REDIRECT]
2340 * - 3 will clear the docHandler (thus closing all documents) [self::DOCUMENT_CLOSE_MODE_CLEAR_ALL]
2341 * - 4 will do no redirect [self::DOCUMENT_CLOSE_MODE_NO_REDIRECT]
2342 * - other values will call setDocument with ->retUrl
2344 * @param int $mode the close mode: one of self::DOCUMENT_CLOSE_MODE_*
2345 * @param ServerRequestInterface $request Incoming request
2346 * @return ResponseInterface|null Redirect response if needed
2348 protected function closeDocument($mode, ServerRequestInterface
$request): ?ResponseInterface
2351 // If current document is found in docHandler,
2352 // then unset it, possibly unset it ALL and finally, write it to the session data
2353 if (isset($this->docHandler
[$this->storeUrlMd5
])) {
2354 // add the closing document to the recent documents
2355 $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
2356 if (!is_array($recentDocs)) {
2359 $closedDoc = $this->docHandler
[$this->storeUrlMd5
];
2360 $recentDocs = array_merge([$this->storeUrlMd5
=> $closedDoc], $recentDocs);
2361 if (count($recentDocs) > 8) {
2362 $recentDocs = array_slice($recentDocs, 0, 8);
2364 // remove it from the list of the open documents
2365 unset($this->docHandler
[$this->storeUrlMd5
]);
2366 if ($mode === self
::DOCUMENT_CLOSE_MODE_CLEAR_ALL
) {
2367 $recentDocs = array_merge($this->docHandler
, $recentDocs);
2368 $this->docHandler
= [];
2370 $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
2371 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler
, $this->docDat
[1]]);
2372 BackendUtility
::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler
));
2374 if ($mode === self
::DOCUMENT_CLOSE_MODE_NO_REDIRECT
) {
2377 $uriBuilder = GeneralUtility
::makeInstance(UriBuilder
::class);
2378 // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by
2379 // other scripts, like wizard_add, to know which records was created or so...
2380 if ($this->returnEditConf
&& $this->retUrl
!= (string)$uriBuilder->buildUriFromRoute('dummy')) {
2381 $this->retUrl
.= '&returnEditConf=' . rawurlencode(json_encode($this->editconf
));
2383 // If mode is NOT set (means 0) OR set to 1, then make a header location redirect to $this->retUrl
2384 if ($mode === self
::DOCUMENT_CLOSE_MODE_DEFAULT ||
$mode === self
::DOCUMENT_CLOSE_MODE_REDIRECT
) {
2385 return new RedirectResponse($this->retUrl
, 303);
2387 if ($this->retUrl
=== '') {
2390 $retUrl = $this->returnUrl
;
2391 if (is_array($this->docHandler
) && !empty($this->docHandler
)) {
2392 if (!empty($setupArr[2])) {
2393 $sParts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
2394 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2397 return new RedirectResponse($retUrl, 303);
2401 * Emits a signal after a function was executed
2403 * @param string $signalName
2404 * @param ServerRequestInterface $request
2406 protected function emitFunctionAfterSignal($signalName, ServerRequestInterface
$request): void
2408 $this->getSignalSlotDispatcher()->dispatch(__CLASS__
, $signalName . 'After', [$this, 'request' => $request]);
2412 * Get the SignalSlot dispatcher
2414 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
2416 protected function getSignalSlotDispatcher()
2418 if (!isset($this->signalSlotDispatcher
)) {
2419 $this->signalSlotDispatcher
= GeneralUtility
::makeInstance(Dispatcher
::class);
2421 return $this->signalSlotDispatcher
;
2425 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2427 protected function getBackendUser()
2429 return $GLOBALS['BE_USER'];
2433 * Returns LanguageService
2435 * @return \TYPO3\CMS\Core\Localization\LanguageService
2437 protected function getLanguageService()
2439 return $GLOBALS['LANG'];