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