'Using EditDocumentController::makeEditForm() is deprecated and will not be possible anymore in TYPO3 v10.0.',
'compileForm' => 'Using EditDocumentController::compileForm() is deprecated and will not be possible anymore in TYPO3 v10.0.',
'languageSwitch' => 'Using EditDocumentController::languageSwitch() is deprecated and will not be possible anymore in TYPO3 v10.0.',
'getLanguages' => 'Using EditDocumentController::getLanguages() is deprecated and will not be possible anymore in TYPO3 v10.0.',
'fixWSversioningInEditConf' => 'Using EditDocumentController::fixWSversioningInEditConf() is deprecated and will not be possible anymore in TYPO3 v10.0.',
'getRecordForEdit' => 'Using EditDocumentController::getRecordForEdit() is deprecated and will not be possible anymore in TYPO3 v10.0.',
];
/**
* Properties which have been moved to protected status from public
*
* @var array
*/
private $deprecatedPublicProperties = [
'editconf' => 'Using $editconf of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'defVals' => 'Using $defVals of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'overrideVals' => 'Using $overrideVals of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'columnsOnly' => 'Using $columnsOnly of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'returnUrl' => 'Using $returnUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'closeDoc' => 'Using $closeDoc of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'doSave' => 'Using $doSave of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'returnEditConf' => 'Using $returnEditConf of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'uc' => 'Using $uc of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'retUrl' => 'Using $retUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'R_URL_parts' => 'Using $R_URL_parts of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'R_URL_getvars' => 'Using $R_URL_getvars of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'storeArray' => 'Using $storeArray of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'storeUrl' => 'Using $storeUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'storeUrlMd5' => 'Using $storeUrlMd5 of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'docDat' => 'Using $docDat of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'docHandler' => 'Using $docHandler of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'cmd' => 'Using $cmd of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'mirror' => 'Using $mirror of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'cacheCmd' => 'Using $cacheCmd of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'redirect' => 'Using $redirect of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'returnNewPageId' => 'Using $returnNewPageId of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'popViewId' => 'Using $popViewId of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'popViewId_addParams' => 'Using $popViewId_addParams of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'viewUrl' => 'Using $viewUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'recTitle' => 'Using $recTitle of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'noView' => 'Using $noView of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'MCONF' => 'Using $MCONF of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'doc' => 'Using $doc of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'perms_clause' => 'Using $perms_clause of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'template' => 'Using $template of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'content' => 'Using $content of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'R_URI' => 'Using $R_URI of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'pageinfo' => 'Using $pageinfo of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'storeTitle' => 'Using $storeTitle of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'firstEl' => 'Using $firstEl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'errorC' => 'Using $errorC of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'newC' => 'Using $newC of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'viewId' => 'Using $viewId of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'viewId_addParams' => 'Using $viewId_addParams of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
'modTSconfig' => 'Using $modTSconfig of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
'dontStoreDocumentRef' => 'Using $dontStoreDocumentRef of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
];
/**
* An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
*
* @see \TYPO3\CMS\Backend\Utility\BackendUtility::editOnClick()
* @var array
*/
protected $editconf = [];
/**
* Comma list of field names to edit. If specified, only those fields will be rendered.
* Otherwise all (available) fields in the record are shown according to the TCA type.
*
* @var string|null
*/
protected $columnsOnly;
/**
* Default values for fields
*
* @var array|null [table][field]
*/
protected $defVals;
/**
* Array of values to force being set as hidden fields in FormEngine
*
* @var array|null [table][field]
*/
protected $overrideVals;
/**
* If set, this value will be set in $this->retUrl as "returnUrl", if not,
* $this->retUrl will link to dummy controller
*
* @var string|null
*/
protected $returnUrl;
/**
* Prepared return URL. Contains the URL that we should return to from FormEngine if
* close button is clicked. Usually passed along as 'returnUrl', but falls back to
* "dummy" controller.
*
* @var string
*/
protected $retUrl;
/**
* Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
*
* @var int
*/
protected $closeDoc;
/**
* If true, the processing of incoming data will be performed as if a save-button is pressed.
* Used in the forms as a hidden field which can be set through
* JavaScript if the form is somehow submitted by JavaScript.
*
* @var bool
*/
protected $doSave;
/**
* Main DataHandler datamap array
*
* @var array
* @todo: Will be set protected later, still used by ConditionMatcher
* @internal Will be removed / protected in TYPO3 v10.0 without further notice
*/
public $data;
/**
* Main DataHandler cmdmap array
*
* @var array
*/
protected $cmd;
/**
* DataHandler 'mirror' input
*
* @var array
*/
protected $mirror;
/**
* @var string
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $cacheCmd;
/**
* @var string
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $redirect;
/**
* Boolean: If set, then the GET var "&id=" will be added to the
* retUrl string so that the NEW id of something is returned to the script calling the form.
*
* @var bool
*/
protected $returnNewPageId = false;
/**
* Updated values for backendUser->uc. Used for new inline records to mark them
* as expanded: uc[inlineView][...]
*
* @var array|null
*/
protected $uc;
/**
* ID for displaying the page in the frontend, "save and view"
*
* @var int
*/
protected $popViewId;
/**
* @var string
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $popViewId_addParams;
/**
* Alternative URL for viewing the frontend pages.
*
* @var string
*/
protected $viewUrl;
/**
* Alternative title for the document handler.
*
* @var string
*/
protected $recTitle;
/**
* If set, then no save & view button is printed
*
* @var bool
*/
protected $noView;
/**
* @var string
*/
protected $perms_clause;
/**
* If true, $this->editconf array is added a redirect response, used by Wizard/AddController
*
* @var bool
*/
protected $returnEditConf;
/**
* Workspace used for the editing action.
*
* @var string|null
*/
protected $workspace;
/**
* @var \TYPO3\CMS\Backend\Template\DocumentTemplate
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $doc;
/**
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $template;
/**
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $content;
/**
* parse_url() of current requested URI, contains ['path'] and ['query'] parts.
*
* @var array
*/
protected $R_URL_parts;
/**
* Contains $request query parameters. This array is the foundation for creating
* the R_URI internal var which becomes the url to which forms are submitted
*
* @var array
*/
protected $R_URL_getvars;
/**
* Set to the URL of this script including variables which is needed to re-display the form.
*
* @var string
*/
protected $R_URI;
/**
* @var array
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $MCONF;
/**
* @var array
*/
protected $pageinfo;
/**
* Is loaded with the "title" of the currently "open document"
* used for the open document toolbar
*
* @var string
*/
protected $storeTitle = '';
/**
* Contains an array with key/value pairs of GET parameters needed to reach the
* current document displayed - used in the 'open documents' toolbar.
*
* @var array
*/
protected $storeArray;
/**
* $this->storeArray imploded to url
*
* @var string
*/
protected $storeUrl;
/**
* md5 hash of storeURL, used to identify a single open document in backend user uc
*
* @var string
*/
protected $storeUrlMd5;
/**
* Backend user session data of this module
*
* @var array
*/
protected $docDat;
/**
* An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
* the various documents on the GET parameter list needed to open it. The values are
* arrays with 0,1,2 keys with information about the document (see compileStoreData()).
* The docHandler variable is stored in the $docDat session data, key "0".
*
* @var array
*/
protected $docHandler;
/**
* Array of the elements to create edit forms for.
*
* @var array
* @todo: Will be set protected later, still used by ConditionMatcher
* @internal Will be removed / protected in TYPO3 v10.0 without further notice
*/
public $elementsData;
/**
* Pointer to the first element in $elementsData
*
* @var array
*/
protected $firstEl;
/**
* Counter, used to count the number of errors (when users do not have edit permissions)
*
* @var int
*/
protected $errorC;
/**
* Counter, used to count the number of new record forms displayed
*
* @var int
*/
protected $newC;
/**
* Is set to the pid value of the last shown record - thus indicating which page to
* show when clicking the SAVE/VIEW button
*
* @var int
*/
protected $viewId;
/**
* Is set to additional parameters (like "&L=xxx") if the record supports it.
*
* @var string
*/
protected $viewId_addParams;
/**
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
*/
protected $modTSconfig = [];
/**
* @var FormResultCompiler
*/
protected $formResultCompiler;
/**
* Used internally to disable the storage of the document reference (eg. new records)
*
* @var bool
*/
protected $dontStoreDocumentRef = 0;
/**
* @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
*/
protected $signalSlotDispatcher;
/**
* Stores information needed to preview the currently saved record
*
* @var array
*/
protected $previewData = [];
/**
* ModuleTemplate object
*
* @var ModuleTemplate
*/
protected $moduleTemplate;
/**
* Check if a record has been saved
*
* @var bool
*/
protected $isSavedRecord;
/**
* Check if a page in free translation mode
*
* @var bool
*/
protected $isPageInFreeTranslationMode = false;
/**
* @var UriBuilder
*/
protected $uriBuilder;
/**
* Constructor
*/
public function __construct()
{
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
$this->moduleTemplate->setUiBlock(true);
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
// @todo Used by TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching
$GLOBALS['SOBE'] = $this;
$this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
}
/**
* Main dispatcher entry method registered as "record_edit" end point
*
* @param ServerRequestInterface $request the current request
* @return ResponseInterface the response with the content
*/
public function mainAction(ServerRequestInterface $request): ResponseInterface
{
// Unlock all locked records
BackendUtility::lockRecords();
if ($response = $this->preInit($request)) {
return $response;
}
// Process incoming data via DataHandler?
$parsedBody = $request->getParsedBody();
if ($this->doSave
|| isset($parsedBody['_savedok'])
|| isset($parsedBody['_saveandclosedok'])
|| isset($parsedBody['_savedokview'])
|| isset($parsedBody['_savedoknew'])
|| isset($parsedBody['_duplicatedoc'])
) {
if ($response = $this->processData($request)) {
return $response;
}
}
$this->init($request);
$this->main($request);
return new HtmlResponse($this->moduleTemplate->renderContent());
}
/**
* First initialization, always called, even before processData() executes DataHandler processing.
*
* @param ServerRequestInterface $request
* @return ResponseInterface|null Possible redirect response
*/
public function preInit(ServerRequestInterface $request = null): ?ResponseInterface
{
if ($request === null) {
// Missing argument? This method must have been called from outside.
// Method will be protected and $request mandatory in TYPO3 v10.0, giving core freedom to move stuff around
// New v10 signature: "protected function preInit(ServerRequestInterface $request): ?ResponseInterface"
// @deprecated since TYPO3 v9, method argument $request will be set to mandatory
trigger_error('EditDocumentController->preInit() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
$request = $GLOBALS['TYPO3_REQUEST'];
}
if ($response = $this->localizationRedirect($request)) {
return $response;
}
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
$this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
$this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
$this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null);
$this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
$this->doSave = (bool)($parsedBody['doSave'] ?? $queryParams['doSave'] ?? false);
$this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
$this->workspace = $parsedBody['workspace'] ?? $queryParams['workspace'] ?? null;
$this->uc = $parsedBody['uc'] ?? $queryParams['uc'] ?? null;
// Set overrideVals as default values if defVals does not exist.
// @todo: Why?
if (!is_array($this->defVals) && is_array($this->overrideVals)) {
$this->defVals = $this->overrideVals;
}
$this->addSlugFieldsToColumnsOnly($queryParams);
// Set final return URL
$this->retUrl = $this->returnUrl ?: (string)$this->uriBuilder->buildUriFromRoute('dummy');
// Change $this->editconf if versioning applies to any of the records
$this->fixWSversioningInEditConf();
// Prepare R_URL (request url)
$this->R_URL_parts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
$this->R_URL_getvars = $queryParams;
$this->R_URL_getvars['edit'] = $this->editconf;
// Prepare 'open documents' url, this is later modified again various times
$this->compileStoreData();
// Backend user session data of this module
$this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
$this->docHandler = $this->docDat[0];
// Close document if a request for closing the document has been sent
if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
if ($response = $this->closeDocument($this->closeDoc, $request)) {
return $response;
}
}
// Sets a temporary workspace, this request is based on
if ($this->workspace !== null) {
$this->getBackendUser()->setTemporaryWorkspace($this->workspace);
}
$this->emitFunctionAfterSignal('preInit', $request);
return null;
}
/**
* Detects, if a save command has been triggered.
*
* @return bool TRUE, then save the document (data submitted)
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
public function doProcessData()
{
trigger_error('EditDocumentController->doProcessData() will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
$out = $this->doSave
|| isset($_POST['_savedok'])
|| isset($_POST['_saveandclosedok'])
|| isset($_POST['_savedokview'])
|| isset($_POST['_savedoknew'])
|| isset($_POST['_duplicatedoc']);
return $out;
}
/**
* Always add required fields of slug field
*
* @param array $queryParams
*/
protected function addSlugFieldsToColumnsOnly(array $queryParams): void
{
$data = $queryParams['edit'] ?? [];
$data = array_keys($data);
$table = reset($data);
if ($this->columnsOnly && $table !== false && isset($GLOBALS['TCA'][$table])) {
$fields = GeneralUtility::trimExplode(',', $this->columnsOnly, true);
foreach ($fields as $field) {
if (isset($GLOBALS['TCA'][$table]['columns'][$field]) && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'slug') {
foreach ($GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['fields'] as $fields) {
$this->columnsOnly .= ',' . (is_array($fields) ? implode(',', $fields) : $fields);
}
}
}
}
}
/**
* Do processing of data, submitting it to DataHandler. May return a RedirectResponse
*
* @param ServerRequestInterface $request
* @return ResponseInterface|null
*/
public function processData(ServerRequestInterface $request = null): ?ResponseInterface
{
// @deprecated Variable can be removed in TYPO3 v10.0
$deprecatedCaller = false;
if ($request === null) {
// Missing argument? This method must have been called from outside.
// Method will be protected and $request mandatory in TYPO3 v10.0, giving core freedom to move stuff around
// New v10 signature: "protected function processData(ServerRequestInterface $request): ?ResponseInterface"
// @deprecated since TYPO3 v9, method argument $request will be set to mandatory
trigger_error('EditDocumentController->processData() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
$request = $GLOBALS['TYPO3_REQUEST'];
$deprecatedCaller = true;
}
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$beUser = $this->getBackendUser();
// Processing related GET / POST vars
$this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];
$this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];
$this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];
// @deprecated property cacheCmd is unused and can be removed in TYPO3 v10.0
$this->cacheCmd = $parsedBody['cacheCmd'] ?? $queryParams['cacheCmd'] ?? null;
// @deprecated property redirect is unused and can be removed in TYPO3 v10.0
$this->redirect = $parsedBody['redirect'] ?? $queryParams['redirect'] ?? null;
$this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);
// Only options related to $this->data submission are included here
$tce = GeneralUtility::makeInstance(DataHandler::class);
$tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);
// Set internal vars
if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {
$tce->neverHideAtCopy = 1;
}
// Load DataHandler with data
$tce->start($this->data, $this->cmd);
if (is_array($this->mirror)) {
$tce->setMirror($this->mirror);
}
// Perform the saving operation with DataHandler:
if ($this->doSave === true) {
// @todo: Make DataHandler understand UploadedFileInterface and submit $request->getUploadedFiles() instead of $_FILES here
$tce->process_uploads($_FILES);
$tce->process_datamap();
$tce->process_cmdmap();
}
// If pages are being edited, we set an instruction about updating the page tree after this operation.
if ($tce->pagetreeNeedsRefresh
&& (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
) {
BackendUtility::setUpdateSignal('updatePageTree');
}
// If there was saved any new items, load them:
if (!empty($tce->substNEWwithIDs_table)) {
// Save the expanded/collapsed states for new inline records, if any
FormEngineUtility::updateInlineView($this->uc, $tce);
$newEditConf = [];
foreach ($this->editconf as $tableName => $tableCmds) {
$keys = array_keys($tce->substNEWwithIDs_table, $tableName);
if (!empty($keys)) {
foreach ($keys as $key) {
$editId = $tce->substNEWwithIDs[$key];
// Check if the $editId isn't a child record of an IRRE action
if (!(is_array($tce->newRelatedIDs[$tableName])
&& in_array($editId, $tce->newRelatedIDs[$tableName]))
) {
// Translate new id to the workspace version
if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
$beUser->workspace,
$tableName,
$editId,
'uid'
)) {
$editId = $versionRec['uid'];
}
$newEditConf[$tableName][$editId] = 'edit';
}
// Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
if ($tableName === 'pages'
&& $this->retUrl !== (string)$this->uriBuilder->buildUriFromRoute('dummy')
&& $this->retUrl !== $this->getCloseUrl()
&& $this->returnNewPageId
) {
$this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
}
}
} else {
$newEditConf[$tableName] = $tableCmds;
}
}
// Reset editconf if newEditConf has values
if (!empty($newEditConf)) {
$this->editconf = $newEditConf;
}
// Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
$this->R_URL_getvars['edit'] = $this->editconf;
// Unset default values since we don't need them anymore.
unset($this->R_URL_getvars['defVals']);
// Recompile the store* values since editconf changed
$this->compileStoreData();
}
// See if any records was auto-created as new versions?
if (!empty($tce->autoVersionIdMap)) {
$this->fixWSversioningInEditConf($tce->autoVersionIdMap);
}
// If a document is saved and a new one is created right after.
if (isset($parsedBody['_savedoknew']) && is_array($this->editconf)) {
if ($redirect = $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request)) {
return $redirect;
}
// Find the current table
reset($this->editconf);
$nTable = key($this->editconf);
// Finding the first id, getting the records pid+uid
reset($this->editconf[$nTable]);
$nUid = key($this->editconf[$nTable]);
$recordFields = 'pid,uid';
if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
$recordFields .= ',t3ver_oid';
}
$nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
// Determine insertion mode: 'top' is self-explaining,
// otherwise new elements are inserted after one using a negative uid
$insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
// Setting a blank editconf array for a new record:
$this->editconf = [];
// Determine related page ID for regular live context
if ($nRec['pid'] != -1) {
if ($insertRecordOnTop) {
$relatedPageId = $nRec['pid'];
} else {
$relatedPageId = -$nRec['uid'];
}
} else {
// Determine related page ID for workspace context
if ($insertRecordOnTop) {
// Fetch live version of workspace version since the pid value is always -1 in workspaces
$liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
$relatedPageId = $liveRecord['pid'];
} else {
// Use uid of live version of workspace version
$relatedPageId = -$nRec['t3ver_oid'];
}
}
$this->editconf[$nTable][$relatedPageId] = 'new';
// Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
$this->R_URL_getvars['edit'] = $this->editconf;
// Recompile the store* values since editconf changed...
$this->compileStoreData();
}
// If a document should be duplicated.
if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf)) {
$this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request);
// Find current table
reset($this->editconf);
$nTable = key($this->editconf);
// Find the first id, getting the records pid+uid
reset($this->editconf[$nTable]);
$nUid = key($this->editconf[$nTable]);
if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
$nUid = $tce->substNEWwithIDs[$nUid];
}
$recordFields = 'pid,uid';
if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
$recordFields .= ',t3ver_oid';
}
$nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
// Setting a blank editconf array for a new record:
$this->editconf = [];
if ($nRec['pid'] != -1) {
$relatedPageId = -$nRec['uid'];
} else {
$relatedPageId = -$nRec['t3ver_oid'];
}
/** @var \TYPO3\CMS\Core\DataHandling\DataHandler $duplicateTce */
$duplicateTce = GeneralUtility::makeInstance(DataHandler::class);
$duplicateCmd = [
$nTable => [
$nUid => [
'copy' => $relatedPageId
]
]
];
$duplicateTce->start([], $duplicateCmd);
$duplicateTce->process_cmdmap();
$duplicateMappingArray = $duplicateTce->copyMappingArray;
$duplicateUid = $duplicateMappingArray[$nTable][$nUid];
if ($nTable === 'pages') {
BackendUtility::setUpdateSignal('updatePageTree');
}
$this->editconf[$nTable][$duplicateUid] = 'edit';
// Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
$this->R_URL_getvars['edit'] = $this->editconf;
// Recompile the store* values since editconf changed...
$this->compileStoreData();
// Inform the user of the duplication
$flashMessage = GeneralUtility::makeInstance(
FlashMessage::class,
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
'',
FlashMessage::OK
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
// If a preview is requested
if (isset($parsedBody['_savedokview'])) {
// Get the first table and id of the data array from DataHandler
$table = reset(array_keys($this->data));
$id = reset(array_keys($this->data[$table]));
if (!MathUtility::canBeInterpretedAsInteger($id)) {
$id = $tce->substNEWwithIDs[$id];
}
// Store this information for later use
$this->previewData['table'] = $table;
$this->previewData['id'] = $id;
}
$tce->printLogErrorMessages();
if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
|| isset($parsedBody['_saveandclosedok'])
) {
// Redirect if element should be closed after save
$possibleRedirect = $this->closeDocument(abs($this->closeDoc), $request);
if ($deprecatedCaller && $possibleRedirect) {
// @deprecated fall back if method has been called from outside. This if can be removed in TYPO3 v10.0
HttpUtility::redirect($possibleRedirect->getHeaders()['location'][0]);
}
return $possibleRedirect;
}
return null;
}
/**
* Initialize the view part of the controller logic.
*
* @param ServerRequestInterface $request
*/
public function init(ServerRequestInterface $request = null): void
{
if ($request === null) {
// Missing argument? This method must have been called from outside.
// Method will be protected and $request mandatory in TYPO3 v10.0, giving core freedom to move stuff around
// New v10 signature: "protected function init(ServerRequestInterface $request): void
// @deprecated since TYPO3 v9, method argument $request will be set to mandatory
trigger_error('EditDocumentController->init() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
$request = $GLOBALS['TYPO3_REQUEST'];
}
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$beUser = $this->getBackendUser();
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused, remove call in TYPO3 v10.0
$this->popViewId_addParams = $parsedBody['popViewId_addParams'] ?? $queryParams['popViewId_addParams'] ?? '';
$this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);
$this->viewUrl = (string)($parsedBody['viewUrl'] ?? $queryParams['viewUrl'] ?? '');
$this->recTitle = (string)($parsedBody['recTitle'] ?? $queryParams['recTitle'] ?? '');
$this->noView = (bool)($parsedBody['noView'] ?? $queryParams['noView'] ?? false);
$this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
// Set other internal variables:
$this->R_URL_getvars['returnUrl'] = $this->retUrl;
$this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
$this->MCONF['name'] = 'xMOD_alt_doc.php';
// @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
$this->doc = $GLOBALS['TBE_TEMPLATE'];
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
// override the default jumpToUrl
$this->moduleTemplate->addJavaScriptCode(
'jumpToUrl',
'
// Info view:
function launchView(table,uid) {
console.warn(\'Calling launchView() has been deprecated in TYPO3 v9 and will be removed in TYPO3 v10.0\');
var thePreviewWindow = window.open(
' . GeneralUtility::quoteJSvalue((string)$this->uriBuilder->buildUriFromRoute('show_item') . '&table=') . ' + encodeURIComponent(table) + "&uid=" + encodeURIComponent(uid),
"ShowItem" + Math.random().toString(16).slice(2),
"height=300,width=410,status=0,menubar=0,resizable=0,location=0,directories=0,scrollbars=1,toolbar=0"
);
if (thePreviewWindow && thePreviewWindow.focus) {
thePreviewWindow.focus();
}
}
function deleteRecord(table,id,url) {
window.location.href = ' . GeneralUtility::quoteJSvalue((string)$this->uriBuilder->buildUriFromRoute('tce_db') . '&cmd[') . '+table+"]["+id+"][delete]=1&redirect="+escape(url);
}
' . (isset($parsedBody['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '')
);
// Set context sensitive menu
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
$this->emitFunctionAfterSignal('init', $request);
}
/**
* Generate the Javascript for opening the preview window
*
* @return string
*/
protected function generatePreviewCode(): string
{
$previewPageId = $this->getPreviewPageId();
$previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
$anchorSection = $this->getPreviewUrlAnchorSection();
$previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
return '
if (window.opener) {
'
. BackendUtility::viewOnClick(
$previewPageId,
'',
$previewPageRootLine,
$anchorSection,
$this->viewUrl,
$previewUrlParameters,
false
)
. '
} else {
'
. BackendUtility::viewOnClick(
$previewPageId,
'',
$previewPageRootLine,
$anchorSection,
$this->viewUrl,
$previewUrlParameters
)
. '
}';
}
/**
* Returns the parameters for the preview URL
*
* @param int $previewPageId
* @return string
*/
protected function getPreviewUrlParameters(int $previewPageId): string
{
$linkParameters = [];
$table = $this->previewData['table'] ?: $this->firstEl['table'];
$recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
$previewConfiguration = BackendUtility::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
$recordArray = BackendUtility::getRecord($table, $recordId);
// language handling
$languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
if ($languageField && !empty($recordArray[$languageField])) {
$recordId = $this->resolvePreviewRecordId($table, $recordArray, $previewConfiguration);
$language = $recordArray[$languageField];
if ($language > 0) {
$linkParameters['L'] = $language;
}
}
// Always use live workspace record uid for the preview
if (BackendUtility::isTableWorkspaceEnabled($table) && $recordArray['t3ver_oid'] > 0) {
$recordId = $recordArray['t3ver_oid'];
}
// map record data to GET parameters
if (isset($previewConfiguration['fieldToParameterMap.'])) {
foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
$value = $recordArray[$field];
if ($field === 'uid') {
$value = $recordId;
}
$linkParameters[$parameterName] = $value;
}
}
// add/override parameters by configuration
if (isset($previewConfiguration['additionalGetParameters.'])) {
$additionalGetParameters = [];
$this->parseAdditionalGetParameters(
$additionalGetParameters,
$previewConfiguration['additionalGetParameters.']
);
$linkParameters = array_replace($linkParameters, $additionalGetParameters);
}
if (!empty($previewConfiguration['useCacheHash'])) {
$cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
$fullLinkParameters = HttpUtility::buildQueryString(array_merge($linkParameters, ['id' => $previewPageId]), '&');
$cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters);
$linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters);
} elseif (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter'])) {
$linkParameters['no_cache'] = 1;
}
return HttpUtility::buildQueryString($linkParameters, '&');
}
/**
* @param string $table
* @param array $recordArray
* @param array $previewConfiguration
*
* @return int
*/
protected function resolvePreviewRecordId(string $table, array $recordArray, array $previewConfiguration): int
{
$l10nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
if ($l10nPointer
&& !empty($recordArray[$l10nPointer])
&& (
// not set -> default to true
!isset($previewConfiguration['useDefaultLanguageRecord'])
// or set -> use value
|| $previewConfiguration['useDefaultLanguageRecord']
)
) {
return (int)$recordArray[$l10nPointer];
}
return (int)$recordArray['uid'];
}
/**
* Returns the anchor section for the preview url
*
* @return string
*/
protected function getPreviewUrlAnchorSection(): string
{
$table = $this->previewData['table'] ?: $this->firstEl['table'];
$recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
return $table === 'tt_content' ? '#c' . (int)$recordId : '';
}
/**
* Returns the preview page id
*
* @return int
*/
protected function getPreviewPageId(): int
{
$previewPageId = 0;
$table = $this->previewData['table'] ?: $this->firstEl['table'];
$recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
$pageId = $this->popViewId ?: $this->viewId;
if ($table === 'pages') {
$currentPageId = (int)$recordId;
} else {
$currentPageId = MathUtility::convertToPositiveInteger($pageId);
}
$previewConfiguration = BackendUtility::getPagesTSconfig($currentPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
if (isset($previewConfiguration['previewPageId'])) {
$previewPageId = (int)$previewConfiguration['previewPageId'];
}
// if no preview page was configured
if (!$previewPageId) {
$rootPageData = null;
$rootLine = BackendUtility::BEgetRootLine($currentPageId);
$currentPage = reset($rootLine);
// Allow all doktypes below 200
// This makes custom doktype work as well with opening a frontend page.
if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) {
// try the current page
$previewPageId = $currentPageId;
} else {
// or search for the root page
foreach ($rootLine as $page) {
if ($page['is_siteroot']) {
$rootPageData = $page;
break;
}
}
$previewPageId = isset($rootPageData)
? (int)$rootPageData['uid']
: $currentPageId;
}
}
$this->popViewId = $previewPageId;
return $previewPageId;
}
/**
* Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
*
* This basically removes the trailing dots of sub-array keys in TypoScript.
* The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
*
* @param array $parameters Should be an empty array by default
* @param array $typoScript The TypoScript configuration
*/
protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
{
foreach ($typoScript as $key => $value) {
if (is_array($value)) {
$key = rtrim($key, '.');
$parameters[$key] = [];
$this->parseAdditionalGetParameters($parameters[$key], $value);
} else {
$parameters[$key] = $value;
}
}
}
/**
* Main module operation
*
* @param ServerRequestInterface $request
*/
public function main(ServerRequestInterface $request = null): void
{
if ($request === null) {
// Set method signature in TYPO3 v10.0 to: "protected function main(ServerRequestInterface $request): void"
trigger_error('EditDocumentController->main() will be set to protected in TYPO3 v10.0.', E_USER_DEPRECATED);
$request = $GLOBALS['TYPO3_REQUEST'];
}
$body = '';
// Begin edit
if (is_array($this->editconf)) {
$this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
// Creating the editing form, wrap it with buttons, document selector etc.
$editForm = $this->makeEditForm();
if ($editForm) {
$this->firstEl = reset($this->elementsData);
// Checking if the currently open document is stored in the list of "open documents" - if not, add it:
if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5]))
&& !$this->dontStoreDocumentRef
) {
$this->docHandler[$this->storeUrlMd5] = [
$this->storeTitle,
$this->storeArray,
$this->storeUrl,
$this->firstEl
];
$this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
}
$body = $this->formResultCompiler->addCssFiles();
$body .= $this->compileForm($editForm);
$body .= $this->formResultCompiler->printNeededJSFunctions();
$body .= '';
}
}
// Access check...
// The page will show only if there is a valid page and if this page may be viewed by the user
$this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
if ($this->pageinfo) {
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
}
// Setting up the buttons and markers for doc header
$this->getButtons($request);
$this->languageSwitch(
(string)($this->firstEl['table'] ?? ''),
(int)($this->firstEl['uid'] ?? 0),
isset($this->firstEl['pid']) ? (int)$this->firstEl['pid'] : null
);
$this->moduleTemplate->setContent($body);
}
/**
* Creates the editing form with FormEngine, based on the input from GPvars.
*
* @return string HTML form elements wrapped in tables
*/
protected function makeEditForm(): string
{
// Initialize variables
$this->elementsData = [];
$this->errorC = 0;
$this->newC = 0;
$editForm = '';
$beUser = $this->getBackendUser();
// Traverse the GPvar edit array tables
foreach ($this->editconf as $table => $conf) {
if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
// Traverse the keys/comments of each table (keys can be a comma list of uids)
foreach ($conf as $cKey => $command) {
if ($command === 'edit' || $command === 'new') {
// Get the ids:
$ids = GeneralUtility::trimExplode(',', $cKey, true);
// Traverse the ids:
foreach ($ids as $theUid) {
// Don't save this document title in the document selector if the document is new.
if ($command === 'new') {
$this->dontStoreDocumentRef = 1;
}
try {
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
// Reset viewId - it should hold data of last entry only
$this->viewId = 0;
$this->viewId_addParams = '';
$formDataCompilerInput = [
'tableName' => $table,
'vanillaUid' => (int)$theUid,
'command' => $command,
'returnUrl' => $this->R_URI,
];
if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
$formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
}
if (!empty($this->defVals) && is_array($this->defVals)) {
$formDataCompilerInput['defaultValues'] = $this->defVals;
}
$formData = $formDataCompiler->compile($formDataCompilerInput);
// Set this->viewId if possible
if ($command === 'new'
&& $table !== 'pages'
&& !empty($formData['parentPageRow']['uid'])
) {
$this->viewId = $formData['parentPageRow']['uid'];
} else {
if ($table === 'pages') {
$this->viewId = $formData['databaseRow']['uid'];
} elseif (!empty($formData['parentPageRow']['uid'])) {
$this->viewId = $formData['parentPageRow']['uid'];
// Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
if (!empty($formData['processedTca']['ctrl']['languageField'])
&& is_array($formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']])
&& $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0] > 0
) {
$this->viewId_addParams = '&L=' . $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0];
}
}
}
// Determine if delete button can be shown
$deleteAccess = false;
if (
$command === 'edit'
|| $command === 'new'
) {
$permission = $formData['userPermissionOnPage'];
if ($formData['tableName'] === 'pages') {
$deleteAccess = $permission & Permission::PAGE_DELETE ? true : false;
} else {
$deleteAccess = $permission & Permission::CONTENT_EDIT ? true : false;
}
}
// Display "is-locked" message
if ($command === 'edit') {
$lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
if ($lockInfo) {
$flashMessage = GeneralUtility::makeInstance(
FlashMessage::class,
$lockInfo['msg'],
'',
FlashMessage::WARNING
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
}
// Record title
if (!$this->storeTitle) {
$this->storeTitle = $this->recTitle
? htmlspecialchars($this->recTitle)
: BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($formData['databaseRow']), true);
}
$this->elementsData[] = [
'table' => $table,
'uid' => $formData['databaseRow']['uid'],
'pid' => $formData['databaseRow']['pid'],
'cmd' => $command,
'deleteAccess' => $deleteAccess
];
if ($command !== 'new') {
BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
}
// Set list if only specific fields should be rendered. This will trigger
// ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
if ($this->columnsOnly) {
if (is_array($this->columnsOnly)) {
$formData['fieldListToRender'] = $this->columnsOnly[$table];
} else {
$formData['fieldListToRender'] = $this->columnsOnly;
}
}
$formData['renderType'] = 'outerWrapContainer';
$formResult = $nodeFactory->create($formData)->render();
$html = $formResult['html'];
$formResult['html'] = '';
$formResult['doSaveFieldName'] = 'doSave';
// @todo: Put all the stuff into FormEngine as final "compiler" class
// @todo: This is done here for now to not rewrite addCssFiles()
// @todo: and printNeededJSFunctions() now
$this->formResultCompiler->mergeResult($formResult);
// Seems the pid is set as hidden field (again) at end?!
if ($command === 'new') {
// @todo: looks ugly
$html .= LF
. '';
$this->newC++;
}
$editForm .= $html;
} catch (AccessDeniedException $e) {
$this->errorC++;
// Try to fetch error message from "recordInternals" be user object
// @todo: This construct should be logged and localized and de-uglified
$message = $beUser->errorMsg;
if (empty($message)) {
// Create message from exception.
$message = $e->getMessage() . ' ' . $e->getCode();
}
$editForm .= htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission'))
. '
' . htmlspecialchars($message) . '
';
} catch (DatabaseRecordException $e) {
$editForm = '