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