[TASK] Use null coalescing operator where possible
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / EditDocumentController.php
1 <?php
2 namespace TYPO3\CMS\Backend\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
20 use TYPO3\CMS\Backend\Form\FormDataCompiler;
21 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
22 use TYPO3\CMS\Backend\Form\FormResultCompiler;
23 use TYPO3\CMS\Backend\Form\NodeFactory;
24 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
25 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
26 use TYPO3\CMS\Backend\Template\ModuleTemplate;
27 use TYPO3\CMS\Backend\Utility\BackendUtility;
28 use TYPO3\CMS\Core\Database\ConnectionPool;
29 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
30 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
31 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
32 use TYPO3\CMS\Core\DataHandling\DataHandler;
33 use TYPO3\CMS\Core\Imaging\Icon;
34 use TYPO3\CMS\Core\Messaging\FlashMessage;
35 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
36 use TYPO3\CMS\Core\Messaging\FlashMessageService;
37 use TYPO3\CMS\Core\Page\PageRenderer;
38 use TYPO3\CMS\Core\Type\Bitmask\Permission;
39 use TYPO3\CMS\Core\Utility\GeneralUtility;
40 use TYPO3\CMS\Core\Utility\HttpUtility;
41 use TYPO3\CMS\Core\Utility\MathUtility;
42 use TYPO3\CMS\Core\Utility\PathUtility;
43 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
44 use TYPO3\CMS\Frontend\Page\PageRepository;
45
46 /**
47 * Script Class: Drawing the editing form for editing records in TYPO3.
48 * Notice: It does NOT use tce_db.php to submit data to, rather it handles submissions itself
49 */
50 class EditDocumentController
51 {
52 const DOCUMENT_CLOSE_MODE_DEFAULT = 0;
53 const DOCUMENT_CLOSE_MODE_REDIRECT = 1; // works like DOCUMENT_CLOSE_MODE_DEFAULT
54 const DOCUMENT_CLOSE_MODE_CLEAR_ALL = 3;
55 const DOCUMENT_CLOSE_MODE_NO_REDIRECT = 4;
56
57 /**
58 * GPvar "edit": Is an array looking approx like [tablename][list-of-ids]=command, eg.
59 * "&edit[pages][123]=edit". See \TYPO3\CMS\Backend\Utility\BackendUtility::editOnClick(). Value can be seen
60 * modified internally (converting NEW keyword to id, workspace/versioning etc).
61 *
62 * @var array
63 */
64 public $editconf;
65
66 /**
67 * Commalist of fieldnames to edit. The point is IF you specify this list, only those
68 * fields will be rendered in the form. Otherwise all (available) fields in the record
69 * is shown according to the types configuration in $GLOBALS['TCA']
70 *
71 * @var bool
72 */
73 public $columnsOnly;
74
75 /**
76 * Default values for fields (array with tablenames, fields etc. as keys).
77 * Can be seen modified internally.
78 *
79 * @var array
80 */
81 public $defVals;
82
83 /**
84 * Array of values to force being set (as hidden fields). Will be set as $this->defVals
85 * IF defVals does not exist.
86 *
87 * @var array
88 */
89 public $overrideVals;
90
91 /**
92 * If set, this value will be set in $this->retUrl (which is used quite many places
93 * as the return URL). If not set, "dummy.php" will be set in $this->retUrl
94 *
95 * @var string
96 */
97 public $returnUrl;
98
99 /**
100 * Close-document command. Not really sure of all options...
101 *
102 * @var int
103 */
104 public $closeDoc;
105
106 /**
107 * Quite simply, if this variable is set, then the processing of incoming data will be performed
108 * as if a save-button is pressed. Used in the forms as a hidden field which can be set through
109 * JavaScript if the form is somehow submitted by JavaScript).
110 *
111 * @var bool
112 */
113 public $doSave;
114
115 /**
116 * The data array from which the data comes...
117 *
118 * @var array
119 */
120 public $data;
121
122 /**
123 * @var string
124 */
125 public $cmd;
126
127 /**
128 * @var array
129 */
130 public $mirror;
131
132 /**
133 * Clear-cache cmd.
134 *
135 * @var string
136 */
137 public $cacheCmd;
138
139 /**
140 * Redirect (not used???)
141 *
142 * @var string
143 */
144 public $redirect;
145
146 /**
147 * Boolean: If set, then the GET var "&id=" will be added to the
148 * retUrl string so that the NEW id of something is returned to the script calling the form.
149 *
150 * @var bool
151 */
152 public $returnNewPageId;
153
154 /**
155 * update BE_USER->uc
156 *
157 * @var array
158 */
159 public $uc;
160
161 /**
162 * ID for displaying the page in the frontend (used for SAVE/VIEW operations)
163 *
164 * @var int
165 */
166 public $popViewId;
167
168 /**
169 * Additional GET vars for the link, eg. "&L=xxx"
170 *
171 * @var string
172 */
173 public $popViewId_addParams;
174
175 /**
176 * Alternative URL for viewing the frontend pages.
177 *
178 * @var string
179 */
180 public $viewUrl;
181
182 /**
183 * Alternative title for the document handler.
184 *
185 * @var string
186 */
187 public $recTitle;
188
189 /**
190 * If set, then no SAVE/VIEW button is printed
191 *
192 * @var bool
193 */
194 public $noView;
195
196 /**
197 * @var string
198 */
199 public $perms_clause;
200
201 /**
202 * If set, the $this->editconf array is returned to the calling script
203 * (used by wizard_add.php for instance)
204 *
205 * @var bool
206 */
207 public $returnEditConf;
208
209 /**
210 * Workspace used for the editing action.
211 *
212 * @var int|null
213 */
214 protected $workspace;
215
216 /**
217 * document template object
218 *
219 * @var \TYPO3\CMS\Backend\Template\DocumentTemplate
220 */
221 public $doc;
222
223 /**
224 * a static HTML template, usually in templates/alt_doc.html
225 *
226 * @var string
227 */
228 public $template;
229
230 /**
231 * Content accumulation
232 *
233 * @var string
234 */
235 public $content;
236
237 /**
238 * Return URL script, processed. This contains the script (if any) that we should
239 * RETURN TO from the FormEngine script IF we press the close button. Thus this
240 * variable is normally passed along from the calling script so we can properly return if needed.
241 *
242 * @var string
243 */
244 public $retUrl;
245
246 /**
247 * Contains the parts of the REQUEST_URI (current url). By parts we mean the result of resolving
248 * REQUEST_URI (current url) by the parse_url() function. The result is an array where eg. "path"
249 * is the script path and "query" is the parameters...
250 *
251 * @var array
252 */
253 public $R_URL_parts;
254
255 /**
256 * Contains the current GET vars array; More specifically this array is the foundation for creating
257 * the R_URI internal var (which becomes the "url of this script" to which we submit the forms etc.)
258 *
259 * @var array
260 */
261 public $R_URL_getvars;
262
263 /**
264 * Set to the URL of this script including variables which is needed to re-display the form. See main()
265 *
266 * @var string
267 */
268 public $R_URI;
269
270 /**
271 * @var array
272 */
273 public $MCONF;
274
275 /**
276 * @var array
277 */
278 public $pageinfo;
279
280 /**
281 * Is loaded with the "title" of the currently "open document" - this is used in the
282 * Document Selector box. (see makeDocSel())
283 *
284 * @var string
285 */
286 public $storeTitle = '';
287
288 /**
289 * Contains an array with key/value pairs of GET parameters needed to reach the
290 * current document displayed - used in the Document Selector box. (see compileStoreDat())
291 *
292 * @var array
293 */
294 public $storeArray;
295
296 /**
297 * Contains storeArray, but imploded into a GET parameter string (see compileStoreDat())
298 *
299 * @var string
300 */
301 public $storeUrl;
302
303 /**
304 * Hashed value of storeURL (see compileStoreDat())
305 *
306 * @var string
307 */
308 public $storeUrlMd5;
309
310 /**
311 * Module session data
312 *
313 * @var array
314 */
315 public $docDat;
316
317 /**
318 * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
319 * the various documents on the GET parameter list needed to open it. The values are
320 * arrays with 0,1,2 keys with information about the document (see compileStoreDat()).
321 * The docHandler variable is stored in the $docDat session data, key "0".
322 *
323 * @var array
324 */
325 public $docHandler;
326
327 /**
328 * Array of the elements to create edit forms for.
329 *
330 * @var array
331 */
332 public $elementsData;
333
334 /**
335 * Pointer to the first element in $elementsData
336 *
337 * @var array
338 */
339 public $firstEl;
340
341 /**
342 * Counter, used to count the number of errors (when users do not have edit permissions)
343 *
344 * @var int
345 */
346 public $errorC;
347
348 /**
349 * Counter, used to count the number of new record forms displayed
350 *
351 * @var int
352 */
353 public $newC;
354
355 /**
356 * Is set to the pid value of the last shown record - thus indicating which page to
357 * show when clicking the SAVE/VIEW button
358 *
359 * @var int
360 */
361 public $viewId;
362
363 /**
364 * Is set to additional parameters (like "&L=xxx") if the record supports it.
365 *
366 * @var string
367 */
368 public $viewId_addParams;
369
370 /**
371 * Module TSconfig, loaded from main() based on the page id value of viewId
372 *
373 * @var array
374 */
375 public $modTSconfig;
376
377 /**
378 * @var FormResultCompiler
379 */
380 protected $formResultCompiler;
381
382 /**
383 * Used internally to disable the storage of the document reference (eg. new records)
384 *
385 * @var bool
386 */
387 public $dontStoreDocumentRef = 0;
388
389 /**
390 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
391 */
392 protected $signalSlotDispatcher;
393
394 /**
395 * Stores information needed to preview the currently saved record
396 *
397 * @var array
398 */
399 protected $previewData = [];
400
401 /**
402 * ModuleTemplate object
403 *
404 * @var ModuleTemplate
405 */
406 protected $moduleTemplate;
407
408 /**
409 * Constructor
410 */
411 public function __construct()
412 {
413 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
414 $this->moduleTemplate->setUiBlock(true);
415 $GLOBALS['SOBE'] = $this;
416 $this->getLanguageService()->includeLLFile('EXT:lang/Resources/Private/Language/locallang_alt_doc.xlf');
417 }
418
419 /**
420 * Get the SignalSlot dispatcher
421 *
422 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
423 */
424 protected function getSignalSlotDispatcher()
425 {
426 if (!isset($this->signalSlotDispatcher)) {
427 $this->signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
428 }
429 return $this->signalSlotDispatcher;
430 }
431
432 /**
433 * Emits a signal after a function was executed
434 *
435 * @param string $signalName
436 */
437 protected function emitFunctionAfterSignal($signalName)
438 {
439 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, $signalName . 'After', [$this]);
440 }
441
442 /**
443 * First initialization.
444 */
445 public function preInit()
446 {
447 if (GeneralUtility::_GP('justLocalized')) {
448 $this->localizationRedirect(GeneralUtility::_GP('justLocalized'));
449 }
450 // Setting GPvars:
451 $this->editconf = GeneralUtility::_GP('edit');
452 $this->defVals = GeneralUtility::_GP('defVals');
453 $this->overrideVals = GeneralUtility::_GP('overrideVals');
454 $this->columnsOnly = GeneralUtility::_GP('columnsOnly');
455 $this->returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
456 $this->closeDoc = (int)GeneralUtility::_GP('closeDoc');
457 $this->doSave = GeneralUtility::_GP('doSave');
458 $this->returnEditConf = GeneralUtility::_GP('returnEditConf');
459 $this->workspace = GeneralUtility::_GP('workspace');
460 $this->uc = GeneralUtility::_GP('uc');
461 // Setting override values as default if defVals does not exist.
462 if (!is_array($this->defVals) && is_array($this->overrideVals)) {
463 $this->defVals = $this->overrideVals;
464 }
465 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
466 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
467 // Setting return URL
468 $this->retUrl = $this->returnUrl ?: (string)$uriBuilder->buildUriFromRoute('dummy');
469 // Fix $this->editconf if versioning applies to any of the records
470 $this->fixWSversioningInEditConf();
471 // Make R_URL (request url) based on input GETvars:
472 $this->R_URL_parts = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
473 $this->R_URL_getvars = GeneralUtility::_GET();
474 $this->R_URL_getvars['edit'] = $this->editconf;
475 // MAKE url for storing
476 $this->compileStoreDat();
477 // Get session data for the module:
478 $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
479 $this->docHandler = $this->docDat[0];
480 // If a request for closing the document has been sent, act accordingly:
481 if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
482 $this->closeDocument($this->closeDoc);
483 }
484 // If NO vars are sent to the script, try to read first document:
485 // Added !is_array($this->editconf) because editConf must not be set either.
486 // Anyways I can't figure out when this situation here will apply...
487 if (is_array($this->R_URL_getvars) && count($this->R_URL_getvars) < 2 && !is_array($this->editconf)) {
488 $this->setDocument($this->docDat[1]);
489 }
490
491 // Sets a temporary workspace, this request is based on
492 if ($this->workspace !== null) {
493 $this->getBackendUser()->setTemporaryWorkspace($this->workspace);
494 }
495
496 $this->emitFunctionAfterSignal(__FUNCTION__);
497 }
498
499 /**
500 * Detects, if a save command has been triggered.
501 *
502 * @return bool TRUE, then save the document (data submitted)
503 */
504 public function doProcessData()
505 {
506 $out = $this->doSave
507 || isset($_POST['_savedok'])
508 || isset($_POST['_saveandclosedok'])
509 || isset($_POST['_savedokview'])
510 || isset($_POST['_savedoknew'])
511 || isset($_POST['_translation_savedok'])
512 || isset($_POST['_translation_savedokclear']);
513 return $out;
514 }
515
516 /**
517 * Do processing of data, submitting it to DataHandler.
518 */
519 public function processData()
520 {
521 $beUser = $this->getBackendUser();
522 // GPvars specifically for processing:
523 $control = GeneralUtility::_GP('control');
524 $this->data = GeneralUtility::_GP('data');
525 $this->cmd = GeneralUtility::_GP('cmd');
526 $this->mirror = GeneralUtility::_GP('mirror');
527 $this->cacheCmd = GeneralUtility::_GP('cacheCmd');
528 $this->redirect = GeneralUtility::_GP('redirect');
529 $this->returnNewPageId = GeneralUtility::_GP('returnNewPageId');
530 // See tce_db.php for relevate options here:
531 // Only options related to $this->data submission are included here.
532 /** @var $tce \TYPO3\CMS\Core\DataHandling\DataHandler */
533 $tce = GeneralUtility::makeInstance(DataHandler::class);
534
535 if (!empty($control)) {
536 $tce->setControl($control);
537 }
538 if (isset($_POST['_translation_savedok'])) {
539 $tce->updateModeL10NdiffData = 'FORCE_FFUPD';
540 }
541 if (isset($_POST['_translation_savedokclear'])) {
542 $tce->updateModeL10NdiffData = 'FORCE_FFUPD';
543 $tce->updateModeL10NdiffDataClear = true;
544 }
545 // Setting default values specific for the user:
546 $TCAdefaultOverride = $beUser->getTSConfigProp('TCAdefaults');
547 if (is_array($TCAdefaultOverride)) {
548 $tce->setDefaultsFromUserTS($TCAdefaultOverride);
549 }
550 // Setting internal vars:
551 if ($beUser->uc['neverHideAtCopy']) {
552 $tce->neverHideAtCopy = 1;
553 }
554 // Loading DataHandler with data:
555 $tce->start($this->data, $this->cmd);
556 if (is_array($this->mirror)) {
557 $tce->setMirror($this->mirror);
558 }
559 // Checking referer / executing
560 $refInfo = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
561 $httpHost = GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY');
562 if ($httpHost != $refInfo['host']
563 && !$GLOBALS['TYPO3_CONF_VARS']['SYS']['doNotCheckReferer']
564 ) {
565 $tce->log(
566 '',
567 0,
568 0,
569 0,
570 1,
571 'Referer host \'%s\' and server host \'%s\' did not match!',
572 1,
573 [$refInfo['host'], $httpHost]
574 );
575 debug('Error: Referer host did not match with server host.');
576 } else {
577 // Perform the saving operation with DataHandler:
578 $tce->process_uploads($_FILES);
579 $tce->process_datamap();
580 $tce->process_cmdmap();
581 // If pages are being edited, we set an instruction about updating the page tree after this operation.
582 if ($tce->pagetreeNeedsRefresh
583 && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
584 ) {
585 BackendUtility::setUpdateSignal('updatePageTree');
586 }
587 // If there was saved any new items, load them:
588 if (!empty($tce->substNEWwithIDs_table)) {
589 // save the expanded/collapsed states for new inline records, if any
590 FormEngineUtility::updateInlineView($this->uc, $tce);
591 $newEditConf = [];
592 foreach ($this->editconf as $tableName => $tableCmds) {
593 $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
594 if (!empty($keys)) {
595 foreach ($keys as $key) {
596 $editId = $tce->substNEWwithIDs[$key];
597 // Check if the $editId isn't a child record of an IRRE action
598 if (!(is_array($tce->newRelatedIDs[$tableName])
599 && in_array($editId, $tce->newRelatedIDs[$tableName]))
600 ) {
601 // Translate new id to the workspace version:
602 if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
603 $beUser->workspace,
604 $tableName,
605 $editId,
606 'uid'
607 )) {
608 $editId = $versionRec['uid'];
609 }
610 $newEditConf[$tableName][$editId] = 'edit';
611 }
612 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
613 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
614 // Traverse all new records and forge the content of ->editconf so we can continue to EDIT
615 // these records!
616 if ($tableName === 'pages'
617 && $this->retUrl != (string)$uriBuilder->buildUriFromRoute('dummy')
618 && $this->returnNewPageId
619 ) {
620 $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
621 }
622 }
623 } else {
624 $newEditConf[$tableName] = $tableCmds;
625 }
626 }
627 // Resetting editconf if newEditConf has values:
628 if (!empty($newEditConf)) {
629 $this->editconf = $newEditConf;
630 }
631 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
632 $this->R_URL_getvars['edit'] = $this->editconf;
633 // Unsetting default values since we don't need them anymore.
634 unset($this->R_URL_getvars['defVals']);
635 // Re-compile the store* values since editconf changed...
636 $this->compileStoreDat();
637 }
638 // See if any records was auto-created as new versions?
639 if (!empty($tce->autoVersionIdMap)) {
640 $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
641 }
642 // If a document is saved and a new one is created right after.
643 if (isset($_POST['_savedoknew']) && is_array($this->editconf)) {
644 $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT);
645 // Finding the current table:
646 reset($this->editconf);
647 $nTable = key($this->editconf);
648 // Finding the first id, getting the records pid+uid
649 reset($this->editconf[$nTable]);
650 $nUid = key($this->editconf[$nTable]);
651 $recordFields = 'pid,uid';
652 if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
653 $recordFields .= ',t3ver_oid';
654 }
655 $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
656 // Determine insertion mode ('top' is self-explaining,
657 // otherwise new elements are inserted after one using a negative uid)
658 $insertRecordOnTop = ($this->getNewIconMode($nTable) === 'top');
659 // Setting a blank editconf array for a new record:
660 $this->editconf = [];
661 // Determine related page ID for regular live context
662 if ($nRec['pid'] != -1) {
663 if ($insertRecordOnTop) {
664 $relatedPageId = $nRec['pid'];
665 } else {
666 $relatedPageId = -$nRec['uid'];
667 }
668 } else {
669 // Determine related page ID for workspace context
670 if ($insertRecordOnTop) {
671 // Fetch live version of workspace version since the pid value is always -1 in workspaces
672 $liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
673 $relatedPageId = $liveRecord['pid'];
674 } else {
675 // Use uid of live version of workspace version
676 $relatedPageId = -$nRec['t3ver_oid'];
677 }
678 }
679 $this->editconf[$nTable][$relatedPageId] = 'new';
680 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
681 $this->R_URL_getvars['edit'] = $this->editconf;
682 // Re-compile the store* values since editconf changed...
683 $this->compileStoreDat();
684 }
685 // If a preview is requested
686 if (isset($_POST['_savedokview'])) {
687 // Get the first table and id of the data array from DataHandler
688 $table = reset(array_keys($this->data));
689 $id = reset(array_keys($this->data[$table]));
690 if (!MathUtility::canBeInterpretedAsInteger($id)) {
691 $id = $tce->substNEWwithIDs[$id];
692 }
693 // Store this information for later use
694 $this->previewData['table'] = $table;
695 $this->previewData['id'] = $id;
696 }
697 $tce->printLogErrorMessages();
698 }
699 // || count($tce->substNEWwithIDs)... If any new items has been save, the document is CLOSED
700 // because if not, we just get that element re-listed as new. And we don't want that!
701 if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
702 || isset($_POST['_saveandclosedok'])
703 || isset($_POST['_translation_savedok'])
704 ) {
705 $this->closeDocument(abs($this->closeDoc));
706 }
707 }
708
709 /**
710 * Initialize the normal module operation
711 */
712 public function init()
713 {
714 $beUser = $this->getBackendUser();
715 // Setting more GPvars:
716 $this->popViewId = GeneralUtility::_GP('popViewId');
717 $this->popViewId_addParams = GeneralUtility::_GP('popViewId_addParams');
718 $this->viewUrl = GeneralUtility::_GP('viewUrl');
719 $this->recTitle = GeneralUtility::_GP('recTitle');
720 $this->noView = GeneralUtility::_GP('noView');
721 $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
722 // Set other internal variables:
723 $this->R_URL_getvars['returnUrl'] = $this->retUrl;
724 $this->R_URI = $this->R_URL_parts['path'] . '?' . ltrim(GeneralUtility::implodeArrayForUrl(
725 '',
726 $this->R_URL_getvars
727 ), '&');
728 // Setting virtual document name
729 $this->MCONF['name'] = 'xMOD_alt_doc.php';
730
731 // Create an instance of the document template object
732 $this->doc = $GLOBALS['TBE_TEMPLATE'];
733 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
734 $pageRenderer->addInlineLanguageLabelFile('EXT:lang/Resources/Private/Language/locallang_alt_doc.xlf');
735 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
736 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
737 // override the default jumpToUrl
738 $this->moduleTemplate->addJavaScriptCode(
739 'jumpToUrl',
740 '
741 function jumpToUrl(URL,formEl) {
742 if (!TBE_EDITOR.isFormChanged()) {
743 window.location.href = URL;
744 } else if (formEl && formEl.type=="checkbox") {
745 formEl.checked = formEl.checked ? 0 : 1;
746 }
747 }
748
749 // Info view:
750 function launchView(table,uid) {
751 var thePreviewWindow = window.open(
752 ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('show_item') . '&table=') . ' + encodeURIComponent(table) + "&uid=" + encodeURIComponent(uid),
753 "ShowItem" + Math.random().toString(16).slice(2),
754 "height=300,width=410,status=0,menubar=0,resizable=0,location=0,directories=0,scrollbars=1,toolbar=0"
755 );
756 if (thePreviewWindow && thePreviewWindow.focus) {
757 thePreviewWindow.focus();
758 }
759 }
760 function deleteRecord(table,id,url) {
761 window.location.href = ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('tce_db') . '&cmd[') . '+table+"]["+id+"][delete]=1&redirect="+escape(url);
762 }
763 ' . (isset($_POST['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '')
764 );
765 // Setting up the context sensitive menu:
766 $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
767
768 $this->emitFunctionAfterSignal(__FUNCTION__);
769 }
770
771 /**
772 * @return string
773 */
774 protected function generatePreviewCode()
775 {
776 $table = $this->previewData['table'];
777 $recordId = $this->previewData['id'];
778
779 if ($table === 'pages') {
780 $currentPageId = $recordId;
781 } else {
782 $currentPageId = MathUtility::convertToPositiveInteger($this->popViewId);
783 }
784
785 $pageTsConfig = BackendUtility::getPagesTSconfig($currentPageId);
786 $previewConfiguration = $pageTsConfig['TCEMAIN.']['preview.'][$table . '.'] ?? [];
787
788 $recordArray = BackendUtility::getRecord($table, $recordId);
789
790 // find the right preview page id
791 $previewPageId = 0;
792 if (isset($previewConfiguration['previewPageId'])) {
793 $previewPageId = $previewConfiguration['previewPageId'];
794 }
795 // if no preview page was configured
796 if (!$previewPageId) {
797 $rootPageData = null;
798 $rootLine = BackendUtility::BEgetRootLine($currentPageId);
799 $currentPage = reset($rootLine);
800 // Allow all doktypes below 200
801 // This makes custom doktype work as well with opening a frontend page.
802 if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) {
803 // try the current page
804 $previewPageId = $currentPageId;
805 } else {
806 // or search for the root page
807 foreach ($rootLine as $page) {
808 if ($page['is_siteroot']) {
809 $rootPageData = $page;
810 break;
811 }
812 }
813 $previewPageId = isset($rootPageData)
814 ? (int)$rootPageData['uid']
815 : $currentPageId;
816 }
817 }
818
819 $linkParameters = [
820 'no_cache' => 1,
821 ];
822
823 // language handling
824 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
825 if ($languageField && !empty($recordArray[$languageField])) {
826 $l18nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
827 if ($l18nPointer && !empty($recordArray[$l18nPointer])
828 && isset($previewConfiguration['useDefaultLanguageRecord'])
829 && !$previewConfiguration['useDefaultLanguageRecord']
830 ) {
831 // use parent record
832 $recordId = $recordArray[$l18nPointer];
833 }
834 $linkParameters['L'] = $recordArray[$languageField];
835 }
836
837 // map record data to GET parameters
838 if (isset($previewConfiguration['fieldToParameterMap.'])) {
839 foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
840 $value = $recordArray[$field];
841 if ($field === 'uid') {
842 $value = $recordId;
843 }
844 $linkParameters[$parameterName] = $value;
845 }
846 }
847
848 // add/override parameters by configuration
849 if (isset($previewConfiguration['additionalGetParameters.'])) {
850 $additionalGetParameters = [];
851 $this->parseAdditionalGetParameters(
852 $additionalGetParameters,
853 $previewConfiguration['additionalGetParameters.']
854 );
855 $linkParameters = array_replace($linkParameters, $additionalGetParameters);
856 }
857
858 // anchor with uid of content element]
859 $anchorSection = $table === 'tt_content' ? '#c' . $recordId : '';
860
861 $this->popViewId = $previewPageId;
862 $this->popViewId_addParams = GeneralUtility::implodeArrayForUrl('', $linkParameters, '', false, true);
863
864 $previewPageRootline = BackendUtility::BEgetRootLine($this->popViewId);
865 return '
866 if (window.opener) {
867 '
868 . BackendUtility::viewOnClick(
869 $this->popViewId,
870 '',
871 $previewPageRootline,
872 $anchorSection,
873 $this->viewUrl,
874 $this->popViewId_addParams,
875 false
876 )
877 . '
878 } else {
879 '
880 . BackendUtility::viewOnClick(
881 $this->popViewId,
882 '',
883 $previewPageRootline,
884 $anchorSection,
885 $this->viewUrl,
886 $this->popViewId_addParams
887 )
888 . '
889 }';
890 }
891
892 /**
893 * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
894 *
895 * This basically removes the trailing dots of sub-array keys in TypoScript.
896 * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
897 *
898 * @param array $parameters Should be an empty array by default
899 * @param array $typoScript The TypoScript configuration
900 */
901 protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
902 {
903 foreach ($typoScript as $key => $value) {
904 if (is_array($value)) {
905 $key = rtrim($key, '.');
906 $parameters[$key] = [];
907 $this->parseAdditionalGetParameters($parameters[$key], $value);
908 } else {
909 $parameters[$key] = $value;
910 }
911 }
912 }
913
914 /**
915 * Main module operation
916 */
917 public function main()
918 {
919 $body = '';
920 // Begin edit:
921 if (is_array($this->editconf)) {
922 /** @var FormResultCompiler formResultCompiler */
923 $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
924
925 // Creating the editing form, wrap it with buttons, document selector etc.
926 $editForm = $this->makeEditForm();
927 if ($editForm) {
928 $this->firstEl = reset($this->elementsData);
929 // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
930 if (($this->docDat[1] !== $this->storeUrlMd5
931 || !isset($this->docHandler[$this->storeUrlMd5]))
932 && !$this->dontStoreDocumentRef
933 ) {
934 $this->docHandler[$this->storeUrlMd5] = [
935 $this->storeTitle,
936 $this->storeArray,
937 $this->storeUrl,
938 $this->firstEl
939 ];
940 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
941 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
942 }
943 // Module configuration
944 $this->modTSconfig = $this->viewId ? BackendUtility::getModTSconfig(
945 $this->viewId,
946 'mod.xMOD_alt_doc'
947 ) : [];
948 $body = $this->formResultCompiler->addCssFiles();
949 $body .= $this->compileForm($editForm);
950 $body .= $this->formResultCompiler->printNeededJSFunctions();
951 $body .= '</form>';
952 }
953 }
954 // Access check...
955 // The page will show only if there is a valid page and if this page may be viewed by the user
956 $this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
957 if ($this->pageinfo) {
958 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
959 }
960 // Setting up the buttons and markers for docheader
961 $this->getButtons();
962 $this->languageSwitch($this->firstEl['table'], $this->firstEl['uid'], $this->firstEl['pid']);
963 $this->moduleTemplate->setContent($body);
964 }
965
966 /***************************
967 *
968 * Sub-content functions, rendering specific parts of the module content.
969 *
970 ***************************/
971 /**
972 * Creates the editing form with FormEnigne, based on the input from GPvars.
973 *
974 * @return string HTML form elements wrapped in tables
975 */
976 public function makeEditForm()
977 {
978 // Initialize variables:
979 $this->elementsData = [];
980 $this->errorC = 0;
981 $this->newC = 0;
982 $editForm = '';
983 $trData = null;
984 $beUser = $this->getBackendUser();
985 // Traverse the GPvar edit array
986 // Tables:
987 foreach ($this->editconf as $table => $conf) {
988 if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
989 // Traverse the keys/comments of each table (keys can be a commalist of uids)
990 foreach ($conf as $cKey => $command) {
991 if ($command === 'edit' || $command === 'new') {
992 // Get the ids:
993 $ids = GeneralUtility::trimExplode(',', $cKey, true);
994 // Traverse the ids:
995 foreach ($ids as $theUid) {
996 // Don't save this document title in the document selector if the document is new.
997 if ($command === 'new') {
998 $this->dontStoreDocumentRef = 1;
999 }
1000
1001 /** @var TcaDatabaseRecord $formDataGroup */
1002 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
1003 /** @var FormDataCompiler $formDataCompiler */
1004 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
1005 /** @var NodeFactory $nodeFactory */
1006 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
1007
1008 try {
1009 // Reset viewId - it should hold data of last entry only
1010 $this->viewId = 0;
1011 $this->viewId_addParams = '';
1012
1013 $formDataCompilerInput = [
1014 'tableName' => $table,
1015 'vanillaUid' => (int)$theUid,
1016 'command' => $command,
1017 'returnUrl' => $this->R_URI,
1018 ];
1019 if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
1020 $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
1021 }
1022
1023 $formData = $formDataCompiler->compile($formDataCompilerInput);
1024
1025 // Set this->viewId if possible
1026 if ($command === 'new'
1027 && $table !== 'pages'
1028 && !empty($formData['parentPageRow']['uid'])
1029 ) {
1030 $this->viewId = $formData['parentPageRow']['uid'];
1031 } else {
1032 if ($table === 'pages') {
1033 $this->viewId = $formData['databaseRow']['uid'];
1034 } elseif (!empty($formData['parentPageRow']['uid'])) {
1035 $this->viewId = $formData['parentPageRow']['uid'];
1036 // Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
1037 if (!empty($formData['processedTca']['ctrl']['languageField'])
1038 && is_array($formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']])
1039 && $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0] > 0
1040 ) {
1041 $this->viewId_addParams = '&L=' . $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0];
1042 }
1043 }
1044 }
1045
1046 // Determine if delete button can be shown
1047 $deleteAccess = false;
1048 if ($command === 'edit') {
1049 $permission = $formData['userPermissionOnPage'];
1050 if ($formData['tableName'] === 'pages') {
1051 $deleteAccess = $permission & Permission::PAGE_DELETE ? true : false;
1052 } else {
1053 $deleteAccess = $permission & Permission::CONTENT_EDIT ? true : false;
1054 }
1055 }
1056
1057 // Display "is-locked" message:
1058 if ($command === 'edit') {
1059 $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
1060 if ($lockInfo) {
1061 /** @var $flashMessage \TYPO3\CMS\Core\Messaging\FlashMessage */
1062 $flashMessage = GeneralUtility::makeInstance(
1063 FlashMessage::class,
1064 $lockInfo['msg'],
1065 '',
1066 FlashMessage::WARNING
1067 );
1068 /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
1069 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1070 /** @var $defaultFlashMessageQueue FlashMessageQueue */
1071 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1072 $defaultFlashMessageQueue->enqueue($flashMessage);
1073 }
1074 }
1075
1076 // Record title
1077 if (!$this->storeTitle) {
1078 $this->storeTitle = $this->recTitle
1079 ? htmlspecialchars($this->recTitle)
1080 : BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($formData['databaseRow']), true);
1081 }
1082
1083 $this->elementsData[] = [
1084 'table' => $table,
1085 'uid' => $formData['databaseRow']['uid'],
1086 'pid' => $formData['databaseRow']['pid'],
1087 'cmd' => $command,
1088 'deleteAccess' => $deleteAccess
1089 ];
1090
1091 if ($command !== 'new') {
1092 BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
1093 }
1094
1095 // Set list if only specific fields should be rendered. This will trigger
1096 // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1097 if ($this->columnsOnly) {
1098 if (is_array($this->columnsOnly)) {
1099 $formData['fieldListToRender'] = $this->columnsOnly[$table];
1100 } else {
1101 $formData['fieldListToRender'] = $this->columnsOnly;
1102 }
1103 }
1104
1105 $formData['renderType'] = 'outerWrapContainer';
1106 $formResult = $nodeFactory->create($formData)->render();
1107
1108 $html = $formResult['html'];
1109
1110 $formResult['html'] = '';
1111 $formResult['doSaveFieldName'] = 'doSave';
1112
1113 // @todo: Put all the stuff into FormEngine as final "compiler" class
1114 // @todo: This is done here for now to not rewrite addCssFiles()
1115 // @todo: and printNeededJSFunctions() now
1116 $this->formResultCompiler->mergeResult($formResult);
1117
1118 // Seems the pid is set as hidden field (again) at end?!
1119 if ($command === 'new') {
1120 // @todo: looks ugly
1121 $html .= LF
1122 . '<input type="hidden"'
1123 . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($formData['databaseRow']['uid']) . '][pid]"'
1124 . ' value="' . (int)$formData['databaseRow']['pid'] . '" />';
1125 $this->newC++;
1126 }
1127
1128 $editForm .= $html;
1129 } catch (AccessDeniedException $e) {
1130 $this->errorC++;
1131 // Try to fetch error message from "recordInternals" be user object
1132 // @todo: This construct should be logged and localized and de-uglified
1133 $message = $beUser->errorMsg;
1134 if (empty($message)) {
1135 // Create message from exception.
1136 $message = $e->getMessage() . ' ' . $e->getCode();
1137 }
1138 $editForm .= htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission'))
1139 . '<br /><br />' . htmlspecialchars($message) . '<br /><br />';
1140 }
1141 } // End of for each uid
1142 }
1143 }
1144 }
1145 }
1146 return $editForm;
1147 }
1148
1149 /**
1150 * Create the panel of buttons for submitting the form or otherwise perform operations.
1151 *
1152 * @return array All available buttons as an assoc. array
1153 */
1154 protected function getButtons()
1155 {
1156 $lang = $this->getLanguageService();
1157 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1158 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1159 // Render SAVE type buttons:
1160 // The action of each button is decided by its name attribute. (See doProcessData())
1161 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1162 if (!$this->errorC && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']) {
1163 $saveSplitButton = $buttonBar->makeSplitButton();
1164 // SAVE button:
1165 $saveButton = $buttonBar->makeInputButton()
1166 ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
1167 ->setName('_savedok')
1168 ->setValue('1')
1169 ->setForm('EditDocumentController')
1170 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL));
1171 $saveSplitButton->addItem($saveButton, true);
1172
1173 // SAVE / VIEW button:
1174 if ($this->viewId && !$this->noView && $this->getNewIconMode($this->firstEl['table'], 'saveDocView')) {
1175 $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
1176 if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1177 $excludeDokTypes = GeneralUtility::intExplode(
1178 ',',
1179 $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
1180 true
1181 );
1182 } else {
1183 // exclude sysfolders, spacers and recycler by default
1184 $excludeDokTypes = [
1185 PageRepository::DOKTYPE_RECYCLER,
1186 PageRepository::DOKTYPE_SYSFOLDER,
1187 PageRepository::DOKTYPE_SPACER
1188 ];
1189 }
1190 if (!in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)
1191 || isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl['table'] . '.']['previewPageId'])
1192 ) {
1193 $saveAndOpenButton = $buttonBar->makeInputButton()
1194 ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveDocShow'))
1195 ->setName('_savedokview')
1196 ->setValue('1')
1197 ->setForm('EditDocumentController')
1198 ->setOnClick("window.open('', 'newTYPO3frontendWindow');")
1199 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1200 'actions-document-save-view',
1201 Icon::SIZE_SMALL
1202 ));
1203 $saveSplitButton->addItem($saveAndOpenButton);
1204 }
1205 }
1206 // SAVE / NEW button:
1207 if (count($this->elementsData) === 1 && $this->getNewIconMode($this->firstEl['table'])) {
1208 $saveAndNewButton = $buttonBar->makeInputButton()
1209 ->setName('_savedoknew')
1210 ->setClasses('t3js-editform-submitButton')
1211 ->setValue('1')
1212 ->setForm('EditDocumentController')
1213 ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveNewDoc'))
1214 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1215 'actions-document-save-new',
1216 Icon::SIZE_SMALL
1217 ));
1218 $saveSplitButton->addItem($saveAndNewButton);
1219 }
1220 // SAVE / CLOSE
1221 $saveAndCloseButton = $buttonBar->makeInputButton()
1222 ->setName('_saveandclosedok')
1223 ->setClasses('t3js-editform-submitButton')
1224 ->setValue('1')
1225 ->setForm('EditDocumentController')
1226 ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveCloseDoc'))
1227 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1228 'actions-document-save-close',
1229 Icon::SIZE_SMALL
1230 ));
1231 $saveSplitButton->addItem($saveAndCloseButton);
1232 // FINISH TRANSLATION / SAVE / CLOSE
1233 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['explicitConfirmationOfTranslation']) {
1234 $saveTranslationButton = $buttonBar->makeInputButton()
1235 ->setName('_translation_savedok')
1236 ->setValue('1')
1237 ->setForm('EditDocumentController')
1238 ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.translationSaveDoc'))
1239 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1240 'actions-document-save-cleartranslationcache',
1241 Icon::SIZE_SMALL
1242 ));
1243 $saveSplitButton->addItem($saveTranslationButton);
1244 $saveAndClearTranslationButton = $buttonBar->makeInputButton()
1245 ->setName('_translation_savedokclear')
1246 ->setValue('1')
1247 ->setForm('EditDocumentController')
1248 ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.translationSaveDocClear'))
1249 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1250 'actions-document-save-cleartranslationcache',
1251 Icon::SIZE_SMALL
1252 ));
1253 $saveSplitButton->addItem($saveAndClearTranslationButton);
1254 }
1255 $buttonBar->addButton($saveSplitButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1256 }
1257 // CLOSE button:
1258 $closeButton = $buttonBar->makeLinkButton()
1259 ->setHref('#')
1260 ->setClasses('t3js-editform-close')
1261 ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
1262 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1263 'actions-close',
1264 Icon::SIZE_SMALL
1265 ));
1266 $buttonBar->addButton($closeButton);
1267 // DELETE + UNDO buttons:
1268 if (!$this->errorC
1269 && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']
1270 && count($this->elementsData) === 1
1271 ) {
1272 if ($this->firstEl['cmd'] !== 'new' && MathUtility::canBeInterpretedAsInteger($this->firstEl['uid'])) {
1273 // Delete:
1274 if ($this->firstEl['deleteAccess']
1275 && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']
1276 && !$this->getNewIconMode($this->firstEl['table'], 'disableDelete')
1277 ) {
1278 $returnUrl = $this->retUrl;
1279 if ($this->firstEl['table'] === 'pages') {
1280 parse_str((string)parse_url($returnUrl, PHP_URL_QUERY), $queryParams);
1281 if (isset($queryParams['route'])
1282 && isset($queryParams['id'])
1283 && (string)$this->firstEl['uid'] === (string)$queryParams['id']
1284 ) {
1285 // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
1286 // tree from the outside to be able to mark the pid as active
1287 $returnUrl = (string)$uriBuilder->buildUriFromRoutePath($queryParams['route'], ['id' => 0]);
1288 }
1289 }
1290 $deleteButton = $buttonBar->makeLinkButton()
1291 ->setHref('#')
1292 ->setClasses('t3js-editform-delete-record')
1293 ->setTitle($lang->getLL('deleteItem'))
1294 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1295 'actions-edit-delete',
1296 Icon::SIZE_SMALL
1297 ))
1298 ->setDataAttributes([
1299 'return-url' => $returnUrl,
1300 'uid' => $this->firstEl['uid'],
1301 'table' => $this->firstEl['table']
1302 ]);
1303 $buttonBar->addButton($deleteButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
1304 }
1305 // Undo:
1306 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1307 ->getQueryBuilderForTable('sys_history');
1308
1309 $undoButtonR = $queryBuilder->select('tstamp')
1310 ->from('sys_history')
1311 ->where(
1312 $queryBuilder->expr()->eq(
1313 'tablename',
1314 $queryBuilder->createNamedParameter($this->firstEl['table'], \PDO::PARAM_STR)
1315 ),
1316 $queryBuilder->expr()->eq(
1317 'recuid',
1318 $queryBuilder->createNamedParameter($this->firstEl['uid'], \PDO::PARAM_INT)
1319 )
1320 )
1321 ->orderBy('tstamp', 'DESC')
1322 ->setMaxResults(1)
1323 ->execute()
1324 ->fetch();
1325
1326 if ($undoButtonR !== false) {
1327 $aOnClick = 'window.location.href=' .
1328 GeneralUtility::quoteJSvalue(
1329 (string)$uriBuilder->buildUriFromRoute(
1330 'record_history',
1331 [
1332 'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1333 'revert' => 'ALL_FIELDS',
1334 'returnUrl' => $this->R_URI,
1335 ]
1336 )
1337 ) . '; return false;';
1338
1339 $undoButton = $buttonBar->makeLinkButton()
1340 ->setHref('#')
1341 ->setOnClick($aOnClick)
1342 ->setTitle(
1343 sprintf(
1344 $lang->getLL('undoLastChange'),
1345 BackendUtility::calcAge(
1346 ($GLOBALS['EXEC_TIME'] - $undoButtonR['tstamp']),
1347 $lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
1348 )
1349 )
1350 )
1351 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1352 'actions-document-history-open',
1353 Icon::SIZE_SMALL
1354 ));
1355 $buttonBar->addButton($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
1356 }
1357 if ($this->getNewIconMode($this->firstEl['table'], 'showHistory')) {
1358 $aOnClick = 'window.location.href=' .
1359 GeneralUtility::quoteJSvalue(
1360 (string)$uriBuilder->buildUriFromRoute(
1361 'record_history',
1362 [
1363 'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1364 'returnUrl' => $this->R_URI,
1365 ]
1366 )
1367 ) . '; return false;';
1368
1369 $historyButton = $buttonBar->makeLinkButton()
1370 ->setHref('#')
1371 ->setOnClick($aOnClick)
1372 ->setTitle('Open history of this record')
1373 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1374 'actions-document-history-open',
1375 Icon::SIZE_SMALL
1376 ));
1377 $buttonBar->addButton($historyButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
1378 }
1379 // If only SOME fields are shown in the form, this will link the user to the FULL form:
1380 if ($this->columnsOnly) {
1381 $columnsOnlyButton = $buttonBar->makeLinkButton()
1382 ->setHref($this->R_URI . '&columnsOnly=')
1383 ->setTitle($lang->getLL('editWholeRecord'))
1384 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1385 'actions-open',
1386 Icon::SIZE_SMALL
1387 ));
1388 $buttonBar->addButton($columnsOnlyButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
1389 }
1390 }
1391 }
1392 $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('TCEforms');
1393 $buttonBar->addButton($cshButton);
1394 $this->shortCutLink();
1395 $this->openInNewWindowLink();
1396 }
1397
1398 /**
1399 * Put together the various elements (buttons, selectors, form) into a table
1400 *
1401 * @param string $editForm HTML form.
1402 * @return string Composite HTML
1403 */
1404 public function compileForm($editForm)
1405 {
1406 $formContent = '
1407 <!-- EDITING FORM -->
1408 <form
1409 action="' . htmlspecialchars($this->R_URI) . '"
1410 method="post"
1411 enctype="multipart/form-data"
1412 name="editform"
1413 id="EditDocumentController"
1414 onsubmit="TBE_EDITOR.checkAndDoSubmit(1); return false;">
1415 ' . $editForm . '
1416
1417 <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
1418 <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />';
1419 if ($this->returnNewPageId) {
1420 $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
1421 }
1422 $formContent .= '<input type="hidden" name="popViewId" value="' . htmlspecialchars($this->viewId) . '" />';
1423 if ($this->viewId_addParams) {
1424 $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams) . '" />';
1425 }
1426 $formContent .= '
1427 <input type="hidden" name="closeDoc" value="0" />
1428 <input type="hidden" name="doSave" value="0" />
1429 <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
1430 <input type="hidden" name="_scrollPosition" value="" />';
1431 return $formContent;
1432 }
1433
1434 /**
1435 * Create shortcut icon
1436 */
1437 public function shortCutLink()
1438 {
1439 if ($this->returnUrl !== $this->getCloseUrl()) {
1440 $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
1441 $shortCutButton->setModuleName($this->MCONF['name'])
1442 ->setGetVariables([
1443 'returnUrl',
1444 'edit',
1445 'defVals',
1446 'overrideVals',
1447 'columnsOnly',
1448 'returnNewPageId',
1449 'noView']);
1450 $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->addButton($shortCutButton);
1451 }
1452 }
1453
1454 /**
1455 * Creates open-in-window link
1456 */
1457 public function openInNewWindowLink()
1458 {
1459 $closeUrl = $this->getCloseUrl();
1460 if ($this->returnUrl !== $closeUrl) {
1461 $aOnClick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue(GeneralUtility::linkThisScript(
1462 ['returnUrl' => $closeUrl]
1463 ))
1464 . ','
1465 . GeneralUtility::quoteJSvalue(md5($this->R_URI))
1466 . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1467 $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
1468 ->makeLinkButton()
1469 ->setHref('#')
1470 ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
1471 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL))
1472 ->setOnClick($aOnClick);
1473 $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->addButton(
1474 $openInNewWindowButton,
1475 ButtonBar::BUTTON_POSITION_RIGHT
1476 );
1477 }
1478 }
1479
1480 /**
1481 * Returns the URL (usually for the "returnUrl") which closes the current window.
1482 * Used when editing a record in a popup.
1483 *
1484 * @return string
1485 */
1486 protected function getCloseUrl(): string
1487 {
1488 $closeUrl = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
1489 return PathUtility::getAbsoluteWebPath($closeUrl);
1490 }
1491
1492 /***************************
1493 *
1494 * Localization stuff
1495 *
1496 ***************************/
1497 /**
1498 * Make selector box for creating new translation for a record or switching to edit the record in an existing
1499 * language.
1500 * Displays only languages which are available for the current page.
1501 *
1502 * @param string $table Table name
1503 * @param int $uid Uid for which to create a new language
1504 * @param int $pid Pid of the record
1505 */
1506 public function languageSwitch($table, $uid, $pid = null)
1507 {
1508 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1509 $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1510 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1511 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1512
1513 // Table editable and activated for languages?
1514 if ($this->getBackendUser()->check('tables_modify', $table)
1515 && $languageField
1516 && $transOrigPointerField
1517 ) {
1518 if (is_null($pid)) {
1519 $row = BackendUtility::getRecord($table, $uid, 'pid');
1520 $pid = $row['pid'];
1521 }
1522 // Get all available languages for the page
1523 // If editing a page, the translations of the current UID need to be fetched
1524 if ($table === 'pages') {
1525 $row = BackendUtility::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
1526 // Ensure the check is always done against the default language page
1527 $langRows = $this->getLanguages($row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $uid);
1528 } else {
1529 $langRows = $this->getLanguages($pid);
1530 }
1531 // Page available in other languages than default language?
1532 if (is_array($langRows) && count($langRows) > 1) {
1533 $rowsByLang = [];
1534 $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
1535 // Get record in current language
1536 $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
1537 if (!is_array($rowCurrent)) {
1538 $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
1539 }
1540 $currentLanguage = (int)$rowCurrent[$languageField];
1541 // Disabled for records with [all] language!
1542 if ($currentLanguage > -1) {
1543 // Get record in default language if needed
1544 if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
1545 $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
1546 $table,
1547 $rowCurrent[$transOrigPointerField],
1548 $fetchFields
1549 );
1550 if (!is_array($rowsByLang[0])) {
1551 $rowsByLang[0] = BackendUtility::getRecord(
1552 $table,
1553 $rowCurrent[$transOrigPointerField],
1554 $fetchFields
1555 );
1556 }
1557 } else {
1558 $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
1559 }
1560 if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
1561 // Get record in other languages to see what's already available
1562
1563 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1564 ->getQueryBuilderForTable($table);
1565
1566 $queryBuilder->getRestrictions()
1567 ->removeAll()
1568 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1569 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1570
1571 $result = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fetchFields, true))
1572 ->from($table)
1573 ->where(
1574 $queryBuilder->expr()->eq(
1575 'pid',
1576 $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
1577 ),
1578 $queryBuilder->expr()->gt(
1579 $languageField,
1580 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1581 ),
1582 $queryBuilder->expr()->eq(
1583 $transOrigPointerField,
1584 $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO::PARAM_INT)
1585 )
1586 )
1587 ->execute();
1588
1589 while ($row = $result->fetch()) {
1590 $rowsByLang[$row[$languageField]] = $row;
1591 }
1592 }
1593 $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
1594 $languageMenu->setIdentifier('_langSelector');
1595 foreach ($langRows as $lang) {
1596 if ($this->getBackendUser()->checkLanguageAccess($lang['uid'])) {
1597 $newTranslation = isset($rowsByLang[$lang['uid']]) ? '' : ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
1598 // Create url for creating a localized record
1599 $addOption = true;
1600 if ($newTranslation) {
1601 $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
1602 'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $lang['uid'],
1603 'returnUrl' => $this->retUrl
1604 ]);
1605
1606 if (array_key_exists(0, $rowsByLang)) {
1607 $href = BackendUtility::getLinkToDataHandlerAction(
1608 '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $lang['uid'],
1609 $redirectUrl
1610 );
1611 } else {
1612 $addOption = false;
1613 }
1614 } else {
1615 $href = (string)$uriBuilder->buildUriFromRoute('record_edit', [
1616 'edit[' . $table . '][' . $rowsByLang[$lang['uid']]['uid'] . ']' => 'edit',
1617 'returnUrl' => $this->retUrl
1618 ]);
1619 }
1620 if ($addOption) {
1621 $menuItem = $languageMenu->makeMenuItem()
1622 ->setTitle($lang['title'] . $newTranslation)
1623 ->setHref($href);
1624 if ((int)$lang['uid'] === $currentLanguage) {
1625 $menuItem->setActive(true);
1626 }
1627 $languageMenu->addMenuItem($menuItem);
1628 }
1629 }
1630 }
1631 $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
1632 }
1633 }
1634 }
1635 }
1636
1637 /**
1638 * Redirects to FormEngine with new parameters to edit a just created localized record
1639 *
1640 * @param string $justLocalized String passed by GET &justLocalized=
1641 */
1642 public function localizationRedirect($justLocalized)
1643 {
1644 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1645 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1646
1647 list($table, $origUid, $language) = explode(':', $justLocalized);
1648 if ($GLOBALS['TCA'][$table]
1649 && $GLOBALS['TCA'][$table]['ctrl']['languageField']
1650 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
1651 ) {
1652 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1653 ->getQueryBuilderForTable($table);
1654 $queryBuilder->getRestrictions()
1655 ->removeAll()
1656 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1657 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1658
1659 $localizedRecord = $queryBuilder->select('uid')
1660 ->from($table)
1661 ->where(
1662 $queryBuilder->expr()->eq(
1663 $GLOBALS['TCA'][$table]['ctrl']['languageField'],
1664 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
1665 ),
1666 $queryBuilder->expr()->eq(
1667 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
1668 $queryBuilder->createNamedParameter($origUid, \PDO::PARAM_INT)
1669 )
1670 )
1671 ->execute()
1672 ->fetch();
1673
1674 if (is_array($localizedRecord)) {
1675 // Create parameters and finally run the classic page module for creating a new page translation
1676 $location = (string)$uriBuilder->buildUriFromRoute('record_edit', [
1677 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
1678 'returnUrl' => GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'))
1679 ]);
1680 HttpUtility::redirect($location);
1681 }
1682 }
1683 }
1684
1685 /**
1686 * Returns sys_language records available for record translations on given page.
1687 *
1688 * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
1689 * hidden. If set to another value, the query will select all sys_language records that has a
1690 * translation record on that page (and is not hidden, unless you are admin user)
1691 * @return array Language records including faked record for default language
1692 */
1693 public function getLanguages($id)
1694 {
1695 $modSharedTSconfig = BackendUtility::getModTSconfig($id, 'mod.SHARED');
1696 // Fallback non sprite-configuration
1697 if (preg_match('/\\.gif$/', $modSharedTSconfig['properties']['defaultLanguageFlag'])) {
1698 $modSharedTSconfig['properties']['defaultLanguageFlag'] = str_replace(
1699 '.gif',
1700 '',
1701 $modSharedTSconfig['properties']['defaultLanguageFlag']
1702 );
1703 }
1704 $languages = [
1705 0 => [
1706 'uid' => 0,
1707 'pid' => 0,
1708 'hidden' => 0,
1709 'title' => $modSharedTSconfig['properties']['defaultLanguageLabel'] !== ''
1710 ? $modSharedTSconfig['properties']['defaultLanguageLabel'] . ' (' . $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage') . ')'
1711 : $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage'),
1712 'flag' => $modSharedTSconfig['properties']['defaultLanguageFlag']
1713 ]
1714 ];
1715
1716 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1717 ->getQueryBuilderForTable('sys_language');
1718
1719 $queryBuilder->select('s.uid', 's.pid', 's.hidden', 's.title', 's.flag')
1720 ->from('sys_language', 's')
1721 ->groupBy('s.uid', 's.pid', 's.hidden', 's.title', 's.flag', 's.sorting')
1722 ->orderBy('s.sorting');
1723
1724 if ($id) {
1725 $queryBuilder->getRestrictions()
1726 ->removeAll()
1727 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1728 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1729
1730 if (!$this->getBackendUser()->isAdmin()) {
1731 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(HiddenRestriction::class));
1732 }
1733
1734 // Add join with pages translations to only show active languages
1735 $queryBuilder->from('pages', 'o')
1736 ->where(
1737 $queryBuilder->expr()->eq('o.' . $GLOBALS['TCA']['pages']['ctrl']['languageField'], $queryBuilder->quoteIdentifier('s.uid')),
1738 $queryBuilder->expr()->eq('o.' . $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
1739 );
1740 }
1741
1742 $result = $queryBuilder->execute();
1743 while ($row = $result->fetch()) {
1744 $languages[$row['uid']] = $row;
1745 }
1746
1747 return $languages;
1748 }
1749
1750 /***************************
1751 *
1752 * Other functions
1753 *
1754 ***************************/
1755 /**
1756 * Fix $this->editconf if versioning applies to any of the records
1757 *
1758 * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
1759 */
1760 public function fixWSversioningInEditConf($mapArray = false)
1761 {
1762 // Traverse the editConf array
1763 if (is_array($this->editconf)) {
1764 // Tables:
1765 foreach ($this->editconf as $table => $conf) {
1766 if (is_array($conf) && $GLOBALS['TCA'][$table]) {
1767 // Traverse the keys/comments of each table (keys can be a commalist of uids)
1768 $newConf = [];
1769 foreach ($conf as $cKey => $cmd) {
1770 if ($cmd === 'edit') {
1771 // Traverse the ids:
1772 $ids = GeneralUtility::trimExplode(',', $cKey, true);
1773 foreach ($ids as $idKey => $theUid) {
1774 if (is_array($mapArray)) {
1775 if ($mapArray[$table][$theUid]) {
1776 $ids[$idKey] = $mapArray[$table][$theUid];
1777 }
1778 } else {
1779 // Default, look for versions in workspace for record:
1780 $calcPRec = $this->getRecordForEdit($table, $theUid);
1781 if (is_array($calcPRec)) {
1782 // Setting UID again if it had changed, eg. due to workspace versioning.
1783 $ids[$idKey] = $calcPRec['uid'];
1784 }
1785 }
1786 }
1787 // Add the possibly manipulated IDs to the new-build newConf array:
1788 $newConf[implode(',', $ids)] = $cmd;
1789 } else {
1790 $newConf[$cKey] = $cmd;
1791 }
1792 }
1793 // Store the new conf array:
1794 $this->editconf[$table] = $newConf;
1795 }
1796 }
1797 }
1798 }
1799
1800 /**
1801 * Get record for editing.
1802 *
1803 * @param string $table Table name
1804 * @param int $theUid Record UID
1805 * @return array Returns record to edit, FALSE if none
1806 */
1807 public function getRecordForEdit($table, $theUid)
1808 {
1809 // Fetch requested record:
1810 $reqRecord = BackendUtility::getRecord($table, $theUid, 'uid,pid');
1811 if (is_array($reqRecord)) {
1812 // If workspace is OFFLINE:
1813 if ($this->getBackendUser()->workspace != 0) {
1814 // Check for versioning support of the table:
1815 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1816 // If the record is already a version of "something" pass it by.
1817 if ($reqRecord['pid'] == -1) {
1818 // (If it turns out not to be a version of the current workspace there will be trouble, but
1819 // that is handled inside DataHandler then and in the interface it would clearly be an error of
1820 // links if the user accesses such a scenario)
1821 return $reqRecord;
1822 }
1823 // The input record was online and an offline version must be found or made:
1824 // Look for version of this workspace:
1825 $versionRec = BackendUtility::getWorkspaceVersionOfRecord(
1826 $this->getBackendUser()->workspace,
1827 $table,
1828 $reqRecord['uid'],
1829 'uid,pid,t3ver_oid'
1830 );
1831 return is_array($versionRec) ? $versionRec : $reqRecord;
1832 }
1833 // This means that editing cannot occur on this record because it was not supporting versioning
1834 // which is required inside an offline workspace.
1835 return false;
1836 }
1837 // In ONLINE workspace, just return the originally requested record:
1838 return $reqRecord;
1839 }
1840 // Return FALSE because the table/uid was not found anyway.
1841 return false;
1842 }
1843
1844 /**
1845 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
1846 *
1847 * @see makeDocSel()
1848 */
1849 public function compileStoreDat()
1850 {
1851 $this->storeArray = GeneralUtility::compileSelectedGetVarsFromArray(
1852 'edit,defVals,overrideVals,columnsOnly,noView,workspace',
1853 $this->R_URL_getvars
1854 );
1855 $this->storeUrl = GeneralUtility::implodeArrayForUrl('', $this->storeArray);
1856 $this->storeUrlMd5 = md5($this->storeUrl);
1857 }
1858
1859 /**
1860 * Function used to look for configuration of buttons in the form: Fx. disabling buttons or showing them at various
1861 * positions.
1862 *
1863 * @param string $table The table for which the configuration may be specific
1864 * @param string $key The option for look for. Default is checking if the saveDocNew button should be displayed.
1865 * @return string Return value fetched from USER TSconfig
1866 */
1867 public function getNewIconMode($table, $key = 'saveDocNew')
1868 {
1869 $TSconfig = $this->getBackendUser()->getTSConfig('options.' . $key);
1870 $output = trim($TSconfig['properties'][$table] ?? $TSconfig['value']);
1871 return $output;
1872 }
1873
1874 /**
1875 * Handling the closing of a document
1876 * The argument $mode can be one of this values:
1877 * - 0/1 will redirect to $this->retUrl [self::DOCUMENT_CLOSE_MODE_DEFAULT || self::DOCUMENT_CLOSE_MODE_REDIRECT]
1878 * - 3 will clear the docHandler (thus closing all documents) [self::DOCUMENT_CLOSE_MODE_CLEAR_ALL]
1879 * - 4 will do no redirect [self::DOCUMENT_CLOSE_MODE_NO_REDIRECT]
1880 * - other values will call setDocument with ->retUrl
1881 *
1882 * @param int $mode the close mode: one of self::DOCUMENT_CLOSE_MODE_*
1883 */
1884 public function closeDocument($mode = self::DOCUMENT_CLOSE_MODE_DEFAULT)
1885 {
1886 $mode = (int)$mode;
1887 // If current document is found in docHandler,
1888 // then unset it, possibly unset it ALL and finally, write it to the session data
1889 if (isset($this->docHandler[$this->storeUrlMd5])) {
1890 // add the closing document to the recent documents
1891 $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
1892 if (!is_array($recentDocs)) {
1893 $recentDocs = [];
1894 }
1895 $closedDoc = $this->docHandler[$this->storeUrlMd5];
1896 $recentDocs = array_merge([$this->storeUrlMd5 => $closedDoc], $recentDocs);
1897 if (count($recentDocs) > 8) {
1898 $recentDocs = array_slice($recentDocs, 0, 8);
1899 }
1900 // remove it from the list of the open documents
1901 unset($this->docHandler[$this->storeUrlMd5]);
1902 if ($mode === self::DOCUMENT_CLOSE_MODE_CLEAR_ALL) {
1903 $recentDocs = array_merge($this->docHandler, $recentDocs);
1904 $this->docHandler = [];
1905 }
1906 $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
1907 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->docDat[1]]);
1908 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
1909 }
1910 if ($mode !== self::DOCUMENT_CLOSE_MODE_NO_REDIRECT) {
1911 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1912 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1913 // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: (used by
1914 // other scripts, like wizard_add, to know which records was created or so...)
1915 if ($this->returnEditConf && $this->retUrl != (string)$uriBuilder->buildUriFromRoute('dummy')) {
1916 $this->retUrl .= '&returnEditConf=' . rawurlencode(json_encode($this->editconf));
1917 }
1918
1919 // If mode is NOT set (means 0) OR set to 1, then make a header location redirect to $this->retUrl
1920 if ($mode === self::DOCUMENT_CLOSE_MODE_DEFAULT || $mode === self::DOCUMENT_CLOSE_MODE_REDIRECT) {
1921 HttpUtility::redirect($this->retUrl);
1922 } else {
1923 $this->setDocument('', $this->retUrl);
1924 }
1925 }
1926 }
1927
1928 /**
1929 * Redirects to the document pointed to by $currentDocFromHandlerMD5 OR $retUrl (depending on some internal
1930 * calculations).
1931 * Most likely you will get a header-location redirect from this function.
1932 *
1933 * @param string $currentDocFromHandlerMD5 Pointer to the document in the docHandler array
1934 * @param string $retUrl Alternative/Default retUrl
1935 */
1936 public function setDocument($currentDocFromHandlerMD5 = '', $retUrl = '')
1937 {
1938 if ($retUrl === '') {
1939 return;
1940 }
1941 if (!$this->modTSconfig['properties']['disableDocSelector']
1942 && is_array($this->docHandler)
1943 && !empty($this->docHandler)
1944 ) {
1945 if (isset($this->docHandler[$currentDocFromHandlerMD5])) {
1946 $setupArr = $this->docHandler[$currentDocFromHandlerMD5];
1947 } else {
1948 $setupArr = reset($this->docHandler);
1949 }
1950 if ($setupArr[2]) {
1951 $sParts = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
1952 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
1953 }
1954 }
1955 HttpUtility::redirect($retUrl);
1956 }
1957
1958 /**
1959 * Injects the request object for the current request or subrequest
1960 *
1961 * @param ServerRequestInterface $request the current request
1962 * @param ResponseInterface $response
1963 * @return ResponseInterface the response with the content
1964 */
1965 public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
1966 {
1967 BackendUtility::lockRecords();
1968
1969 // Preprocessing, storing data if submitted to
1970 $this->preInit();
1971
1972 // Checks, if a save button has been clicked (or the doSave variable is sent)
1973 if ($this->doProcessData()) {
1974 $this->processData();
1975 }
1976
1977 $this->init();
1978 $this->main();
1979
1980 $response->getBody()->write($this->moduleTemplate->renderContent());
1981 return $response;
1982 }
1983
1984 /**
1985 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1986 */
1987 protected function getBackendUser()
1988 {
1989 return $GLOBALS['BE_USER'];
1990 }
1991
1992 /**
1993 * Returns LanguageService
1994 *
1995 * @return \TYPO3\CMS\Core\Localization\LanguageService
1996 */
1997 protected function getLanguageService()
1998 {
1999 return $GLOBALS['LANG'];
2000 }
2001 }