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