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