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