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