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