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