[BUGFIX] Minor code cleanups
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / EditDocumentController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Backend\Controller;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
21 use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
22 use TYPO3\CMS\Backend\Form\FormDataCompiler;
23 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
24 use TYPO3\CMS\Backend\Form\FormResultCompiler;
25 use TYPO3\CMS\Backend\Form\NodeFactory;
26 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
27 use TYPO3\CMS\Backend\Routing\UriBuilder;
28 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
29 use TYPO3\CMS\Backend\Template\ModuleTemplate;
30 use TYPO3\CMS\Backend\Utility\BackendUtility;
31 use TYPO3\CMS\Core\Database\ConnectionPool;
32 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
33 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
34 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
35 use TYPO3\CMS\Core\Database\ReferenceIndex;
36 use TYPO3\CMS\Core\DataHandling\DataHandler;
37 use TYPO3\CMS\Core\Http\HtmlResponse;
38 use TYPO3\CMS\Core\Http\RedirectResponse;
39 use TYPO3\CMS\Core\Imaging\Icon;
40 use TYPO3\CMS\Core\Messaging\FlashMessage;
41 use TYPO3\CMS\Core\Messaging\FlashMessageService;
42 use TYPO3\CMS\Core\Page\PageRenderer;
43 use TYPO3\CMS\Core\Routing\SiteMatcher;
44 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
45 use TYPO3\CMS\Core\Type\Bitmask\Permission;
46 use TYPO3\CMS\Core\Utility\GeneralUtility;
47 use TYPO3\CMS\Core\Utility\HttpUtility;
48 use TYPO3\CMS\Core\Utility\MathUtility;
49 use TYPO3\CMS\Core\Utility\PathUtility;
50 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
51 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
52 use TYPO3\CMS\Frontend\Page\PageRepository;
53
54 /**
55 * Main backend controller almost always used if some database record is edited in the backend.
56 *
57 * Main job of this controller is to evaluate and sanitize $request parameters,
58 * call the DataHandler if records should be created or updated and
59 * execute FormEngine for record rendering.
60 */
61 class EditDocumentController
62 {
63 protected const DOCUMENT_CLOSE_MODE_DEFAULT = 0;
64 // works like DOCUMENT_CLOSE_MODE_DEFAULT
65 protected const DOCUMENT_CLOSE_MODE_REDIRECT = 1;
66 protected const DOCUMENT_CLOSE_MODE_CLEAR_ALL = 3;
67 protected const DOCUMENT_CLOSE_MODE_NO_REDIRECT = 4;
68
69 /**
70 * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
71 *
72 * @see \TYPO3\CMS\Backend\Utility\BackendUtility::editOnClick()
73 * @var array
74 */
75 protected $editconf = [];
76
77 /**
78 * Comma list of field names to edit. If specified, only those fields will be rendered.
79 * Otherwise all (available) fields in the record are shown according to the TCA type.
80 *
81 * @var string|null
82 */
83 protected $columnsOnly;
84
85 /**
86 * Default values for fields
87 *
88 * @var array|null [table][field]
89 */
90 protected $defVals;
91
92 /**
93 * Array of values to force being set as hidden fields in FormEngine
94 *
95 * @var array|null [table][field]
96 */
97 protected $overrideVals;
98
99 /**
100 * If set, this value will be set in $this->retUrl as "returnUrl", if not,
101 * $this->retUrl will link to dummy controller
102 *
103 * @var string|null
104 */
105 protected $returnUrl;
106
107 /**
108 * Prepared return URL. Contains the URL that we should return to from FormEngine if
109 * close button is clicked. Usually passed along as 'returnUrl', but falls back to
110 * "dummy" controller.
111 *
112 * @var string
113 */
114 protected $retUrl;
115
116 /**
117 * Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
118 *
119 * @var int
120 */
121 protected $closeDoc;
122
123 /**
124 * If true, the processing of incoming data will be performed as if a save-button is pressed.
125 * Used in the forms as a hidden field which can be set through
126 * JavaScript if the form is somehow submitted by JavaScript.
127 *
128 * @var bool
129 */
130 protected $doSave;
131
132 /**
133 * Main DataHandler datamap array
134 *
135 * @var array
136 * @todo: Will be set protected later, still used by ConditionMatcher
137 * @internal Will be removed / protected in TYPO3 v10.0 without further notice
138 */
139 public $data;
140
141 /**
142 * Main DataHandler cmdmap array
143 *
144 * @var array
145 */
146 protected $cmd;
147
148 /**
149 * DataHandler 'mirror' input
150 *
151 * @var array
152 */
153 protected $mirror;
154
155 /**
156 * Boolean: If set, then the GET var "&id=" will be added to the
157 * retUrl string so that the NEW id of something is returned to the script calling the form.
158 *
159 * @var bool
160 */
161 protected $returnNewPageId = false;
162
163 /**
164 * Updated values for backendUser->uc. Used for new inline records to mark them
165 * as expanded: uc[inlineView][...]
166 *
167 * @var array|null
168 */
169 protected $uc;
170
171 /**
172 * ID for displaying the page in the frontend, "save and view"
173 *
174 * @var int
175 */
176 protected $popViewId;
177
178 /**
179 * Alternative URL for viewing the frontend pages.
180 *
181 * @var string
182 */
183 protected $viewUrl;
184
185 /**
186 * Alternative title for the document handler.
187 *
188 * @var string
189 */
190 protected $recTitle;
191
192 /**
193 * If set, then no save & view button is printed
194 *
195 * @var bool
196 */
197 protected $noView;
198
199 /**
200 * @var string
201 */
202 protected $perms_clause;
203
204 /**
205 * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
206 *
207 * @var bool
208 */
209 protected $returnEditConf;
210
211 /**
212 * Workspace used for the editing action.
213 *
214 * @var string|null
215 */
216 protected $workspace;
217
218 /**
219 * parse_url() of current requested URI, contains ['path'] and ['query'] parts.
220 *
221 * @var array
222 */
223 protected $R_URL_parts;
224
225 /**
226 * Contains $request query parameters. This array is the foundation for creating
227 * the R_URI internal var which becomes the url to which forms are submitted
228 *
229 * @var array
230 */
231 protected $R_URL_getvars;
232
233 /**
234 * Set to the URL of this script including variables which is needed to re-display the form.
235 *
236 * @var string
237 */
238 protected $R_URI;
239
240 /**
241 * @var array
242 */
243 protected $pageinfo;
244
245 /**
246 * Is loaded with the "title" of the currently "open document"
247 * used for the open document toolbar
248 *
249 * @var string
250 */
251 protected $storeTitle = '';
252
253 /**
254 * Contains an array with key/value pairs of GET parameters needed to reach the
255 * current document displayed - used in the 'open documents' toolbar.
256 *
257 * @var array
258 */
259 protected $storeArray;
260
261 /**
262 * $this->storeArray imploded to url
263 *
264 * @var string
265 */
266 protected $storeUrl;
267
268 /**
269 * md5 hash of storeURL, used to identify a single open document in backend user uc
270 *
271 * @var string
272 */
273 protected $storeUrlMd5;
274
275 /**
276 * Backend user session data of this module
277 *
278 * @var array
279 */
280 protected $docDat;
281
282 /**
283 * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
284 * the various documents on the GET parameter list needed to open it. The values are
285 * arrays with 0,1,2 keys with information about the document (see compileStoreData()).
286 * The docHandler variable is stored in the $docDat session data, key "0".
287 *
288 * @var array
289 */
290 protected $docHandler;
291
292 /**
293 * Array of the elements to create edit forms for.
294 *
295 * @var array
296 * @todo: Will be set protected later, still used by ConditionMatcher
297 * @internal Will be removed / protected in TYPO3 v10.0 without further notice
298 */
299 public $elementsData;
300
301 /**
302 * Pointer to the first element in $elementsData
303 *
304 * @var array
305 */
306 protected $firstEl;
307
308 /**
309 * Counter, used to count the number of errors (when users do not have edit permissions)
310 *
311 * @var int
312 */
313 protected $errorC;
314
315 /**
316 * Counter, used to count the number of new record forms displayed
317 *
318 * @var int
319 */
320 protected $newC;
321
322 /**
323 * Is set to the pid value of the last shown record - thus indicating which page to
324 * show when clicking the SAVE/VIEW button
325 *
326 * @var int
327 */
328 protected $viewId;
329
330 /**
331 * Is set to additional parameters (like "&L=xxx") if the record supports it.
332 *
333 * @var string
334 */
335 protected $viewId_addParams;
336
337 /**
338 * @var FormResultCompiler
339 */
340 protected $formResultCompiler;
341
342 /**
343 * Used internally to disable the storage of the document reference (eg. new records)
344 *
345 * @var bool
346 */
347 protected $dontStoreDocumentRef = 0;
348
349 /**
350 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
351 */
352 protected $signalSlotDispatcher;
353
354 /**
355 * Stores information needed to preview the currently saved record
356 *
357 * @var array
358 */
359 protected $previewData = [];
360
361 /**
362 * ModuleTemplate object
363 *
364 * @var ModuleTemplate
365 */
366 protected $moduleTemplate;
367
368 /**
369 * Check if a record has been saved
370 *
371 * @var bool
372 */
373 protected $isSavedRecord;
374
375 /**
376 * Check if a page in free translation mode
377 *
378 * @var bool
379 */
380 protected $isPageInFreeTranslationMode = false;
381
382 /**
383 * Constructor
384 */
385 public function __construct()
386 {
387 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
388 $this->moduleTemplate->setUiBlock(true);
389 // @todo Used by TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching
390 $GLOBALS['SOBE'] = $this;
391 $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
392 }
393
394 /**
395 * Main dispatcher entry method registered as "record_edit" end point
396 *
397 * @param ServerRequestInterface $request the current request
398 * @return ResponseInterface the response with the content
399 */
400 public function mainAction(ServerRequestInterface $request): ResponseInterface
401 {
402 // Unlock all locked records
403 BackendUtility::lockRecords();
404 if ($response = $this->preInit($request)) {
405 return $response;
406 }
407
408 // Process incoming data via DataHandler?
409 $parsedBody = $request->getParsedBody();
410 if ($this->doSave
411 || isset($parsedBody['_savedok'])
412 || isset($parsedBody['_saveandclosedok'])
413 || isset($parsedBody['_savedokview'])
414 || isset($parsedBody['_savedoknew'])
415 || isset($parsedBody['_duplicatedoc'])
416 ) {
417 if ($response = $this->processData($request)) {
418 return $response;
419 }
420 }
421
422 $this->init($request);
423 $this->main($request);
424
425 return new HtmlResponse($this->moduleTemplate->renderContent());
426 }
427
428 /**
429 * First initialization, always called, even before processData() executes DataHandler processing.
430 *
431 * @param ServerRequestInterface $request
432 * @return ResponseInterface Possible redirect response
433 */
434 protected function preInit(ServerRequestInterface $request): ?ResponseInterface
435 {
436 if ($response = $this->localizationRedirect($request)) {
437 return $response;
438 }
439
440 $parsedBody = $request->getParsedBody();
441 $queryParams = $request->getQueryParams();
442
443 $this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
444 $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
445 $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
446 $this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
447 $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null);
448 $this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
449 $this->doSave = (bool)($parsedBody['doSave'] ?? $queryParams['doSave'] ?? false);
450 $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
451 $this->workspace = $parsedBody['workspace'] ?? $queryParams['workspace'] ?? null;
452 $this->uc = $parsedBody['uc'] ?? $queryParams['uc'] ?? null;
453
454 // Set overrideVals as default values if defVals does not exist.
455 // @todo: Why?
456 if (!is_array($this->defVals) && is_array($this->overrideVals)) {
457 $this->defVals = $this->overrideVals;
458 }
459
460 // Set final return URL
461 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
462 $this->retUrl = $this->returnUrl ?: (string)$uriBuilder->buildUriFromRoute('dummy');
463
464 // Change $this->editconf if versioning applies to any of the records
465 $this->fixWSversioningInEditConf();
466
467 // Prepare R_URL (request url)
468 $this->R_URL_parts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
469 $this->R_URL_getvars = $queryParams;
470 $this->R_URL_getvars['edit'] = $this->editconf;
471
472 // Prepare 'open documents' url, this is later modified again various times
473 $this->compileStoreData();
474 // Backend user session data of this module
475 $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
476 $this->docHandler = $this->docDat[0];
477
478 // Close document if a request for closing the document has been sent
479 if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
480 if ($response = $this->closeDocument($this->closeDoc, $request)) {
481 return $response;
482 }
483 }
484
485 // Sets a temporary workspace, this request is based on
486 if ($this->workspace !== null) {
487 $this->getBackendUser()->setTemporaryWorkspace($this->workspace);
488 }
489
490 $this->emitFunctionAfterSignal('preInit', $request);
491
492 return null;
493 }
494
495 /**
496 * Do processing of data, submitting it to DataHandler. May return a RedirectResponse
497 *
498 * @param ServerRequestInterface $request
499 * @return ResponseInterface|null
500 */
501 protected function processData(ServerRequestInterface $request): ?ResponseInterface
502 {
503 $parsedBody = $request->getParsedBody();
504 $queryParams = $request->getQueryParams();
505
506 $beUser = $this->getBackendUser();
507
508 // Processing related GET / POST vars
509 $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];
510 $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];
511 $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];
512 $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);
513
514 // Only options related to $this->data submission are included here
515 $tce = GeneralUtility::makeInstance(DataHandler::class);
516
517 $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);
518
519 // Set default values specific for the user
520 $TCAdefaultOverride = $beUser->getTSConfig()['TCAdefaults.'] ?? null;
521 if (is_array($TCAdefaultOverride)) {
522 $tce->setDefaultsFromUserTS($TCAdefaultOverride);
523 }
524 // Set internal vars
525 if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {
526 $tce->neverHideAtCopy = 1;
527 }
528 // Load DataHandler with data
529 $tce->start($this->data, $this->cmd);
530 if (is_array($this->mirror)) {
531 $tce->setMirror($this->mirror);
532 }
533
534 // Perform the saving operation with DataHandler:
535 if ($this->doSave === true) {
536 // @todo: Make DataHandler understand UploadedFileInterface and submit $request->getUploadedFiles() instead of $_FILES here
537 $tce->process_uploads($_FILES);
538 $tce->process_datamap();
539 $tce->process_cmdmap();
540 }
541 // If pages are being edited, we set an instruction about updating the page tree after this operation.
542 if ($tce->pagetreeNeedsRefresh
543 && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
544 ) {
545 BackendUtility::setUpdateSignal('updatePageTree');
546 }
547 // If there was saved any new items, load them:
548 if (!empty($tce->substNEWwithIDs_table)) {
549 // Save the expanded/collapsed states for new inline records, if any
550 FormEngineUtility::updateInlineView($this->uc, $tce);
551 $newEditConf = [];
552 foreach ($this->editconf as $tableName => $tableCmds) {
553 $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
554 if (!empty($keys)) {
555 foreach ($keys as $key) {
556 $editId = $tce->substNEWwithIDs[$key];
557 // Check if the $editId isn't a child record of an IRRE action
558 if (!(is_array($tce->newRelatedIDs[$tableName])
559 && in_array($editId, $tce->newRelatedIDs[$tableName]))
560 ) {
561 // Translate new id to the workspace version
562 if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
563 $beUser->workspace,
564 $tableName,
565 $editId,
566 'uid'
567 )) {
568 $editId = $versionRec['uid'];
569 }
570 $newEditConf[$tableName][$editId] = 'edit';
571 }
572 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
573 // Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
574 if ($tableName === 'pages'
575 && $this->retUrl != (string)$uriBuilder->buildUriFromRoute('dummy')
576 && $this->returnNewPageId
577 ) {
578 $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
579 }
580 }
581 } else {
582 $newEditConf[$tableName] = $tableCmds;
583 }
584 }
585 // Reset editconf if newEditConf has values
586 if (!empty($newEditConf)) {
587 $this->editconf = $newEditConf;
588 }
589 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
590 $this->R_URL_getvars['edit'] = $this->editconf;
591 // Unset default values since we don't need them anymore.
592 unset($this->R_URL_getvars['defVals']);
593 // Recompile the store* values since editconf changed
594 $this->compileStoreData();
595 }
596 // See if any records was auto-created as new versions?
597 if (!empty($tce->autoVersionIdMap)) {
598 $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
599 }
600 // If a document is saved and a new one is created right after.
601 if (isset($parsedBody['_savedoknew']) && is_array($this->editconf)) {
602 if ($redirect = $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request)) {
603 return $redirect;
604 }
605 // Find the current table
606 reset($this->editconf);
607 $nTable = key($this->editconf);
608 // Finding the first id, getting the records pid+uid
609 reset($this->editconf[$nTable]);
610 $nUid = key($this->editconf[$nTable]);
611 $recordFields = 'pid,uid';
612 if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
613 $recordFields .= ',t3ver_oid';
614 }
615 $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
616 // Determine insertion mode: 'top' is self-explaining,
617 // otherwise new elements are inserted after one using a negative uid
618 $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
619 // Setting a blank editconf array for a new record:
620 $this->editconf = [];
621 // Determine related page ID for regular live context
622 if ($nRec['pid'] != -1) {
623 if ($insertRecordOnTop) {
624 $relatedPageId = $nRec['pid'];
625 } else {
626 $relatedPageId = -$nRec['uid'];
627 }
628 } else {
629 // Determine related page ID for workspace context
630 if ($insertRecordOnTop) {
631 // Fetch live version of workspace version since the pid value is always -1 in workspaces
632 $liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
633 $relatedPageId = $liveRecord['pid'];
634 } else {
635 // Use uid of live version of workspace version
636 $relatedPageId = -$nRec['t3ver_oid'];
637 }
638 }
639 $this->editconf[$nTable][$relatedPageId] = 'new';
640 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
641 $this->R_URL_getvars['edit'] = $this->editconf;
642 // Recompile the store* values since editconf changed...
643 $this->compileStoreData();
644 }
645 // If a document should be duplicated.
646 if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf)) {
647 $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request);
648 // Find current table
649 reset($this->editconf);
650 $nTable = key($this->editconf);
651 // Find the first id, getting the records pid+uid
652 reset($this->editconf[$nTable]);
653 $nUid = key($this->editconf[$nTable]);
654 if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
655 $nUid = $tce->substNEWwithIDs[$nUid];
656 }
657
658 $recordFields = 'pid,uid';
659 if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
660 $recordFields .= ',t3ver_oid';
661 }
662 $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
663
664 // Setting a blank editconf array for a new record:
665 $this->editconf = [];
666
667 if ($nRec['pid'] != -1) {
668 $relatedPageId = -$nRec['uid'];
669 } else {
670 $relatedPageId = -$nRec['t3ver_oid'];
671 }
672
673 /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $duplicateTce */
674 $duplicateTce = GeneralUtility::makeInstance(DataHandler::class);
675
676 $duplicateCmd = [
677 $nTable => [
678 $nUid => [
679 'copy' => $relatedPageId
680 ]
681 ]
682 ];
683
684 $duplicateTce->start([], $duplicateCmd);
685 $duplicateTce->process_cmdmap();
686
687 $duplicateMappingArray = $duplicateTce->copyMappingArray;
688 $duplicateUid = $duplicateMappingArray[$nTable][$nUid];
689
690 if ($nTable === 'pages') {
691 BackendUtility::setUpdateSignal('updatePageTree');
692 }
693
694 $this->editconf[$nTable][$duplicateUid] = 'edit';
695 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
696 $this->R_URL_getvars['edit'] = $this->editconf;
697 // Recompile the store* values since editconf changed...
698 $this->compileStoreData();
699
700 // Inform the user of the duplication
701 $flashMessage = GeneralUtility::makeInstance(
702 FlashMessage::class,
703 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
704 '',
705 FlashMessage::OK
706 );
707 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
708 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
709 $defaultFlashMessageQueue->enqueue($flashMessage);
710 }
711 // If a preview is requested
712 if (isset($parsedBody['_savedokview'])) {
713 // Get the first table and id of the data array from DataHandler
714 $table = reset(array_keys($this->data));
715 $id = reset(array_keys($this->data[$table]));
716 if (!MathUtility::canBeInterpretedAsInteger($id)) {
717 $id = $tce->substNEWwithIDs[$id];
718 }
719 // Store this information for later use
720 $this->previewData['table'] = $table;
721 $this->previewData['id'] = $id;
722 }
723 $tce->printLogErrorMessages();
724
725 if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
726 || isset($parsedBody['_saveandclosedok'])
727 ) {
728 // Redirect if element should be closed after save
729 return $this->closeDocument((int)abs($this->closeDoc), $request);
730 }
731 return null;
732 }
733
734 /**
735 * Initialize the view part of the controller logic.
736 *
737 * @param ServerRequestInterface $request
738 */
739 protected function init(ServerRequestInterface $request): void
740 {
741 $parsedBody = $request->getParsedBody();
742 $queryParams = $request->getQueryParams();
743
744 $beUser = $this->getBackendUser();
745
746 $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);
747 $this->viewUrl = (string)($parsedBody['viewUrl'] ?? $queryParams['viewUrl'] ?? '');
748 $this->recTitle = (string)($parsedBody['recTitle'] ?? $queryParams['recTitle'] ?? '');
749 $this->noView = (bool)($parsedBody['noView'] ?? $queryParams['noView'] ?? false);
750 $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
751 // Set other internal variables:
752 $this->R_URL_getvars['returnUrl'] = $this->retUrl;
753 $this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
754
755 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
756 $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
757
758 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
759 // override the default jumpToUrl
760 $this->moduleTemplate->addJavaScriptCode(
761 'jumpToUrl',
762 '
763 function deleteRecord(table,id,url) {
764 window.location.href = ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('tce_db') . '&cmd[') . '+table+"]["+id+"][delete]=1&redirect="+escape(url);
765 }
766 ' . (isset($parsedBody['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '')
767 );
768 // Set context sensitive menu
769 $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
770
771 $this->emitFunctionAfterSignal('init', $request);
772 }
773
774 /**
775 * Generate the Javascript for opening the preview window
776 *
777 * @return string
778 */
779 protected function generatePreviewCode(): string
780 {
781 $previewPageId = $this->getPreviewPageId();
782 $previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
783 $anchorSection = $this->getPreviewUrlAnchorSection();
784 $previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
785
786 return '
787 if (window.opener) {
788 '
789 . BackendUtility::viewOnClick(
790 $previewPageId,
791 '',
792 $previewPageRootLine,
793 $anchorSection,
794 $this->viewUrl,
795 $previewUrlParameters,
796 false
797 )
798 . '
799 } else {
800 '
801 . BackendUtility::viewOnClick(
802 $previewPageId,
803 '',
804 $previewPageRootLine,
805 $anchorSection,
806 $this->viewUrl,
807 $previewUrlParameters
808 )
809 . '
810 }';
811 }
812
813 /**
814 * Returns the parameters for the preview URL
815 *
816 * @param int $previewPageId
817 * @return string
818 */
819 protected function getPreviewUrlParameters(int $previewPageId): string
820 {
821 $linkParameters = [];
822 $table = $this->previewData['table'] ?: $this->firstEl['table'];
823 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
824 $previewConfiguration = BackendUtility::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
825 $recordArray = BackendUtility::getRecord($table, $recordId);
826
827 // language handling
828 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
829 if ($languageField && !empty($recordArray[$languageField])) {
830 $l18nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
831 if ($l18nPointer && !empty($recordArray[$l18nPointer])
832 && isset($previewConfiguration['useDefaultLanguageRecord'])
833 && !$previewConfiguration['useDefaultLanguageRecord']
834 ) {
835 // use parent record
836 $recordId = $recordArray[$l18nPointer];
837 }
838 $language = $recordArray[$languageField];
839 if ($language > 0) {
840 $linkParameters['L'] = $language;
841 }
842 }
843
844 // map record data to GET parameters
845 if (isset($previewConfiguration['fieldToParameterMap.'])) {
846 foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
847 $value = $recordArray[$field];
848 if ($field === 'uid') {
849 $value = $recordId;
850 }
851 $linkParameters[$parameterName] = $value;
852 }
853 }
854
855 // add/override parameters by configuration
856 if (isset($previewConfiguration['additionalGetParameters.'])) {
857 $additionalGetParameters = [];
858 $this->parseAdditionalGetParameters(
859 $additionalGetParameters,
860 $previewConfiguration['additionalGetParameters.']
861 );
862 $linkParameters = array_replace($linkParameters, $additionalGetParameters);
863 }
864
865 if (!empty($previewConfiguration['useCacheHash'])) {
866 $cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
867 $fullLinkParameters = HttpUtility::buildQueryString(array_merge($linkParameters, ['id' => $previewPageId]), '&');
868 $cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters);
869 $linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters);
870 } else {
871 $linkParameters['no_cache'] = 1;
872 }
873
874 return HttpUtility::buildQueryString($linkParameters, '&');
875 }
876
877 /**
878 * Returns the anchor section for the preview url
879 *
880 * @return string
881 */
882 protected function getPreviewUrlAnchorSection(): string
883 {
884 $table = $this->previewData['table'] ?: $this->firstEl['table'];
885 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
886
887 return $table === 'tt_content' ? '#c' . (int)$recordId : '';
888 }
889
890 /**
891 * Returns the preview page id
892 *
893 * @return int
894 */
895 protected function getPreviewPageId(): int
896 {
897 $previewPageId = 0;
898 $table = $this->previewData['table'] ?: $this->firstEl['table'];
899 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
900 $pageId = $this->popViewId ?: $this->viewId;
901
902 if ($table === 'pages') {
903 $currentPageId = (int)$recordId;
904 } else {
905 $currentPageId = MathUtility::convertToPositiveInteger($pageId);
906 }
907
908 $previewConfiguration = BackendUtility::getPagesTSconfig($currentPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
909
910 if (isset($previewConfiguration['previewPageId'])) {
911 $previewPageId = (int)$previewConfiguration['previewPageId'];
912 }
913 // if no preview page was configured
914 if (!$previewPageId) {
915 $rootPageData = null;
916 $rootLine = BackendUtility::BEgetRootLine($currentPageId);
917 $currentPage = reset($rootLine);
918 // Allow all doktypes below 200
919 // This makes custom doktype work as well with opening a frontend page.
920 if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) {
921 // try the current page
922 $previewPageId = $currentPageId;
923 } else {
924 // or search for the root page
925 foreach ($rootLine as $page) {
926 if ($page['is_siteroot']) {
927 $rootPageData = $page;
928 break;
929 }
930 }
931 $previewPageId = isset($rootPageData)
932 ? (int)$rootPageData['uid']
933 : $currentPageId;
934 }
935 }
936
937 $this->popViewId = $previewPageId;
938
939 return $previewPageId;
940 }
941
942 /**
943 * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
944 *
945 * This basically removes the trailing dots of sub-array keys in TypoScript.
946 * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
947 *
948 * @param array $parameters Should be an empty array by default
949 * @param array $typoScript The TypoScript configuration
950 */
951 protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
952 {
953 foreach ($typoScript as $key => $value) {
954 if (is_array($value)) {
955 $key = rtrim($key, '.');
956 $parameters[$key] = [];
957 $this->parseAdditionalGetParameters($parameters[$key], $value);
958 } else {
959 $parameters[$key] = $value;
960 }
961 }
962 }
963
964 /**
965 * Main module operation
966 *
967 * @param ServerRequestInterface $request
968 */
969 protected function main(ServerRequestInterface $request): void
970 {
971 $body = '';
972 // Begin edit
973 if (is_array($this->editconf)) {
974 $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
975
976 // Creating the editing form, wrap it with buttons, document selector etc.
977 $editForm = $this->makeEditForm();
978 if ($editForm) {
979 $this->firstEl = reset($this->elementsData);
980 // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
981 if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5]))
982 && !$this->dontStoreDocumentRef
983 ) {
984 $this->docHandler[$this->storeUrlMd5] = [
985 $this->storeTitle,
986 $this->storeArray,
987 $this->storeUrl,
988 $this->firstEl
989 ];
990 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
991 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
992 }
993 $body = $this->formResultCompiler->addCssFiles();
994 $body .= $this->compileForm($editForm);
995 $body .= $this->formResultCompiler->printNeededJSFunctions();
996 $body .= '</form>';
997 }
998 }
999 // Access check...
1000 // The page will show only if there is a valid page and if this page may be viewed by the user
1001 $this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
1002 if ($this->pageinfo) {
1003 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
1004 }
1005 // Setting up the buttons and markers for doc header
1006 $this->getButtons($request);
1007 $this->languageSwitch(
1008 (string)($this->firstEl['table'] ?? ''),
1009 (int)($this->firstEl['uid'] ?? 0),
1010 isset($this->firstEl['pid']) ? (int)$this->firstEl['pid'] : null
1011 );
1012 $this->moduleTemplate->setContent($body);
1013 }
1014
1015 /**
1016 * Creates the editing form with FormEngine, based on the input from GPvars.
1017 *
1018 * @return string HTML form elements wrapped in tables
1019 */
1020 protected function makeEditForm(): string
1021 {
1022 // Initialize variables
1023 $this->elementsData = [];
1024 $this->errorC = 0;
1025 $this->newC = 0;
1026 $editForm = '';
1027 $beUser = $this->getBackendUser();
1028 // Traverse the GPvar edit array tables
1029 foreach ($this->editconf as $table => $conf) {
1030 if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
1031 // Traverse the keys/comments of each table (keys can be a comma list of uids)
1032 foreach ($conf as $cKey => $command) {
1033 if ($command === 'edit' || $command === 'new') {
1034 // Get the ids:
1035 $ids = GeneralUtility::trimExplode(',', $cKey, true);
1036 // Traverse the ids:
1037 foreach ($ids as $theUid) {
1038 // Don't save this document title in the document selector if the document is new.
1039 if ($command === 'new') {
1040 $this->dontStoreDocumentRef = 1;
1041 }
1042
1043 try {
1044 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
1045 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
1046 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
1047
1048 // Reset viewId - it should hold data of last entry only
1049 $this->viewId = 0;
1050 $this->viewId_addParams = '';
1051
1052 $formDataCompilerInput = [
1053 'tableName' => $table,
1054 'vanillaUid' => (int)$theUid,
1055 'command' => $command,
1056 'returnUrl' => $this->R_URI,
1057 ];
1058 if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
1059 $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
1060 }
1061 if (!empty($this->defVals) && is_array($this->defVals)) {
1062 $formDataCompilerInput['defaultValues'] = $this->defVals;
1063 }
1064
1065 $formData = $formDataCompiler->compile($formDataCompilerInput);
1066
1067 // Set this->viewId if possible
1068 if ($command === 'new'
1069 && $table !== 'pages'
1070 && !empty($formData['parentPageRow']['uid'])
1071 ) {
1072 $this->viewId = $formData['parentPageRow']['uid'];
1073 } else {
1074 if ($table === 'pages') {
1075 $this->viewId = $formData['databaseRow']['uid'];
1076 } elseif (!empty($formData['parentPageRow']['uid'])) {
1077 $this->viewId = $formData['parentPageRow']['uid'];
1078 // Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
1079 if (!empty($formData['processedTca']['ctrl']['languageField'])
1080 && is_array($formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']])
1081 && $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0] > 0
1082 ) {
1083 $this->viewId_addParams = '&L=' . $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0];
1084 }
1085 }
1086 }
1087
1088 // Determine if delete button can be shown
1089 $deleteAccess = false;
1090 if (
1091 $command === 'edit'
1092 || $command === 'new'
1093 ) {
1094 $permission = $formData['userPermissionOnPage'];
1095 if ($formData['tableName'] === 'pages') {
1096 $deleteAccess = $permission & Permission::PAGE_DELETE ? true : false;
1097 } else {
1098 $deleteAccess = $permission & Permission::CONTENT_EDIT ? true : false;
1099 }
1100 }
1101
1102 // Display "is-locked" message
1103 if ($command === 'edit') {
1104 $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
1105 if ($lockInfo) {
1106 $flashMessage = GeneralUtility::makeInstance(
1107 FlashMessage::class,
1108 $lockInfo['msg'],
1109 '',
1110 FlashMessage::WARNING
1111 );
1112 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1113 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1114 $defaultFlashMessageQueue->enqueue($flashMessage);
1115 }
1116 }
1117
1118 // Record title
1119 if (!$this->storeTitle) {
1120 $this->storeTitle = $this->recTitle
1121 ? htmlspecialchars($this->recTitle)
1122 : BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($formData['databaseRow']), true);
1123 }
1124
1125 $this->elementsData[] = [
1126 'table' => $table,
1127 'uid' => $formData['databaseRow']['uid'],
1128 'pid' => $formData['databaseRow']['pid'],
1129 'cmd' => $command,
1130 'deleteAccess' => $deleteAccess
1131 ];
1132
1133 if ($command !== 'new') {
1134 BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
1135 }
1136
1137 // Set list if only specific fields should be rendered. This will trigger
1138 // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1139 if ($this->columnsOnly) {
1140 if (is_array($this->columnsOnly)) {
1141 $formData['fieldListToRender'] = $this->columnsOnly[$table];
1142 } else {
1143 $formData['fieldListToRender'] = $this->columnsOnly;
1144 }
1145 }
1146
1147 $formData['renderType'] = 'outerWrapContainer';
1148 $formResult = $nodeFactory->create($formData)->render();
1149
1150 $html = $formResult['html'];
1151
1152 $formResult['html'] = '';
1153 $formResult['doSaveFieldName'] = 'doSave';
1154
1155 // @todo: Put all the stuff into FormEngine as final "compiler" class
1156 // @todo: This is done here for now to not rewrite addCssFiles()
1157 // @todo: and printNeededJSFunctions() now
1158 $this->formResultCompiler->mergeResult($formResult);
1159
1160 // Seems the pid is set as hidden field (again) at end?!
1161 if ($command === 'new') {
1162 // @todo: looks ugly
1163 $html .= LF
1164 . '<input type="hidden"'
1165 . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($formData['databaseRow']['uid']) . '][pid]"'
1166 . ' value="' . (int)$formData['databaseRow']['pid'] . '" />';
1167 $this->newC++;
1168 }
1169
1170 $editForm .= $html;
1171 } catch (AccessDeniedException $e) {
1172 $this->errorC++;
1173 // Try to fetch error message from "recordInternals" be user object
1174 // @todo: This construct should be logged and localized and de-uglified
1175 $message = $beUser->errorMsg;
1176 if (empty($message)) {
1177 // Create message from exception.
1178 $message = $e->getMessage() . ' ' . $e->getCode();
1179 }
1180 $editForm .= htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission'))
1181 . '<br /><br />' . htmlspecialchars($message) . '<br /><br />';
1182 } catch (DatabaseRecordException $e) {
1183 $editForm = '<div class="alert alert-warning">' . htmlspecialchars($e->getMessage()) . '</div>';
1184 }
1185 } // End of for each uid
1186 }
1187 }
1188 }
1189 }
1190 return $editForm;
1191 }
1192
1193 /**
1194 * Create the panel of buttons for submitting the form or otherwise perform operations.
1195 *
1196 * @param ServerRequestInterface $request
1197 */
1198 protected function getButtons(ServerRequestInterface $request): void
1199 {
1200 $record = BackendUtility::getRecord($this->firstEl['table'], $this->firstEl['uid']);
1201 $TCActrl = $GLOBALS['TCA'][$this->firstEl['table']]['ctrl'];
1202
1203 $this->setIsSavedRecord();
1204
1205 $sysLanguageUid = 0;
1206 if (
1207 $this->isSavedRecord
1208 && isset($TCActrl['languageField'], $record[$TCActrl['languageField']])
1209 ) {
1210 $sysLanguageUid = (int)$record[$TCActrl['languageField']];
1211 } elseif (isset($this->defVals['sys_language_uid'])) {
1212 $sysLanguageUid = (int)$this->defVals['sys_language_uid'];
1213 }
1214
1215 $l18nParent = isset($TCActrl['transOrigPointerField'], $record[$TCActrl['transOrigPointerField']])
1216 ? (int)$record[$TCActrl['transOrigPointerField']]
1217 : 0;
1218
1219 $this->setIsPageInFreeTranslationMode($record, $sysLanguageUid);
1220
1221 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1222
1223 $this->registerCloseButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 1);
1224
1225 // Show buttons when table is not read-only
1226 if (
1227 !$this->errorC
1228 && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']
1229 ) {
1230 $this->registerSaveButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 2);
1231 $this->registerViewButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 3);
1232 $this->registerNewButtonToButtonBar(
1233 $buttonBar,
1234 ButtonBar::BUTTON_POSITION_LEFT,
1235 4,
1236 $sysLanguageUid,
1237 $l18nParent
1238 );
1239 $this->registerDuplicationButtonToButtonBar(
1240 $buttonBar,
1241 ButtonBar::BUTTON_POSITION_LEFT,
1242 5,
1243 $sysLanguageUid,
1244 $l18nParent
1245 );
1246 $this->registerDeleteButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 6);
1247 $this->registerColumnsOnlyButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 7);
1248 $this->registerHistoryButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1249 }
1250
1251 $this->registerOpenInNewWindowButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 2);
1252 $this->registerShortcutButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 3);
1253 $this->registerCshButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 4);
1254 }
1255
1256 /**
1257 * Set the boolean to check if the record is saved
1258 */
1259 protected function setIsSavedRecord()
1260 {
1261 if (!is_bool($this->isSavedRecord)) {
1262 $this->isSavedRecord = (
1263 $this->firstEl['cmd'] !== 'new'
1264 && MathUtility::canBeInterpretedAsInteger($this->firstEl['uid'])
1265 );
1266 }
1267 }
1268
1269 /**
1270 * Returns if inconsistent language handling is allowed
1271 *
1272 * @return bool
1273 */
1274 protected function isInconsistentLanguageHandlingAllowed(): bool
1275 {
1276 $allowInconsistentLanguageHandling = BackendUtility::getPagesTSconfig(
1277 $this->pageinfo['uid']
1278 )['mod']['web_layout']['allowInconsistentLanguageHandling'];
1279
1280 return $allowInconsistentLanguageHandling['value'] === '1';
1281 }
1282
1283 /**
1284 * Set the boolean to check if the page is in free translation mode
1285 *
1286 * @param array|null $record
1287 * @param int $sysLanguageUid
1288 */
1289 protected function setIsPageInFreeTranslationMode($record, int $sysLanguageUid)
1290 {
1291 if ($this->firstEl['table'] === 'tt_content') {
1292 if (!$this->isSavedRecord) {
1293 $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1294 (int)$this->pageinfo['uid'],
1295 (int)$this->defVals['colPos'],
1296 $sysLanguageUid
1297 );
1298 } else {
1299 $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1300 (int)$this->pageinfo['uid'],
1301 (int)$record['colPos'],
1302 $sysLanguageUid
1303 );
1304 }
1305 }
1306 }
1307
1308 /**
1309 * Check if the page is in free translation mode
1310 *
1311 * @param int $page
1312 * @param int $column
1313 * @param int $language
1314 * @return bool
1315 */
1316 protected function getFreeTranslationMode(int $page, int $column, int $language): bool
1317 {
1318 $freeTranslationMode = false;
1319
1320 if (
1321 $this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0
1322 && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0
1323 ) {
1324 $freeTranslationMode = true;
1325 }
1326
1327 return $freeTranslationMode;
1328 }
1329
1330 /**
1331 * Register the close button to the button bar
1332 *
1333 * @param ButtonBar $buttonBar
1334 * @param string $position
1335 * @param int $group
1336 */
1337 protected function registerCloseButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1338 {
1339 $closeButton = $buttonBar->makeLinkButton()
1340 ->setHref('#')
1341 ->setClasses('t3js-editform-close')
1342 ->setTitle($this->getLanguageService()->sL(
1343 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'
1344 ))
1345 ->setShowLabelText(true)
1346 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1347 'actions-close',
1348 Icon::SIZE_SMALL
1349 ));
1350
1351 $buttonBar->addButton($closeButton, $position, $group);
1352 }
1353
1354 /**
1355 * Register the save button to the button bar
1356 *
1357 * @param ButtonBar $buttonBar
1358 * @param string $position
1359 * @param int $group
1360 */
1361 protected function registerSaveButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1362 {
1363 $saveButton = $buttonBar->makeInputButton()
1364 ->setForm('EditDocumentController')
1365 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1366 ->setName('_savedok')
1367 ->setShowLabelText(true)
1368 ->setTitle($this->getLanguageService()->sL(
1369 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'
1370 ))
1371 ->setValue('1');
1372
1373 $buttonBar->addButton($saveButton, $position, $group);
1374 }
1375
1376 /**
1377 * Register the view button to the button bar
1378 *
1379 * @param ButtonBar $buttonBar
1380 * @param string $position
1381 * @param int $group
1382 */
1383 protected function registerViewButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1384 {
1385 if (
1386 $this->viewId // Pid to show the record
1387 && !$this->noView // Passed parameter
1388 && !empty($this->firstEl['table']) // No table
1389
1390 // @TODO: TsConfig option should change to viewDoc
1391 && $this->getTsConfigOption($this->firstEl['table'], 'saveDocView')
1392 ) {
1393 $classNames = 't3js-editform-view';
1394
1395 $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
1396
1397 if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1398 $excludeDokTypes = GeneralUtility::intExplode(
1399 ',',
1400 $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
1401 true
1402 );
1403 } else {
1404 // exclude sysfolders, spacers and recycler by default
1405 $excludeDokTypes = [
1406 PageRepository::DOKTYPE_RECYCLER,
1407 PageRepository::DOKTYPE_SYSFOLDER,
1408 PageRepository::DOKTYPE_SPACER
1409 ];
1410 }
1411
1412 if (
1413 !in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)
1414 || isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl['table'] . '.']['previewPageId'])
1415 ) {
1416 $previewPageId = $this->getPreviewPageId();
1417 $previewUrl = BackendUtility::getPreviewUrl(
1418 $previewPageId,
1419 '',
1420 BackendUtility::BEgetRootLine($previewPageId),
1421 $this->getPreviewUrlAnchorSection(),
1422 $this->viewUrl,
1423 $this->getPreviewUrlParameters($previewPageId)
1424 );
1425
1426 $viewButton = $buttonBar->makeLinkButton()
1427 ->setHref($previewUrl)
1428 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1429 'actions-view',
1430 Icon::SIZE_SMALL
1431 ))
1432 ->setShowLabelText(true)
1433 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc'));
1434
1435 if (!$this->isSavedRecord) {
1436 if ($this->firstEl['table'] === 'pages') {
1437 $viewButton->setDataAttributes(['is-new' => '']);
1438 }
1439 }
1440
1441 if ($classNames !== '') {
1442 $viewButton->setClasses($classNames);
1443 }
1444
1445 $buttonBar->addButton($viewButton, $position, $group);
1446 }
1447 }
1448 }
1449
1450 /**
1451 * Register the new button to the button bar
1452 *
1453 * @param ButtonBar $buttonBar
1454 * @param string $position
1455 * @param int $group
1456 * @param int $sysLanguageUid
1457 * @param int $l18nParent
1458 */
1459 protected function registerNewButtonToButtonBar(
1460 ButtonBar $buttonBar,
1461 string $position,
1462 int $group,
1463 int $sysLanguageUid,
1464 int $l18nParent
1465 ) {
1466 if (
1467 $this->firstEl['table'] !== 'sys_file_metadata'
1468 && !empty($this->firstEl['table'])
1469 && (
1470 (
1471 (
1472 $this->isInconsistentLanguageHandlingAllowed()
1473 || $this->isPageInFreeTranslationMode
1474 )
1475 && $this->firstEl['table'] === 'tt_content'
1476 )
1477 || (
1478 $this->firstEl['table'] !== 'tt_content'
1479 && (
1480 $sysLanguageUid === 0
1481 || $l18nParent === 0
1482 )
1483 )
1484 )
1485 ) {
1486 $classNames = 't3js-editform-new';
1487
1488 $newButton = $buttonBar->makeLinkButton()
1489 ->setHref('#')
1490 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1491 'actions-add',
1492 Icon::SIZE_SMALL
1493 ))
1494 ->setShowLabelText(true)
1495 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc'));
1496
1497 if (!$this->isSavedRecord) {
1498 $newButton->setDataAttributes(['is-new' => '']);
1499 }
1500
1501 if ($classNames !== '') {
1502 $newButton->setClasses($classNames);
1503 }
1504
1505 $buttonBar->addButton($newButton, $position, $group);
1506 }
1507 }
1508
1509 /**
1510 * Register the duplication button to the button bar
1511 *
1512 * @param ButtonBar $buttonBar
1513 * @param string $position
1514 * @param int $group
1515 * @param int $sysLanguageUid
1516 * @param int $l18nParent
1517 */
1518 protected function registerDuplicationButtonToButtonBar(
1519 ButtonBar $buttonBar,
1520 string $position,
1521 int $group,
1522 int $sysLanguageUid,
1523 int $l18nParent
1524 ) {
1525 if (
1526 $this->firstEl['table'] !== 'sys_file_metadata'
1527 && !empty($this->firstEl['table'])
1528 && (
1529 (
1530 (
1531 $this->isInconsistentLanguageHandlingAllowed()
1532 || $this->isPageInFreeTranslationMode
1533 )
1534 && $this->firstEl['table'] === 'tt_content'
1535 )
1536 || (
1537 $this->firstEl['table'] !== 'tt_content'
1538 && (
1539 $sysLanguageUid === 0
1540 || $l18nParent === 0
1541 )
1542 )
1543 )
1544 && $this->getTsConfigOption($this->firstEl['table'], 'showDuplicate')
1545 ) {
1546 $classNames = 't3js-editform-duplicate';
1547
1548 $duplicateButton = $buttonBar->makeLinkButton()
1549 ->setHref('#')
1550 ->setShowLabelText(true)
1551 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc'))
1552 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1553 'actions-document-duplicates-select',
1554 Icon::SIZE_SMALL
1555 ));
1556
1557 if (!$this->isSavedRecord) {
1558 $duplicateButton->setDataAttributes(['is-new' => '']);
1559 }
1560
1561 if ($classNames !== '') {
1562 $duplicateButton->setClasses($classNames);
1563 }
1564
1565 $buttonBar->addButton($duplicateButton, $position, $group);
1566 }
1567 }
1568
1569 /**
1570 * Register the delete button to the button bar
1571 *
1572 * @param ButtonBar $buttonBar
1573 * @param string $position
1574 * @param int $group
1575 */
1576 protected function registerDeleteButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1577 {
1578 if (
1579 $this->firstEl['deleteAccess']
1580 && !$this->getDisableDelete()
1581 && $this->isSavedRecord
1582 && count($this->elementsData) === 1
1583 ) {
1584 $classNames = 't3js-editform-delete-record';
1585
1586 $returnUrl = $this->retUrl;
1587 if ($this->firstEl['table'] === 'pages') {
1588 parse_str((string)parse_url($returnUrl, PHP_URL_QUERY), $queryParams);
1589 if (
1590 isset($queryParams['route'], $queryParams['id'])
1591 && (string)$this->firstEl['uid'] === (string)$queryParams['id']
1592 ) {
1593
1594 /** @var UriBuilder $uriBuilder */
1595 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1596
1597 // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
1598 // tree from the outside to be able to mark the pid as active
1599 $returnUrl = (string)$uriBuilder->buildUriFromRoutePath($queryParams['route'], ['id' => 0]);
1600 }
1601 }
1602
1603 /** @var ReferenceIndex $referenceIndex */
1604 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1605 $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords(
1606 $this->firstEl['table'],
1607 (int)$this->firstEl['uid']
1608 );
1609
1610 $referenceCountMessage = BackendUtility::referenceCount(
1611 $this->firstEl['table'],
1612 (int)$this->firstEl['uid'],
1613 $this->getLanguageService()->sL(
1614 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'
1615 ),
1616 $numberOfReferences
1617 );
1618 $translationCountMessage = BackendUtility::translationCount(
1619 $this->firstEl['table'],
1620 (int)$this->firstEl['uid'],
1621 $this->getLanguageService()->sL(
1622 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'
1623 )
1624 );
1625
1626 $deleteButton = $buttonBar->makeLinkButton()
1627 ->setClasses($classNames)
1628 ->setDataAttributes([
1629 'return-url' => $returnUrl,
1630 'uid' => $this->firstEl['uid'],
1631 'table' => $this->firstEl['table'],
1632 'reference-count-message' => $referenceCountMessage,
1633 'translation-count-message' => $translationCountMessage
1634 ])
1635 ->setHref('#')
1636 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1637 'actions-edit-delete',
1638 Icon::SIZE_SMALL
1639 ))
1640 ->setShowLabelText(true)
1641 ->setTitle($this->getLanguageService()->getLL('deleteItem'));
1642
1643 $buttonBar->addButton($deleteButton, $position, $group);
1644 }
1645 }
1646
1647 /**
1648 * Register the history button to the button bar
1649 *
1650 * @param ButtonBar $buttonBar
1651 * @param string $position
1652 * @param int $group
1653 */
1654 protected function registerHistoryButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1655 {
1656 if (
1657 count($this->elementsData) === 1
1658 && !empty($this->firstEl['table'])
1659 && $this->getTsConfigOption($this->firstEl['table'], 'showHistory')
1660 ) {
1661 /** @var UriBuilder $uriBuilder */
1662 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1663
1664 $historyButtonOnClick = 'window.location.href=' .
1665 GeneralUtility::quoteJSvalue(
1666 (string)$uriBuilder->buildUriFromRoute(
1667 'record_history',
1668 [
1669 'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1670 'returnUrl' => $this->R_URI,
1671 ]
1672 )
1673 ) . '; return false;';
1674
1675 $historyButton = $buttonBar->makeLinkButton()
1676 ->setHref('#')
1677 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1678 'actions-document-history-open',
1679 Icon::SIZE_SMALL
1680 ))
1681 ->setOnClick($historyButtonOnClick)
1682 ->setTitle('Open history of this record')
1683 ;
1684
1685 $buttonBar->addButton($historyButton, $position, $group);
1686 }
1687 }
1688
1689 /**
1690 * Register the columns only button to the button bar
1691 *
1692 * @param ButtonBar $buttonBar
1693 * @param string $position
1694 * @param int $group
1695 */
1696 protected function registerColumnsOnlyButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1697 {
1698 if (
1699 $this->columnsOnly
1700 && count($this->elementsData) === 1
1701 ) {
1702 $columnsOnlyButton = $buttonBar->makeLinkButton()
1703 ->setHref($this->R_URI . '&columnsOnly=')
1704 ->setTitle($this->getLanguageService()->getLL('editWholeRecord'))
1705 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1706 'actions-open',
1707 Icon::SIZE_SMALL
1708 ));
1709
1710 $buttonBar->addButton($columnsOnlyButton, $position, $group);
1711 }
1712 }
1713
1714 /**
1715 * Register the open in new window button to the button bar
1716 *
1717 * @param ButtonBar $buttonBar
1718 * @param string $position
1719 * @param int $group
1720 */
1721 protected function registerOpenInNewWindowButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1722 {
1723 $closeUrl = $this->getCloseUrl();
1724 if ($this->returnUrl !== $closeUrl) {
1725 $requestUri = GeneralUtility::linkThisScript([
1726 'returnUrl' => $closeUrl,
1727 ]);
1728 $aOnClick = 'vHWin=window.open('
1729 . GeneralUtility::quoteJSvalue($requestUri) . ','
1730 . GeneralUtility::quoteJSvalue(md5($this->R_URI))
1731 . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1732
1733 $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
1734 ->makeLinkButton()
1735 ->setHref('#')
1736 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
1737 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL))
1738 ->setOnClick($aOnClick);
1739
1740 $buttonBar->addButton($openInNewWindowButton, $position, $group);
1741 }
1742 }
1743
1744 /**
1745 * Register the shortcut button to the button bar
1746 *
1747 * @param ButtonBar $buttonBar
1748 * @param string $position
1749 * @param int $group
1750 */
1751 protected function registerShortcutButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1752 {
1753 if ($this->returnUrl !== $this->getCloseUrl()) {
1754 $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
1755 $shortCutButton->setModuleName('xMOD_alt_doc.php')
1756 ->setGetVariables([
1757 'returnUrl',
1758 'edit',
1759 'defVals',
1760 'overrideVals',
1761 'columnsOnly',
1762 'returnNewPageId',
1763 'noView']);
1764
1765 $buttonBar->addButton($shortCutButton, $position, $group);
1766 }
1767 }
1768
1769 /**
1770 * Register the CSH button to the button bar
1771 *
1772 * @param ButtonBar $buttonBar
1773 * @param string $position
1774 * @param int $group
1775 */
1776 protected function registerCshButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1777 {
1778 $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('TCEforms');
1779
1780 $buttonBar->addButton($cshButton, $position, $group);
1781 }
1782
1783 /**
1784 * Get the count of connected translated content elements
1785 *
1786 * @param int $page
1787 * @param int $column
1788 * @param int $language
1789 * @return int
1790 */
1791 protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int
1792 {
1793 $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1794
1795 return (int)$queryBuilder
1796 ->andWhere(
1797 $queryBuilder->expr()->gt(
1798 $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1799 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1800 )
1801 )
1802 ->execute()
1803 ->fetchColumn(0);
1804 }
1805
1806 /**
1807 * Get the count of standalone translated content elements
1808 *
1809 * @param int $page
1810 * @param int $column
1811 * @param int $language
1812 * @return int
1813 */
1814 protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int
1815 {
1816 $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1817
1818 return (int)$queryBuilder
1819 ->andWhere(
1820 $queryBuilder->expr()->eq(
1821 $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1822 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1823 )
1824 )
1825 ->execute()
1826 ->fetchColumn(0);
1827 }
1828
1829 /**
1830 * Get the query builder for the translation mode
1831 *
1832 * @param int $page
1833 * @param int $column
1834 * @param int $language
1835 * @return QueryBuilder
1836 */
1837 protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder
1838 {
1839 $languageField = $GLOBALS['TCA']['tt_content']['ctrl']['languageField'];
1840
1841 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1842 ->getQueryBuilderForTable('tt_content');
1843
1844 $queryBuilder->getRestrictions()
1845 ->removeAll()
1846 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1847 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1848
1849 return $queryBuilder
1850 ->count('uid')
1851 ->from('tt_content')
1852 ->where(
1853 $queryBuilder->expr()->eq(
1854 'pid',
1855 $queryBuilder->createNamedParameter($page, \PDO::PARAM_INT)
1856 ),
1857 $queryBuilder->expr()->eq(
1858 $languageField,
1859 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
1860 ),
1861 $queryBuilder->expr()->eq(
1862 'colPos',
1863 $queryBuilder->createNamedParameter($column, \PDO::PARAM_INT)
1864 )
1865 );
1866 }
1867
1868 /**
1869 * Put together the various elements (buttons, selectors, form) into a table
1870 *
1871 * @param string $editForm HTML form.
1872 * @return string Composite HTML
1873 */
1874 protected function compileForm(string $editForm): string
1875 {
1876 $formContent = '
1877 <form
1878 action="' . htmlspecialchars($this->R_URI) . '"
1879 method="post"
1880 enctype="multipart/form-data"
1881 name="editform"
1882 id="EditDocumentController"
1883 onsubmit="TBE_EDITOR.checkAndDoSubmit(1); return false;"
1884 >
1885 ' . $editForm . '
1886 <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
1887 <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />
1888 <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$this->viewId) . '" />
1889 <input type="hidden" name="closeDoc" value="0" />
1890 <input type="hidden" name="doSave" value="0" />
1891 <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
1892 <input type="hidden" name="_scrollPosition" value="" />';
1893 if ($this->returnNewPageId) {
1894 $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
1895 }
1896 if ($this->viewId_addParams) {
1897 $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams) . '" />';
1898 }
1899 return $formContent;
1900 }
1901
1902 /**
1903 * Returns if delete for the current table is disabled by configuration.
1904 * For sys_file_metadata in default language delete is always disabled.
1905 *
1906 * @return bool
1907 */
1908 protected function getDisableDelete(): bool
1909 {
1910 $disableDelete = false;
1911 if ($this->firstEl['table'] === 'sys_file_metadata') {
1912 $row = BackendUtility::getRecord('sys_file_metadata', $this->firstEl['uid'], 'sys_language_uid');
1913 $languageUid = $row['sys_language_uid'];
1914 if ($languageUid === 0) {
1915 $disableDelete = true;
1916 }
1917 } else {
1918 $disableDelete = (bool)$this->getTsConfigOption($this->firstEl['table'] ?? '', 'disableDelete');
1919 }
1920 return $disableDelete;
1921 }
1922
1923 /**
1924 * Returns the URL (usually for the "returnUrl") which closes the current window.
1925 * Used when editing a record in a popup.
1926 *
1927 * @return string
1928 */
1929 protected function getCloseUrl(): string
1930 {
1931 $closeUrl = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
1932 return PathUtility::getAbsoluteWebPath($closeUrl);
1933 }
1934
1935 /***************************
1936 *
1937 * Localization stuff
1938 *
1939 ***************************/
1940 /**
1941 * Make selector box for creating new translation for a record or switching to edit the record in an existing
1942 * language.
1943 * Displays only languages which are available for the current page.
1944 *
1945 * @param string $table Table name
1946 * @param int $uid Uid for which to create a new language
1947 * @param int $pid|null Pid of the record
1948 */
1949 protected function languageSwitch(string $table, int $uid, $pid = null)
1950 {
1951 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1952 $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1953 /** @var UriBuilder $uriBuilder */
1954 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1955
1956 // Table editable and activated for languages?
1957 if ($this->getBackendUser()->check('tables_modify', $table)
1958 && $languageField
1959 && $transOrigPointerField
1960 ) {
1961 if ($pid === null) {
1962 $row = BackendUtility::getRecord($table, $uid, 'pid');
1963 $pid = $row['pid'];
1964 }
1965 // Get all available languages for the page
1966 // If editing a page, the translations of the current UID need to be fetched
1967 if ($table === 'pages') {
1968 $row = BackendUtility::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
1969 // Ensure the check is always done against the default language page
1970 $availableLanguages = $this->getLanguages(
1971 (int)$row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $uid,
1972 $table
1973 );
1974 } else {
1975 $availableLanguages = $this->getLanguages((int)$pid, $table);
1976 }
1977 // Page available in other languages than default language?
1978 if (count($availableLanguages) > 1) {
1979 $rowsByLang = [];
1980 $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
1981 // Get record in current language
1982 $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
1983 if (!is_array($rowCurrent)) {
1984 $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
1985 }
1986 $currentLanguage = (int)$rowCurrent[$languageField];
1987 // Disabled for records with [all] language!
1988 if ($currentLanguage > -1) {
1989 // Get record in default language if needed
1990 if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
1991 $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
1992 $table,
1993 $rowCurrent[$transOrigPointerField],
1994 $fetchFields
1995 );
1996 if (!is_array($rowsByLang[0])) {
1997 $rowsByLang[0] = BackendUtility::getRecord(
1998 $table,
1999 $rowCurrent[$transOrigPointerField],
2000 $fetchFields
2001 );
2002 }
2003 } else {
2004 $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
2005 }
2006 if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
2007 // Get record in other languages to see what's already available
2008
2009 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2010 ->getQueryBuilderForTable($table);
2011
2012 $queryBuilder->getRestrictions()
2013 ->removeAll()
2014 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2015 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2016
2017 $result = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fetchFields, true))
2018 ->from($table)
2019 ->where(
2020 $queryBuilder->expr()->eq(
2021 'pid',
2022 $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
2023 ),
2024 $queryBuilder->expr()->gt(
2025 $languageField,
2026 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2027 ),
2028 $queryBuilder->expr()->eq(
2029 $transOrigPointerField,
2030 $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO::PARAM_INT)
2031 )
2032 )
2033 ->execute();
2034
2035 while ($row = $result->fetch()) {
2036 $rowsByLang[$row[$languageField]] = $row;
2037 }
2038 }
2039 $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
2040 $languageMenu->setIdentifier('_langSelector');
2041 foreach ($availableLanguages as $language) {
2042 $languageId = $language->getLanguageId();
2043 $selectorOptionLabel = $language->getTitle();
2044 // Create url for creating a localized record
2045 $addOption = true;
2046 $href = '';
2047 if (!isset($rowsByLang[$languageId])) {
2048 // Translation in this language does not exist
2049 $selectorOptionLabel .= ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
2050 $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2051 'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $languageId,
2052 'returnUrl' => $this->retUrl
2053 ]);
2054
2055 if (array_key_exists(0, $rowsByLang)) {
2056 $href = BackendUtility::getLinkToDataHandlerAction(
2057 '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $languageId,
2058 $redirectUrl
2059 );
2060 } else {
2061 $addOption = false;
2062 }
2063 } else {
2064 $params = [
2065 'edit[' . $table . '][' . $rowsByLang[$languageId]['uid'] . ']' => 'edit',
2066 'returnUrl' => $this->retUrl
2067 ];
2068 if ($table === 'pages') {
2069 // Disallow manual adjustment of the language field for pages
2070 $params['overrideVals'] = [
2071 'pages' => [
2072 'sys_language_uid' => $languageId
2073 ]
2074 ];
2075 }
2076 $href = (string)$uriBuilder->buildUriFromRoute('record_edit', $params);
2077 }
2078 if ($addOption) {
2079 $menuItem = $languageMenu->makeMenuItem()
2080 ->setTitle($selectorOptionLabel)
2081 ->setHref($href);
2082 if ($languageId === $currentLanguage) {
2083 $menuItem->setActive(true);
2084 }
2085 $languageMenu->addMenuItem($menuItem);
2086 }
2087 }
2088 $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
2089 }
2090 }
2091 }
2092 }
2093
2094 /**
2095 * Redirects to FormEngine with new parameters to edit a just created localized record
2096 *
2097 * @param ServerRequestInterface $request Incoming request object
2098 * @return ResponseInterface|null Possible redirect response
2099 */
2100 protected function localizationRedirect(ServerRequestInterface $request): ?ResponseInterface
2101 {
2102 $justLocalized = $request->getQueryParams()['justLocalized'];
2103
2104 if (empty($justLocalized)) {
2105 return null;
2106 }
2107
2108 list($table, $origUid, $language) = explode(':', $justLocalized);
2109
2110 if ($GLOBALS['TCA'][$table]
2111 && $GLOBALS['TCA'][$table]['ctrl']['languageField']
2112 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
2113 ) {
2114 $parsedBody = $request->getParsedBody();
2115 $queryParams = $request->getQueryParams();
2116
2117 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2118 $queryBuilder->getRestrictions()
2119 ->removeAll()
2120 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2121 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2122 $localizedRecord = $queryBuilder->select('uid')
2123 ->from($table)
2124 ->where(
2125 $queryBuilder->expr()->eq(
2126 $GLOBALS['TCA'][$table]['ctrl']['languageField'],
2127 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
2128 ),
2129 $queryBuilder->expr()->eq(
2130 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2131 $queryBuilder->createNamedParameter($origUid, \PDO::PARAM_INT)
2132 )
2133 )
2134 ->execute()
2135 ->fetch();
2136 $returnUrl = $parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '';
2137 if (is_array($localizedRecord)) {
2138 // Create redirect response to self to edit just created record
2139 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2140 return new RedirectResponse(
2141 (string)$uriBuilder->buildUriFromRoute(
2142 'record_edit',
2143 [
2144 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2145 'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2146 ]
2147 ),
2148 303
2149 );
2150 }
2151 }
2152 return null;
2153 }
2154
2155 /**
2156 * Returns languages available for record translations on given page.
2157 *
2158 * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
2159 * hidden. If set to another value, the query will select all sys_language records that has a
2160 * translation record on that page (and is not hidden, unless you are admin user)
2161 * @param string $table For pages we want all languages, for other records the languages of the page translations
2162 * @return SiteLanguage[] Language
2163 */
2164 protected function getLanguages(int $id, string $table): array
2165 {
2166 // This usually happens when a non-pages record is added after another, so we are fetching the proper page ID
2167 if ($id < 0 && $table !== 'pages') {
2168 $pageId = $this->pageinfo['uid'] ?? null;
2169 if ($pageId !== null) {
2170 $pageId = (int)$pageId;
2171 } else {
2172 $fullRecord = BackendUtility::getRecord($table, abs($id));
2173 $pageId = (int)$fullRecord['pid'];
2174 }
2175 } else {
2176 $pageId = $id;
2177 }
2178 $site = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId($pageId);
2179
2180 // Fetch the current translations of this page, to only show the ones where there is a page translation
2181 $allLanguages = $site->getAvailableLanguages($this->getBackendUser(), false, $pageId);
2182 if ($table !== 'pages' && $id > 0) {
2183 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
2184 $queryBuilder->getRestrictions()->removeAll()
2185 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2186 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2187 $statement = $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
2188 ->from('pages')
2189 ->where(
2190 $queryBuilder->expr()->eq(
2191 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2192 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
2193 )
2194 )
2195 ->execute();
2196
2197 $availableLanguages = [];
2198
2199 if ($allLanguages[0] ?? false) {
2200 $availableLanguages = [
2201 0 => $allLanguages[0]
2202 ];
2203 }
2204
2205 while ($row = $statement->fetch()) {
2206 $languageId = (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
2207 if (isset($allLanguages[$languageId])) {
2208 $availableLanguages[$languageId] = $allLanguages[$languageId];
2209 }
2210 }
2211 return $availableLanguages;
2212 }
2213 return $allLanguages;
2214 }
2215
2216 /**
2217 * Fix $this->editconf if versioning applies to any of the records
2218 *
2219 * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
2220 */
2221 protected function fixWSversioningInEditConf($mapArray = false): void
2222 {
2223 // Traverse the editConf array
2224 if (is_array($this->editconf)) {
2225 // Tables:
2226 foreach ($this->editconf as $table => $conf) {
2227 if (is_array($conf) && $GLOBALS['TCA'][$table]) {
2228 // Traverse the keys/comments of each table (keys can be a comma list of uids)
2229 $newConf = [];
2230 foreach ($conf as $cKey => $cmd) {
2231 if ($cmd === 'edit') {
2232 // Traverse the ids:
2233 $ids = GeneralUtility::trimExplode(',', $cKey, true);
2234 foreach ($ids as $idKey => $theUid) {
2235 if (is_array($mapArray)) {
2236 if ($mapArray[$table][$theUid]) {
2237 $ids[$idKey] = $mapArray[$table][$theUid];
2238 }
2239 } else {
2240 // Default, look for versions in workspace for record:
2241 $calcPRec = $this->getRecordForEdit((string)$table, (int)$theUid);
2242 if (is_array($calcPRec)) {
2243 // Setting UID again if it had changed, eg. due to workspace versioning.
2244 $ids[$idKey] = $calcPRec['uid'];
2245 }
2246 }
2247 }
2248 // Add the possibly manipulated IDs to the new-build newConf array:
2249 $newConf[implode(',', $ids)] = $cmd;
2250 } else {
2251 $newConf[$cKey] = $cmd;
2252 }
2253 }
2254 // Store the new conf array:
2255 $this->editconf[$table] = $newConf;
2256 }
2257 }
2258 }
2259 }
2260
2261 /**
2262 * Get record for editing.
2263 *
2264 * @param string $table Table name
2265 * @param int $theUid Record UID
2266 * @return array|false Returns record to edit, false if none
2267 */
2268 protected function getRecordForEdit(string $table, int $theUid)
2269 {
2270 // Fetch requested record:
2271 $reqRecord = BackendUtility::getRecord($table, $theUid, 'uid,pid');
2272 if (is_array($reqRecord)) {
2273 // If workspace is OFFLINE:
2274 if ($this->getBackendUser()->workspace != 0) {
2275 // Check for versioning support of the table:
2276 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
2277 // If the record is already a version of "something" pass it by.
2278 if ($reqRecord['pid'] == -1) {
2279 // (If it turns out not to be a version of the current workspace there will be trouble, but
2280 // that is handled inside DataHandler then and in the interface it would clearly be an error of
2281 // links if the user accesses such a scenario)
2282 return $reqRecord;
2283 }
2284 // The input record was online and an offline version must be found or made:
2285 // Look for version of this workspace:
2286 $versionRec = BackendUtility::getWorkspaceVersionOfRecord(
2287 $this->getBackendUser()->workspace,
2288 $table,
2289 $reqRecord['uid'],
2290 'uid,pid,t3ver_oid'
2291 );
2292 return is_array($versionRec) ? $versionRec : $reqRecord;
2293 }
2294 // This means that editing cannot occur on this record because it was not supporting versioning
2295 // which is required inside an offline workspace.
2296 return false;
2297 }
2298 // In ONLINE workspace, just return the originally requested record:
2299 return $reqRecord;
2300 }
2301 // Return FALSE because the table/uid was not found anyway.
2302 return false;
2303 }
2304
2305 /**
2306 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2307 * to prepare 'open documents' urls
2308 */
2309 protected function compileStoreData(): void
2310 {
2311 // @todo: Refactor in TYPO3 v10.0: This GeneralUtility method fiddles with _GP()
2312 $this->storeArray = GeneralUtility::compileSelectedGetVarsFromArray(
2313 'edit,defVals,overrideVals,columnsOnly,noView,workspace',
2314 $this->R_URL_getvars
2315 );
2316 $this->storeUrl = HttpUtility::buildQueryString($this->storeArray, '&');
2317 $this->storeUrlMd5 = md5($this->storeUrl);
2318 }
2319
2320 /**
2321 * Get a TSConfig 'option.' array, possibly for a specific table.
2322 *
2323 * @param string $table Table name
2324 * @param string $key Options key
2325 * @return string
2326 */
2327 protected function getTsConfigOption(string $table, string $key): string
2328 {
2329 return \trim((string)(
2330 $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table]
2331 ?? $this->getBackendUser()->getTSConfig()['options.'][$key]
2332 ?? ''
2333 ));
2334 }
2335
2336 /**
2337 * Handling the closing of a document
2338 * The argument $mode can be one of this values:
2339 * - 0/1 will redirect to $this->retUrl [self::DOCUMENT_CLOSE_MODE_DEFAULT || self::DOCUMENT_CLOSE_MODE_REDIRECT]
2340 * - 3 will clear the docHandler (thus closing all documents) [self::DOCUMENT_CLOSE_MODE_CLEAR_ALL]
2341 * - 4 will do no redirect [self::DOCUMENT_CLOSE_MODE_NO_REDIRECT]
2342 * - other values will call setDocument with ->retUrl
2343 *
2344 * @param int $mode the close mode: one of self::DOCUMENT_CLOSE_MODE_*
2345 * @param ServerRequestInterface $request Incoming request
2346 * @return ResponseInterface|null Redirect response if needed
2347 */
2348 protected function closeDocument($mode, ServerRequestInterface $request): ?ResponseInterface
2349 {
2350 $mode = (int)$mode;
2351 // If current document is found in docHandler,
2352 // then unset it, possibly unset it ALL and finally, write it to the session data
2353 if (isset($this->docHandler[$this->storeUrlMd5])) {
2354 // add the closing document to the recent documents
2355 $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
2356 if (!is_array($recentDocs)) {
2357 $recentDocs = [];
2358 }
2359 $closedDoc = $this->docHandler[$this->storeUrlMd5];
2360 $recentDocs = array_merge([$this->storeUrlMd5 => $closedDoc], $recentDocs);
2361 if (count($recentDocs) > 8) {
2362 $recentDocs = array_slice($recentDocs, 0, 8);
2363 }
2364 // remove it from the list of the open documents
2365 unset($this->docHandler[$this->storeUrlMd5]);
2366 if ($mode === self::DOCUMENT_CLOSE_MODE_CLEAR_ALL) {
2367 $recentDocs = array_merge($this->docHandler, $recentDocs);
2368 $this->docHandler = [];
2369 }
2370 $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
2371 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->docDat[1]]);
2372 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
2373 }
2374 if ($mode === self::DOCUMENT_CLOSE_MODE_NO_REDIRECT) {
2375 return null;
2376 }
2377 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2378 // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by
2379 // other scripts, like wizard_add, to know which records was created or so...
2380 if ($this->returnEditConf && $this->retUrl != (string)$uriBuilder->buildUriFromRoute('dummy')) {
2381 $this->retUrl .= '&returnEditConf=' . rawurlencode(json_encode($this->editconf));
2382 }
2383 // If mode is NOT set (means 0) OR set to 1, then make a header location redirect to $this->retUrl
2384 if ($mode === self::DOCUMENT_CLOSE_MODE_DEFAULT || $mode === self::DOCUMENT_CLOSE_MODE_REDIRECT) {
2385 return new RedirectResponse($this->retUrl, 303);
2386 }
2387 if ($this->retUrl === '') {
2388 return null;
2389 }
2390 $retUrl = $this->returnUrl;
2391 if (is_array($this->docHandler) && !empty($this->docHandler)) {
2392 if (!empty($setupArr[2])) {
2393 $sParts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
2394 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2395 }
2396 }
2397 return new RedirectResponse($retUrl, 303);
2398 }
2399
2400 /**
2401 * Emits a signal after a function was executed
2402 *
2403 * @param string $signalName
2404 * @param ServerRequestInterface $request
2405 */
2406 protected function emitFunctionAfterSignal($signalName, ServerRequestInterface $request): void
2407 {
2408 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, $signalName . 'After', [$this, 'request' => $request]);
2409 }
2410
2411 /**
2412 * Get the SignalSlot dispatcher
2413 *
2414 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
2415 */
2416 protected function getSignalSlotDispatcher()
2417 {
2418 if (!isset($this->signalSlotDispatcher)) {
2419 $this->signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
2420 }
2421 return $this->signalSlotDispatcher;
2422 }
2423
2424 /**
2425 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2426 */
2427 protected function getBackendUser()
2428 {
2429 return $GLOBALS['BE_USER'];
2430 }
2431
2432 /**
2433 * Returns LanguageService
2434 *
2435 * @return \TYPO3\CMS\Core\Localization\LanguageService
2436 */
2437 protected function getLanguageService()
2438 {
2439 return $GLOBALS['LANG'];
2440 }
2441 }