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