58eee95ba3085a91d005bc4546f0dfc278321af0
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / EditDocumentController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Backend\Controller;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
21 use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
22 use TYPO3\CMS\Backend\Form\FormDataCompiler;
23 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
24 use TYPO3\CMS\Backend\Form\FormResultCompiler;
25 use TYPO3\CMS\Backend\Form\NodeFactory;
26 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
27 use TYPO3\CMS\Backend\Routing\UriBuilder;
28 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
29 use TYPO3\CMS\Backend\Template\ModuleTemplate;
30 use TYPO3\CMS\Backend\Utility\BackendUtility;
31 use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
32 use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
33 use TYPO3\CMS\Core\Database\ConnectionPool;
34 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
35 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
36 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
37 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
38 use TYPO3\CMS\Core\Database\ReferenceIndex;
39 use TYPO3\CMS\Core\DataHandling\DataHandler;
40 use TYPO3\CMS\Core\Http\HtmlResponse;
41 use TYPO3\CMS\Core\Http\RedirectResponse;
42 use TYPO3\CMS\Core\Imaging\Icon;
43 use TYPO3\CMS\Core\Messaging\FlashMessage;
44 use TYPO3\CMS\Core\Messaging\FlashMessageService;
45 use TYPO3\CMS\Core\Page\PageRenderer;
46 use TYPO3\CMS\Core\Type\Bitmask\Permission;
47 use TYPO3\CMS\Core\Utility\GeneralUtility;
48 use TYPO3\CMS\Core\Utility\HttpUtility;
49 use TYPO3\CMS\Core\Utility\MathUtility;
50 use TYPO3\CMS\Core\Utility\PathUtility;
51 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
52 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
53 use TYPO3\CMS\Frontend\Page\PageRepository;
54
55 /**
56 * Main backend controller almost always used if some database record is edited in the backend.
57 *
58 * Main job of this controller is to evaluate and sanitize $request parameters,
59 * call the DataHandler if records should be created or updated and
60 * execute FormEngine for record rendering.
61 */
62 class EditDocumentController
63 {
64 use PublicMethodDeprecationTrait;
65 use PublicPropertyDeprecationTrait;
66
67 /**
68 * @deprecated since v9. These constants will be set to protected in v10
69 */
70 public const DOCUMENT_CLOSE_MODE_DEFAULT = 0;
71 public const DOCUMENT_CLOSE_MODE_REDIRECT = 1; // works like DOCUMENT_CLOSE_MODE_DEFAULT
72 public const DOCUMENT_CLOSE_MODE_CLEAR_ALL = 3;
73 public const DOCUMENT_CLOSE_MODE_NO_REDIRECT = 4;
74
75 /**
76 * @var array
77 */
78 private $deprecatedPublicMethods = [
79 'makeEditForm' => 'Using EditDocumentController::makeEditForm() is deprecated and will not be possible anymore in TYPO3 v10.',
80 'compileForm' => 'Using EditDocumentController::compileForm() is deprecated and will not be possible anymore in TYPO3 v10.',
81 'languageSwitch' => 'Using EditDocumentController::languageSwitch() is deprecated and will not be possible anymore in TYPO3 v10.',
82 'getLanguages' => 'Using EditDocumentController::getLanguages() is deprecated and will not be possible anymore in TYPO3 v10.',
83 'fixWSversioningInEditConf' => 'Using EditDocumentController::fixWSversioningInEditConf() is deprecated and will not be possible anymore in TYPO3 v10.',
84 'getRecordForEdit' => 'Using EditDocumentController::getRecordForEdit() is deprecated and will not be possible anymore in TYPO3 v10.',
85 ];
86
87 /**
88 * Properties which have been moved to protected status from public
89 *
90 * @var array
91 */
92 private $deprecatedPublicProperties = [
93 'editconf' => 'Using $editconf of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
94 'defVals' => 'Using $defVals of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
95 'overrideVals' => 'Using $overrideVals of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
96 'columnsOnly' => 'Using $columnsOnly of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
97 'returnUrl' => 'Using $returnUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
98 'closeDoc' => 'Using $closeDoc of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
99 'doSave' => 'Using $doSave of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
100 'returnEditConf' => 'Using $returnEditConf of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
101 'uc' => 'Using $uc of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
102 'retUrl' => 'Using $retUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
103 'R_URL_parts' => 'Using $R_URL_parts of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
104 'R_URL_getvars' => 'Using $R_URL_getvars of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
105 'storeArray' => 'Using $storeArray of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
106 'storeUrl' => 'Using $storeUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
107 'storeUrlMd5' => 'Using $storeUrlMd5 of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
108 'docDat' => 'Using $docDat of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
109 'docHandler' => 'Using $docHandler of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
110 'cmd' => 'Using $cmd of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
111 'mirror' => 'Using $mirror of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
112 'cacheCmd' => 'Using $cacheCmd of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
113 'redirect' => 'Using $redirect of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
114 'returnNewPageId' => 'Using $returnNewPageId of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
115 'popViewId' => 'Using $popViewId of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
116 'popViewId_addParams' => 'Using $popViewId_addParams of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
117 'viewUrl' => 'Using $viewUrl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
118 'recTitle' => 'Using $recTitle of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
119 'noView' => 'Using $noView of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
120 'MCONF' => 'Using $MCONF of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
121 'doc' => 'Using $doc of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
122 'perms_clause' => 'Using $perms_clause of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
123 'template' => 'Using $template of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
124 'content' => 'Using $content of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
125 'R_URI' => 'Using $R_URI of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
126 'pageinfo' => 'Using $pageinfo of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
127 'storeTitle' => 'Using $storeTitle of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
128 'firstEl' => 'Using $firstEl of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
129 'errorC' => 'Using $errorC of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
130 'newC' => 'Using $newC of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
131 'viewId' => 'Using $viewId of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
132 'viewId_addParams' => 'Using $viewId_addParams of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
133 'modTSconfig' => 'Using $modTSconfig of class EditDocumentTemplate from the outside is discouraged, the variable will be removed.',
134 'dontStoreDocumentRef' => 'Using $dontStoreDocumentRef of class EditDocumentTemplate from the outside is discouraged, as this variable is only used for internal storage.',
135 ];
136
137 /**
138 * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
139 *
140 * @see \TYPO3\CMS\Backend\Utility\BackendUtility::editOnClick()
141 * @var array
142 */
143 protected $editconf = [];
144
145 /**
146 * Comma list of field names to edit. If specified, only those fields will be rendered.
147 * Otherwise all (available) fields in the record are shown according to the TCA type.
148 *
149 * @var string|null
150 */
151 protected $columnsOnly;
152
153 /**
154 * Default values for fields
155 *
156 * @var array|null [table][field]
157 */
158 protected $defVals;
159
160 /**
161 * Array of values to force being set as hidden fields in FormEngine
162 *
163 * @var array|null [table][field]
164 */
165 protected $overrideVals;
166
167 /**
168 * If set, this value will be set in $this->retUrl as "returnUrl", if not,
169 * $this->retUrl will link to dummy controller
170 *
171 * @var string|null
172 */
173 protected $returnUrl;
174
175 /**
176 * Prepared return URL. Contains the URL that we should return to from FormEngine if
177 * close button is clicked. Usually passed along as 'returnUrl', but falls back to
178 * "dummy" controller.
179 *
180 * @var string
181 */
182 protected $retUrl;
183
184 /**
185 * Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
186 *
187 * @var int
188 */
189 protected $closeDoc;
190
191 /**
192 * If true, the processing of incoming data will be performed as if a save-button is pressed.
193 * Used in the forms as a hidden field which can be set through
194 * JavaScript if the form is somehow submitted by JavaScript.
195 *
196 * @var bool
197 */
198 protected $doSave;
199
200 /**
201 * Main DataHandler datamap array
202 *
203 * @var array
204 * @todo: Will be set protected later, still used by ConditionMatcher
205 * @internal Will be removed / protected in v10 without further notice
206 */
207 public $data;
208
209 /**
210 * Main DataHandler cmdmap array
211 *
212 * @var array
213 */
214 protected $cmd;
215
216 /**
217 * DataHandler 'mirror' input
218 *
219 * @var array
220 */
221 protected $mirror;
222
223 /**
224 * @var string
225 * @deprecated since v9, will be removed in v10, unused
226 */
227 protected $cacheCmd;
228
229 /**
230 * @var string
231 * @deprecated since v9, will be removed in v10, unused
232 */
233 protected $redirect;
234
235 /**
236 * Boolean: If set, then the GET var "&id=" will be added to the
237 * retUrl string so that the NEW id of something is returned to the script calling the form.
238 *
239 * @var bool
240 */
241 protected $returnNewPageId = false;
242
243 /**
244 * Updated values for backendUser->uc. Used for new inline records to mark them
245 * as expanded: uc[inlineView][...]
246 *
247 * @var array|null
248 */
249 protected $uc;
250
251 /**
252 * ID for displaying the page in the frontend, "save and view"
253 *
254 * @var int
255 */
256 protected $popViewId;
257
258 /**
259 * @var string
260 * @deprecated since v9, will be removed in v10, unused
261 */
262 protected $popViewId_addParams;
263
264 /**
265 * Alternative URL for viewing the frontend pages.
266 *
267 * @var string
268 */
269 protected $viewUrl;
270
271 /**
272 * Alternative title for the document handler.
273 *
274 * @var string
275 */
276 protected $recTitle;
277
278 /**
279 * If set, then no save & view button is printed
280 *
281 * @var bool
282 */
283 protected $noView;
284
285 /**
286 * @var string
287 */
288 protected $perms_clause;
289
290 /**
291 * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
292 *
293 * @var bool
294 */
295 protected $returnEditConf;
296
297 /**
298 * Workspace used for the editing action.
299 *
300 * @var string|null
301 */
302 protected $workspace;
303
304 /**
305 * @var \TYPO3\CMS\Backend\Template\DocumentTemplate
306 * @deprecated since v9, will be removed in v10, unused
307 */
308 protected $doc;
309
310 /**
311 * @deprecated since v9, will be removed in v10, unused
312 */
313 protected $template;
314
315 /**
316 * @deprecated since v9, will be removed in v10, unused
317 */
318 protected $content;
319
320 /**
321 * parse_url() of current requested URI, contains ['path'] and ['query'] parts.
322 *
323 * @var array
324 */
325 protected $R_URL_parts;
326
327 /**
328 * Contains $request query parameters. This array is the foundation for creating
329 * the R_URI internal var which becomes the url to which forms are submitted
330 *
331 * @var array
332 */
333 protected $R_URL_getvars;
334
335 /**
336 * Set to the URL of this script including variables which is needed to re-display the form.
337 *
338 * @var string
339 */
340 protected $R_URI;
341
342 /**
343 * @var array
344 * @deprecated since v9, will be removed in v10, unused
345 */
346 protected $MCONF;
347
348 /**
349 * @var array
350 */
351 protected $pageinfo;
352
353 /**
354 * Is loaded with the "title" of the currently "open document"
355 * used for the open document toolbar
356 *
357 * @var string
358 */
359 protected $storeTitle = '';
360
361 /**
362 * Contains an array with key/value pairs of GET parameters needed to reach the
363 * current document displayed - used in the 'open documents' toolbar.
364 *
365 * @var array
366 */
367 protected $storeArray;
368
369 /**
370 * $this->storeArray imploded to url
371 *
372 * @var string
373 */
374 protected $storeUrl;
375
376 /**
377 * md5 hash of storeURL, used to identify a single open document in backend user uc
378 *
379 * @var string
380 */
381 protected $storeUrlMd5;
382
383 /**
384 * Backend user session data of this module
385 *
386 * @var array
387 */
388 protected $docDat;
389
390 /**
391 * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
392 * the various documents on the GET parameter list needed to open it. The values are
393 * arrays with 0,1,2 keys with information about the document (see compileStoreData()).
394 * The docHandler variable is stored in the $docDat session data, key "0".
395 *
396 * @var array
397 */
398 protected $docHandler;
399
400 /**
401 * Array of the elements to create edit forms for.
402 *
403 * @var array
404 * @todo: Will be set protected later, still used by ConditionMatcher
405 * @internal Will be removed / protected in v10 without further notice
406 */
407 public $elementsData;
408
409 /**
410 * Pointer to the first element in $elementsData
411 *
412 * @var array
413 */
414 protected $firstEl;
415
416 /**
417 * Counter, used to count the number of errors (when users do not have edit permissions)
418 *
419 * @var int
420 */
421 protected $errorC;
422
423 /**
424 * Counter, used to count the number of new record forms displayed
425 *
426 * @var int
427 */
428 protected $newC;
429
430 /**
431 * Is set to the pid value of the last shown record - thus indicating which page to
432 * show when clicking the SAVE/VIEW button
433 *
434 * @var int
435 */
436 protected $viewId;
437
438 /**
439 * Is set to additional parameters (like "&L=xxx") if the record supports it.
440 *
441 * @var string
442 */
443 protected $viewId_addParams;
444
445 /**
446 * @deprecated since v9, will be removed in v10, unused
447 */
448 protected $modTSconfig = [];
449
450 /**
451 * @var FormResultCompiler
452 */
453 protected $formResultCompiler;
454
455 /**
456 * Used internally to disable the storage of the document reference (eg. new records)
457 *
458 * @var bool
459 */
460 protected $dontStoreDocumentRef = 0;
461
462 /**
463 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
464 */
465 protected $signalSlotDispatcher;
466
467 /**
468 * Stores information needed to preview the currently saved record
469 *
470 * @var array
471 */
472 protected $previewData = [];
473
474 /**
475 * ModuleTemplate object
476 *
477 * @var ModuleTemplate
478 */
479 protected $moduleTemplate;
480
481 /**
482 * Check if a record has been saved
483 *
484 * @var bool
485 */
486 protected $isSavedRecord;
487
488 /**
489 * Check if a page in free translation mode
490 *
491 * @var bool
492 */
493 protected $isPageInFreeTranslationMode = false;
494
495 /**
496 * Constructor
497 */
498 public function __construct()
499 {
500 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
501 $this->moduleTemplate->setUiBlock(true);
502 // @todo Used by TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching
503 $GLOBALS['SOBE'] = $this;
504 $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
505 }
506
507 /**
508 * Main dispatcher entry method registered as "record_edit" end point
509 *
510 * @param ServerRequestInterface $request the current request
511 * @return ResponseInterface the response with the content
512 */
513 public function mainAction(ServerRequestInterface $request): ResponseInterface
514 {
515 // Unlock all locked records
516 BackendUtility::lockRecords();
517 if ($response = $this->preInit($request)) {
518 return $response;
519 }
520
521 // Process incoming data via DataHandler?
522 $parsedBody = $request->getParsedBody();
523 if ($this->doSave
524 || isset($parsedBody['_savedok'])
525 || isset($parsedBody['_saveandclosedok'])
526 || isset($parsedBody['_savedokview'])
527 || isset($parsedBody['_savedoknew'])
528 || isset($parsedBody['_duplicatedoc'])
529 ) {
530 if ($response = $this->processData($request)) {
531 return $response;
532 }
533 }
534
535 $this->init($request);
536 $this->main($request);
537
538 return new HtmlResponse($this->moduleTemplate->renderContent());
539 }
540
541 /**
542 * First initialization, always called, even before processData() executes DataHandler processing.
543 *
544 * @param ServerRequestInterface $request
545 * @return ResponseInterface|null Possible redirect response
546 */
547 public function preInit(ServerRequestInterface $request = null): ?ResponseInterface
548 {
549 if ($request === null) {
550 // Missing argument? This method must have been called from outside.
551 // Method will be protected and $request mandatory in v10, giving core freedom to move stuff around
552 // New v10 signature: "protected function preInit(ServerRequestInterface $request): ?ResponseInterface"
553 // @deprecated since TYPO3 v9, method argument $request will be set to mandatory
554 trigger_error('Method preInit() will be set to protected in v10. Do not call from other extension', E_USER_DEPRECATED);
555 $request = $GLOBALS['TYPO3_REQUEST'];
556 }
557
558 if ($response = $this->localizationRedirect($request)) {
559 return $response;
560 }
561
562 $parsedBody = $request->getParsedBody();
563 $queryParams = $request->getQueryParams();
564
565 $this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
566 $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
567 $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
568 $this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
569 $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null);
570 $this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
571 $this->doSave = (bool)($parsedBody['doSave'] ?? $queryParams['doSave'] ?? false);
572 $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
573 $this->workspace = $parsedBody['workspace'] ?? $queryParams['workspace'] ?? null;
574 $this->uc = $parsedBody['uc'] ?? $queryParams['uc'] ?? null;
575
576 // Set overrideVals as default values if defVals does not exist.
577 // @todo: Why?
578 if (!is_array($this->defVals) && is_array($this->overrideVals)) {
579 $this->defVals = $this->overrideVals;
580 }
581
582 // Set final return URL
583 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
584 $this->retUrl = $this->returnUrl ?: (string)$uriBuilder->buildUriFromRoute('dummy');
585
586 // Change $this->editconf if versioning applies to any of the records
587 $this->fixWSversioningInEditConf();
588
589 // Prepare R_URL (request url)
590 $this->R_URL_parts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
591 $this->R_URL_getvars = $queryParams;
592 $this->R_URL_getvars['edit'] = $this->editconf;
593
594 // Prepare 'open documents' url, this is later modified again various times
595 $this->compileStoreData();
596 // Backend user session data of this module
597 $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
598 $this->docHandler = $this->docDat[0];
599
600 // Close document if a request for closing the document has been sent
601 if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
602 if ($response = $this->closeDocument($this->closeDoc, $request)) {
603 return $response;
604 }
605 }
606
607 // Sets a temporary workspace, this request is based on
608 if ($this->workspace !== null) {
609 $this->getBackendUser()->setTemporaryWorkspace($this->workspace);
610 }
611
612 $this->emitFunctionAfterSignal('preInit', $request);
613
614 return null;
615 }
616
617 /**
618 * Detects, if a save command has been triggered.
619 *
620 * @return bool TRUE, then save the document (data submitted)
621 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
622 */
623 public function doProcessData()
624 {
625 trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
626
627 $out = $this->doSave
628 || isset($_POST['_savedok'])
629 || isset($_POST['_saveandclosedok'])
630 || isset($_POST['_savedokview'])
631 || isset($_POST['_savedoknew'])
632 || isset($_POST['_duplicatedoc']);
633 return $out;
634 }
635
636 /**
637 * Do processing of data, submitting it to DataHandler. May return a RedirectResponse
638 *
639 * @param $request ServerRequestInterface
640 * @return ResponseInterface|null
641 */
642 public function processData(ServerRequestInterface $request = null): ?ResponseInterface
643 {
644 // @deprecated Variable can be removed in v10
645 $deprecatedCaller = false;
646 if ($request === null) {
647 // Missing argument? This method must have been called from outside.
648 // Method will be protected and $request mandatory in v10, giving core freedom to move stuff around
649 // New v10 signature: "protected function processData(ServerRequestInterface $request): ?ResponseInterface"
650 // @deprecated since TYPO3 v9, method argument $request will be set to mandatory
651 trigger_error('Method processData() will be set to protected in v10. Do not call from other extension', E_USER_DEPRECATED);
652 $request = $GLOBALS['TYPO3_REQUEST'];
653 $deprecatedCaller = true;
654 }
655
656 $parsedBody = $request->getParsedBody();
657 $queryParams = $request->getQueryParams();
658
659 $beUser = $this->getBackendUser();
660
661 // Processing related GET / POST vars
662 $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];
663 $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];
664 $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];
665 // @deprecated property cacheCmd is unused and can be removed in v10
666 $this->cacheCmd = $parsedBody['cacheCmd'] ?? $queryParams['cacheCmd'] ?? null;
667 // @deprecated property redirect is unused and can be removed in v10
668 $this->redirect = $parsedBody['redirect'] ?? $queryParams['redirect'] ?? null;
669 $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);
670
671 // Only options related to $this->data submission are included here
672 $tce = GeneralUtility::makeInstance(DataHandler::class);
673
674 $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);
675
676 // Set default values specific for the user
677 $TCAdefaultOverride = $beUser->getTSConfig()['TCAdefaults.'] ?? null;
678 if (is_array($TCAdefaultOverride)) {
679 $tce->setDefaultsFromUserTS($TCAdefaultOverride);
680 }
681 // Set internal vars
682 if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {
683 $tce->neverHideAtCopy = 1;
684 }
685 // Load DataHandler with data
686 $tce->start($this->data, $this->cmd);
687 if (is_array($this->mirror)) {
688 $tce->setMirror($this->mirror);
689 }
690
691 // Perform the saving operation with DataHandler:
692 if ($this->doSave === true) {
693 // @todo: Make DataHandler understand UploadedFileInterface and submit $request->getUploadedFiles() instead of $_FILES here
694 $tce->process_uploads($_FILES);
695 $tce->process_datamap();
696 $tce->process_cmdmap();
697 }
698 // If pages are being edited, we set an instruction about updating the page tree after this operation.
699 if ($tce->pagetreeNeedsRefresh
700 && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
701 ) {
702 BackendUtility::setUpdateSignal('updatePageTree');
703 }
704 // If there was saved any new items, load them:
705 if (!empty($tce->substNEWwithIDs_table)) {
706 // Save the expanded/collapsed states for new inline records, if any
707 FormEngineUtility::updateInlineView($this->uc, $tce);
708 $newEditConf = [];
709 foreach ($this->editconf as $tableName => $tableCmds) {
710 $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
711 if (!empty($keys)) {
712 foreach ($keys as $key) {
713 $editId = $tce->substNEWwithIDs[$key];
714 // Check if the $editId isn't a child record of an IRRE action
715 if (!(is_array($tce->newRelatedIDs[$tableName])
716 && in_array($editId, $tce->newRelatedIDs[$tableName]))
717 ) {
718 // Translate new id to the workspace version
719 if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
720 $beUser->workspace,
721 $tableName,
722 $editId,
723 'uid'
724 )) {
725 $editId = $versionRec['uid'];
726 }
727 $newEditConf[$tableName][$editId] = 'edit';
728 }
729 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
730 // Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
731 if ($tableName === 'pages'
732 && $this->retUrl != (string)$uriBuilder->buildUriFromRoute('dummy')
733 && $this->returnNewPageId
734 ) {
735 $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
736 }
737 }
738 } else {
739 $newEditConf[$tableName] = $tableCmds;
740 }
741 }
742 // Reset editconf if newEditConf has values
743 if (!empty($newEditConf)) {
744 $this->editconf = $newEditConf;
745 }
746 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
747 $this->R_URL_getvars['edit'] = $this->editconf;
748 // Unset default values since we don't need them anymore.
749 unset($this->R_URL_getvars['defVals']);
750 // Recompile the store* values since editconf changed
751 $this->compileStoreData();
752 }
753 // See if any records was auto-created as new versions?
754 if (!empty($tce->autoVersionIdMap)) {
755 $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
756 }
757 // If a document is saved and a new one is created right after.
758 if (isset($parsedBody['_savedoknew']) && is_array($this->editconf)) {
759 if ($redirect = $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request)) {
760 return $redirect;
761 }
762 // Find the current table
763 reset($this->editconf);
764 $nTable = key($this->editconf);
765 // Finding the first id, getting the records pid+uid
766 reset($this->editconf[$nTable]);
767 $nUid = key($this->editconf[$nTable]);
768 $recordFields = 'pid,uid';
769 if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
770 $recordFields .= ',t3ver_oid';
771 }
772 $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
773 // Determine insertion mode: 'top' is self-explaining,
774 // otherwise new elements are inserted after one using a negative uid
775 $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
776 // Setting a blank editconf array for a new record:
777 $this->editconf = [];
778 // Determine related page ID for regular live context
779 if ($nRec['pid'] != -1) {
780 if ($insertRecordOnTop) {
781 $relatedPageId = $nRec['pid'];
782 } else {
783 $relatedPageId = -$nRec['uid'];
784 }
785 } else {
786 // Determine related page ID for workspace context
787 if ($insertRecordOnTop) {
788 // Fetch live version of workspace version since the pid value is always -1 in workspaces
789 $liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
790 $relatedPageId = $liveRecord['pid'];
791 } else {
792 // Use uid of live version of workspace version
793 $relatedPageId = -$nRec['t3ver_oid'];
794 }
795 }
796 $this->editconf[$nTable][$relatedPageId] = 'new';
797 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
798 $this->R_URL_getvars['edit'] = $this->editconf;
799 // Recompile the store* values since editconf changed...
800 $this->compileStoreData();
801 }
802 // If a document should be duplicated.
803 if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf)) {
804 $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request);
805 // Find current table
806 reset($this->editconf);
807 $nTable = key($this->editconf);
808 // Find the first id, getting the records pid+uid
809 reset($this->editconf[$nTable]);
810 $nUid = key($this->editconf[$nTable]);
811 if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
812 $nUid = $tce->substNEWwithIDs[$nUid];
813 }
814
815 $recordFields = 'pid,uid';
816 if (!empty($GLOBALS['TCA'][$nTable]['ctrl']['versioningWS'])) {
817 $recordFields .= ',t3ver_oid';
818 }
819 $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
820
821 // Setting a blank editconf array for a new record:
822 $this->editconf = [];
823
824 if ($nRec['pid'] != -1) {
825 $relatedPageId = -$nRec['uid'];
826 } else {
827 $relatedPageId = -$nRec['t3ver_oid'];
828 }
829
830 /** @var $duplicateTce \TYPO3\CMS\Core\DataHandling\DataHandler */
831 $duplicateTce = GeneralUtility::makeInstance(DataHandler::class);
832
833 $duplicateCmd = [
834 $nTable => [
835 $nUid => [
836 'copy' => $relatedPageId
837 ]
838 ]
839 ];
840
841 $duplicateTce->start([], $duplicateCmd);
842 $duplicateTce->process_cmdmap();
843
844 $duplicateMappingArray = $duplicateTce->copyMappingArray;
845 $duplicateUid = $duplicateMappingArray[$nTable][$nUid];
846
847 if ($nTable === 'pages') {
848 BackendUtility::setUpdateSignal('updatePageTree');
849 }
850
851 $this->editconf[$nTable][$duplicateUid] = 'edit';
852 // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
853 $this->R_URL_getvars['edit'] = $this->editconf;
854 // Recompile the store* values since editconf changed...
855 $this->compileStoreData();
856
857 // Inform the user of the duplication
858 $flashMessage = GeneralUtility::makeInstance(
859 FlashMessage::class,
860 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
861 '',
862 FlashMessage::OK
863 );
864 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
865 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
866 $defaultFlashMessageQueue->enqueue($flashMessage);
867 }
868 // If a preview is requested
869 if (isset($parsedBody['_savedokview'])) {
870 // Get the first table and id of the data array from DataHandler
871 $table = reset(array_keys($this->data));
872 $id = reset(array_keys($this->data[$table]));
873 if (!MathUtility::canBeInterpretedAsInteger($id)) {
874 $id = $tce->substNEWwithIDs[$id];
875 }
876 // Store this information for later use
877 $this->previewData['table'] = $table;
878 $this->previewData['id'] = $id;
879 }
880 $tce->printLogErrorMessages();
881
882 if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
883 || isset($parsedBody['_saveandclosedok'])
884 ) {
885 // Redirect if element should be closed after save
886 $possibleRedirect = $this->closeDocument(abs($this->closeDoc), $request);
887 if ($deprecatedCaller && $possibleRedirect) {
888 // @deprecated fall back if method has been called from outside. This if can be removed in v10
889 HttpUtility::redirect($possibleRedirect->getHeaders()['location'][0]);
890 }
891 return $possibleRedirect;
892 }
893 return null;
894 }
895
896 /**
897 * Initialize the view part of the controller logic.
898 *
899 * @param $request ServerRequestInterface
900 */
901 public function init(ServerRequestInterface $request = null): void
902 {
903 if ($request === null) {
904 // Missing argument? This method must have been called from outside.
905 // Method will be protected and $request mandatory in v10, giving core freedom to move stuff around
906 // New v10 signature: "protected function init(ServerRequestInterface $request): void
907 // @deprecated since TYPO3 v9, method argument $request will be set to mandatory
908 trigger_error('Method init() will be set to protected in v10. Do not call from other extension', E_USER_DEPRECATED);
909 $request = $GLOBALS['TYPO3_REQUEST'];
910 }
911
912 $parsedBody = $request->getParsedBody();
913 $queryParams = $request->getQueryParams();
914
915 $beUser = $this->getBackendUser();
916
917 // @deprecated since v9, will be removed in v10, unused, remove call in v10
918 $this->popViewId_addParams = $parsedBody['popViewId_addParams'] ?? $queryParams['popViewId_addParams'] ?? '';
919
920 $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);
921 $this->viewUrl = (string)($parsedBody['viewUrl'] ?? $queryParams['viewUrl'] ?? '');
922 $this->recTitle = (string)($parsedBody['recTitle'] ?? $queryParams['recTitle'] ?? '');
923 $this->noView = (bool)($parsedBody['noView'] ?? $queryParams['noView'] ?? false);
924 $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
925 // Set other internal variables:
926 $this->R_URL_getvars['returnUrl'] = $this->retUrl;
927 $this->R_URI = $this->R_URL_parts['path'] . '?' . ltrim(GeneralUtility::implodeArrayForUrl(
928 '',
929 $this->R_URL_getvars
930 ), '&');
931
932 // @deprecated since v9, will be removed in v10, unused
933 $this->MCONF['name'] = 'xMOD_alt_doc.php';
934 // @deprecated since v9, will be removed in v10, unused
935 $this->doc = $GLOBALS['TBE_TEMPLATE'];
936
937 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
938 $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
939
940 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
941 // override the default jumpToUrl
942 $this->moduleTemplate->addJavaScriptCode(
943 'jumpToUrl',
944 '
945 // Info view:
946 function launchView(table,uid) {
947 console.warn(\'Calling launchView() has been deprecated in v9 and will be removed in v10.0\');
948 var thePreviewWindow = window.open(
949 ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('show_item') . '&table=') . ' + encodeURIComponent(table) + "&uid=" + encodeURIComponent(uid),
950 "ShowItem" + Math.random().toString(16).slice(2),
951 "height=300,width=410,status=0,menubar=0,resizable=0,location=0,directories=0,scrollbars=1,toolbar=0"
952 );
953 if (thePreviewWindow && thePreviewWindow.focus) {
954 thePreviewWindow.focus();
955 }
956 }
957 function deleteRecord(table,id,url) {
958 window.location.href = ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('tce_db') . '&cmd[') . '+table+"]["+id+"][delete]=1&redirect="+escape(url);
959 }
960 ' . (isset($parsedBody['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '')
961 );
962 // Set context sensitive menu
963 $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
964
965 $this->emitFunctionAfterSignal('init', $request);
966 }
967
968 /**
969 * Generate the Javascript for opening the preview window
970 *
971 * @return string
972 */
973 protected function generatePreviewCode(): string
974 {
975 $previewPageId = $this->getPreviewPageId();
976 $previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
977 $anchorSection = $this->getPreviewUrlAnchorSection();
978 $previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
979
980 return '
981 if (window.opener) {
982 '
983 . BackendUtility::viewOnClick(
984 $previewPageId,
985 '',
986 $previewPageRootLine,
987 $anchorSection,
988 $this->viewUrl,
989 $previewUrlParameters,
990 false
991 )
992 . '
993 } else {
994 '
995 . BackendUtility::viewOnClick(
996 $previewPageId,
997 '',
998 $previewPageRootLine,
999 $anchorSection,
1000 $this->viewUrl,
1001 $previewUrlParameters
1002 )
1003 . '
1004 }';
1005 }
1006
1007 /**
1008 * Returns the parameters for the preview URL
1009 *
1010 * @param int $previewPageId
1011 * @return string
1012 */
1013 protected function getPreviewUrlParameters(int $previewPageId): string
1014 {
1015 $linkParameters = [];
1016 $table = $this->previewData['table'] ?: $this->firstEl['table'];
1017 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
1018 $previewConfiguration = BackendUtility::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
1019 $recordArray = BackendUtility::getRecord($table, $recordId);
1020
1021 // language handling
1022 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
1023 if ($languageField && !empty($recordArray[$languageField])) {
1024 $l18nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
1025 if ($l18nPointer && !empty($recordArray[$l18nPointer])
1026 && isset($previewConfiguration['useDefaultLanguageRecord'])
1027 && !$previewConfiguration['useDefaultLanguageRecord']
1028 ) {
1029 // use parent record
1030 $recordId = $recordArray[$l18nPointer];
1031 }
1032 $linkParameters['L'] = $recordArray[$languageField];
1033 }
1034
1035 // map record data to GET parameters
1036 if (isset($previewConfiguration['fieldToParameterMap.'])) {
1037 foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
1038 $value = $recordArray[$field];
1039 if ($field === 'uid') {
1040 $value = $recordId;
1041 }
1042 $linkParameters[$parameterName] = $value;
1043 }
1044 }
1045
1046 // add/override parameters by configuration
1047 if (isset($previewConfiguration['additionalGetParameters.'])) {
1048 $additionalGetParameters = [];
1049 $this->parseAdditionalGetParameters(
1050 $additionalGetParameters,
1051 $previewConfiguration['additionalGetParameters.']
1052 );
1053 $linkParameters = array_replace($linkParameters, $additionalGetParameters);
1054 }
1055
1056 if (!empty($previewConfiguration['useCacheHash'])) {
1057 $cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
1058 $fullLinkParameters = GeneralUtility::implodeArrayForUrl('', array_merge($linkParameters, ['id' => $previewPageId]));
1059 $cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters);
1060 $linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters);
1061 } else {
1062 $linkParameters['no_cache'] = 1;
1063 }
1064
1065 return GeneralUtility::implodeArrayForUrl('', $linkParameters, '', false, true);
1066 }
1067
1068 /**
1069 * Returns the anchor section for the preview url
1070 *
1071 * @return string
1072 */
1073 protected function getPreviewUrlAnchorSection(): string
1074 {
1075 $table = $this->previewData['table'] ?: $this->firstEl['table'];
1076 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
1077
1078 return $table === 'tt_content' ? '#c' . (int)$recordId : '';
1079 }
1080
1081 /**
1082 * Returns the preview page id
1083 *
1084 * @return int
1085 */
1086 protected function getPreviewPageId(): int
1087 {
1088 $previewPageId = 0;
1089 $table = $this->previewData['table'] ?: $this->firstEl['table'];
1090 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
1091 $pageId = $this->popViewId ?: $this->viewId;
1092
1093 if ($table === 'pages') {
1094 $currentPageId = (int)$recordId;
1095 } else {
1096 $currentPageId = MathUtility::convertToPositiveInteger($pageId);
1097 }
1098
1099 $previewConfiguration = BackendUtility::getPagesTSconfig($currentPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
1100
1101 if (isset($previewConfiguration['previewPageId'])) {
1102 $previewPageId = (int)$previewConfiguration['previewPageId'];
1103 }
1104 // if no preview page was configured
1105 if (!$previewPageId) {
1106 $rootPageData = null;
1107 $rootLine = BackendUtility::BEgetRootLine($currentPageId);
1108 $currentPage = reset($rootLine);
1109 // Allow all doktypes below 200
1110 // This makes custom doktype work as well with opening a frontend page.
1111 if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) {
1112 // try the current page
1113 $previewPageId = $currentPageId;
1114 } else {
1115 // or search for the root page
1116 foreach ($rootLine as $page) {
1117 if ($page['is_siteroot']) {
1118 $rootPageData = $page;
1119 break;
1120 }
1121 }
1122 $previewPageId = isset($rootPageData)
1123 ? (int)$rootPageData['uid']
1124 : $currentPageId;
1125 }
1126 }
1127
1128 $this->popViewId = $previewPageId;
1129
1130 return $previewPageId;
1131 }
1132
1133 /**
1134 * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
1135 *
1136 * This basically removes the trailing dots of sub-array keys in TypoScript.
1137 * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
1138 *
1139 * @param array $parameters Should be an empty array by default
1140 * @param array $typoScript The TypoScript configuration
1141 */
1142 protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
1143 {
1144 foreach ($typoScript as $key => $value) {
1145 if (is_array($value)) {
1146 $key = rtrim($key, '.');
1147 $parameters[$key] = [];
1148 $this->parseAdditionalGetParameters($parameters[$key], $value);
1149 } else {
1150 $parameters[$key] = $value;
1151 }
1152 }
1153 }
1154
1155 /**
1156 * Main module operation
1157 *
1158 * @param $request ServerRequestInterface
1159 */
1160 public function main(ServerRequestInterface $request = null): void
1161 {
1162 if ($request === null) {
1163 // Set method signature in v10 to: "protected function main(ServerRequestInterface $request): void"
1164 trigger_error('@deprecated since v9, this method will be set to protected in v10', E_USER_DEPRECATED);
1165 $request = $GLOBALS['TYPO3_REQUEST'];
1166 }
1167
1168 $body = '';
1169 // Begin edit
1170 if (is_array($this->editconf)) {
1171 $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
1172
1173 // Creating the editing form, wrap it with buttons, document selector etc.
1174 $editForm = $this->makeEditForm();
1175 if ($editForm) {
1176 $this->firstEl = reset($this->elementsData);
1177 // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
1178 if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5]))
1179 && !$this->dontStoreDocumentRef
1180 ) {
1181 $this->docHandler[$this->storeUrlMd5] = [
1182 $this->storeTitle,
1183 $this->storeArray,
1184 $this->storeUrl,
1185 $this->firstEl
1186 ];
1187 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
1188 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
1189 }
1190 $body = $this->formResultCompiler->addCssFiles();
1191 $body .= $this->compileForm($editForm);
1192 $body .= $this->formResultCompiler->printNeededJSFunctions();
1193 $body .= '</form>';
1194 }
1195 }
1196 // Access check...
1197 // The page will show only if there is a valid page and if this page may be viewed by the user
1198 $this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
1199 if ($this->pageinfo) {
1200 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
1201 }
1202 // Setting up the buttons and markers for doc header
1203 $this->getButtons($request);
1204 $this->languageSwitch(
1205 (string)($this->firstEl['table'] ?? ''),
1206 (int)($this->firstEl['uid'] ?? 0),
1207 isset($this->firstEl['pid']) ? (int)$this->firstEl['pid'] : null
1208 );
1209 $this->moduleTemplate->setContent($body);
1210 }
1211
1212 /**
1213 * Creates the editing form with FormEngine, based on the input from GPvars.
1214 *
1215 * @return string HTML form elements wrapped in tables
1216 */
1217 protected function makeEditForm(): string
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 protected function compileForm(string $editForm): string
2073 {
2074 $formContent = '
2075 <form
2076 action="' . htmlspecialchars($this->R_URI) . '"
2077 method="post"
2078 enctype="multipart/form-data"
2079 name="editform"
2080 id="EditDocumentController"
2081 onsubmit="TBE_EDITOR.checkAndDoSubmit(1); return false;"
2082 >
2083 ' . $editForm . '
2084 <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
2085 <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />
2086 <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$this->viewId) . '" />
2087 <input type="hidden" name="closeDoc" value="0" />
2088 <input type="hidden" name="doSave" value="0" />
2089 <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
2090 <input type="hidden" name="_scrollPosition" value="" />';
2091 if ($this->returnNewPageId) {
2092 $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
2093 }
2094 if ($this->viewId_addParams) {
2095 $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams) . '" />';
2096 }
2097 return $formContent;
2098 }
2099
2100 /**
2101 * Create shortcut icon
2102 *
2103 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2104 */
2105 public function shortCutLink()
2106 {
2107 trigger_error('Method shortCutLink() will be removed in v10', E_USER_DEPRECATED);
2108
2109 if ($this->returnUrl !== $this->getCloseUrl()) {
2110 $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
2111 $shortCutButton->setModuleName('xMOD_alt_doc.php')
2112 ->setGetVariables([
2113 'returnUrl',
2114 'edit',
2115 'defVals',
2116 'overrideVals',
2117 'columnsOnly',
2118 'returnNewPageId',
2119 'noView']);
2120 $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->addButton($shortCutButton);
2121 }
2122 }
2123
2124 /**
2125 * Creates open-in-window link
2126 *
2127 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2128 */
2129 public function openInNewWindowLink()
2130 {
2131 trigger_error('Method openInNewWindowLink() will be removed in v10', E_USER_DEPRECATED);
2132
2133 $closeUrl = $this->getCloseUrl();
2134 if ($this->returnUrl !== $closeUrl) {
2135 $aOnClick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue(GeneralUtility::linkThisScript(
2136 ['returnUrl' => $closeUrl]
2137 ))
2138 . ','
2139 . GeneralUtility::quoteJSvalue(md5($this->R_URI))
2140 . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
2141 $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
2142 ->makeLinkButton()
2143 ->setHref('#')
2144 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
2145 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL))
2146 ->setOnClick($aOnClick);
2147 $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->addButton(
2148 $openInNewWindowButton,
2149 ButtonBar::BUTTON_POSITION_RIGHT
2150 );
2151 }
2152 }
2153
2154 /**
2155 * Returns if delete for the current table is disabled by configuration.
2156 * For sys_file_metadata in default language delete is always disabled.
2157 *
2158 * @return bool
2159 */
2160 protected function getDisableDelete(): bool
2161 {
2162 $disableDelete = false;
2163 if ($this->firstEl['table'] === 'sys_file_metadata') {
2164 $row = BackendUtility::getRecord('sys_file_metadata', $this->firstEl['uid'], 'sys_language_uid');
2165 $languageUid = $row['sys_language_uid'];
2166 if ($languageUid === 0) {
2167 $disableDelete = true;
2168 }
2169 } else {
2170 $disableDelete = (bool)$this->getTsConfigOption($this->firstEl['table'] ?? '', 'disableDelete');
2171 }
2172 return $disableDelete;
2173 }
2174
2175 /**
2176 * Returns the URL (usually for the "returnUrl") which closes the current window.
2177 * Used when editing a record in a popup.
2178 *
2179 * @return string
2180 */
2181 protected function getCloseUrl(): string
2182 {
2183 $closeUrl = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
2184 return PathUtility::getAbsoluteWebPath($closeUrl);
2185 }
2186
2187 /***************************
2188 *
2189 * Localization stuff
2190 *
2191 ***************************/
2192 /**
2193 * Make selector box for creating new translation for a record or switching to edit the record in an existing
2194 * language.
2195 * Displays only languages which are available for the current page.
2196 *
2197 * @param string $table Table name
2198 * @param int $uid Uid for which to create a new language
2199 * @param int $pid|null Pid of the record
2200 */
2201 protected function languageSwitch(string $table, int $uid, $pid = null)
2202 {
2203 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
2204 $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2205 /** @var UriBuilder $uriBuilder */
2206 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2207
2208 // Table editable and activated for languages?
2209 if ($this->getBackendUser()->check('tables_modify', $table)
2210 && $languageField
2211 && $transOrigPointerField
2212 ) {
2213 if ($pid === null) {
2214 $row = BackendUtility::getRecord($table, $uid, 'pid');
2215 $pid = $row['pid'];
2216 }
2217 // Get all available languages for the page
2218 // If editing a page, the translations of the current UID need to be fetched
2219 if ($table === 'pages') {
2220 $row = BackendUtility::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
2221 // Ensure the check is always done against the default language page
2222 $langRows = $this->getLanguages(
2223 (int)$row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $uid,
2224 $table
2225 );
2226 } else {
2227 $langRows = $this->getLanguages((int)$pid, $table);
2228 }
2229 // Page available in other languages than default language?
2230 if (is_array($langRows) && count($langRows) > 1) {
2231 $rowsByLang = [];
2232 $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
2233 // Get record in current language
2234 $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
2235 if (!is_array($rowCurrent)) {
2236 $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
2237 }
2238 $currentLanguage = (int)$rowCurrent[$languageField];
2239 // Disabled for records with [all] language!
2240 if ($currentLanguage > -1) {
2241 // Get record in default language if needed
2242 if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
2243 $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
2244 $table,
2245 $rowCurrent[$transOrigPointerField],
2246 $fetchFields
2247 );
2248 if (!is_array($rowsByLang[0])) {
2249 $rowsByLang[0] = BackendUtility::getRecord(
2250 $table,
2251 $rowCurrent[$transOrigPointerField],
2252 $fetchFields
2253 );
2254 }
2255 } else {
2256 $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
2257 }
2258 if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
2259 // Get record in other languages to see what's already available
2260
2261 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2262 ->getQueryBuilderForTable($table);
2263
2264 $queryBuilder->getRestrictions()
2265 ->removeAll()
2266 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2267 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2268
2269 $result = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fetchFields, true))
2270 ->from($table)
2271 ->where(
2272 $queryBuilder->expr()->eq(
2273 'pid',
2274 $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
2275 ),
2276 $queryBuilder->expr()->gt(
2277 $languageField,
2278 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2279 ),
2280 $queryBuilder->expr()->eq(
2281 $transOrigPointerField,
2282 $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO::PARAM_INT)
2283 )
2284 )
2285 ->execute();
2286
2287 while ($row = $result->fetch()) {
2288 $rowsByLang[$row[$languageField]] = $row;
2289 }
2290 }
2291 $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
2292 $languageMenu->setIdentifier('_langSelector');
2293 foreach ($langRows as $lang) {
2294 if ($this->getBackendUser()->checkLanguageAccess($lang['uid'])) {
2295 $newTranslation = isset($rowsByLang[$lang['uid']]) ? '' : ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
2296 // Create url for creating a localized record
2297 $addOption = true;
2298 $href = '';
2299 if ($newTranslation) {
2300 $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2301 'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $lang['uid'],
2302 'returnUrl' => $this->retUrl
2303 ]);
2304
2305 if (array_key_exists(0, $rowsByLang)) {
2306 $href = BackendUtility::getLinkToDataHandlerAction(
2307 '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $lang['uid'],
2308 $redirectUrl
2309 );
2310 } else {
2311 $addOption = false;
2312 }
2313 } else {
2314 $href = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2315 'edit[' . $table . '][' . $rowsByLang[$lang['uid']]['uid'] . ']' => 'edit',
2316 'returnUrl' => $this->retUrl
2317 ]);
2318 }
2319 if ($addOption) {
2320 $menuItem = $languageMenu->makeMenuItem()
2321 ->setTitle($lang['title'] . $newTranslation)
2322 ->setHref($href);
2323 if ((int)$lang['uid'] === $currentLanguage) {
2324 $menuItem->setActive(true);
2325 }
2326 $languageMenu->addMenuItem($menuItem);
2327 }
2328 }
2329 }
2330 $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
2331 }
2332 }
2333 }
2334 }
2335
2336 /**
2337 * Redirects to FormEngine with new parameters to edit a just created localized record
2338 *
2339 * @param ServerRequestInterface $request Incoming request object
2340 * @return ResponseInterface|null Possible redirect response
2341 */
2342 public function localizationRedirect(ServerRequestInterface $request = null): ?ResponseInterface
2343 {
2344 $deprecatedCaller = false;
2345 if (!$request instanceof ServerRequestInterface) {
2346 // @deprecated since TYPO3 v9
2347 // Method signature in v10: protected function localizationRedirect(ServerRequestInterface $request): ?ResponseInterface
2348 trigger_error('Method localizationRedirect() will be set to protected in v10. Do not call from other extension', E_USER_DEPRECATED);
2349 $justLocalized = $request;
2350 $request = $GLOBALS['TYPO3_REQUEST'];
2351 $deprecatedCaller = true;
2352 } else {
2353 $justLocalized = $request->getQueryParams()['justLocalized'];
2354 }
2355
2356 if (empty($justLocalized)) {
2357 return null;
2358 }
2359
2360 list($table, $origUid, $language) = explode(':', $justLocalized);
2361
2362 if ($GLOBALS['TCA'][$table]
2363 && $GLOBALS['TCA'][$table]['ctrl']['languageField']
2364 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
2365 ) {
2366 $parsedBody = $request->getParsedBody();
2367 $queryParams = $request->getQueryParams();
2368
2369 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2370 $queryBuilder->getRestrictions()
2371 ->removeAll()
2372 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2373 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2374 $localizedRecord = $queryBuilder->select('uid')
2375 ->from($table)
2376 ->where(
2377 $queryBuilder->expr()->eq(
2378 $GLOBALS['TCA'][$table]['ctrl']['languageField'],
2379 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
2380 ),
2381 $queryBuilder->expr()->eq(
2382 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2383 $queryBuilder->createNamedParameter($origUid, \PDO::PARAM_INT)
2384 )
2385 )
2386 ->execute()
2387 ->fetch();
2388 $returnUrl = $parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '';
2389 if (is_array($localizedRecord)) {
2390 if ($deprecatedCaller) {
2391 // @deprecated fall back if method has been called from outside. This if can be removed in v10
2392 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2393 $location = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2394 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2395 'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2396 ]);
2397 HttpUtility::redirect($location);
2398 }
2399 // Create redirect response to self to edit just created record
2400 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2401 return new RedirectResponse(
2402 (string)$uriBuilder->buildUriFromRoute(
2403 'record_edit',
2404 [
2405 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2406 'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2407 ]
2408 ),
2409 303
2410 );
2411 }
2412 }
2413 return null;
2414 }
2415
2416 /**
2417 * Returns sys_language records available for record translations on given page.
2418 *
2419 * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
2420 * hidden. If set to another value, the query will select all sys_language records that has a
2421 * translation record on that page (and is not hidden, unless you are admin user)
2422 * @param string $table For pages we want all languages, for other records the languages of the page translations
2423 * @return array Language records including faked record for default language
2424 */
2425 protected function getLanguages(int $id, string $table = ''): array
2426 {
2427 $languageService = $this->getLanguageService();
2428 $modPageTsConfig = BackendUtility::getPagesTSconfig($id)['mod.']['SHARED.'] ?? [];
2429 // Fallback non sprite-configuration
2430 if (preg_match('/\\.gif$/', $modPageTsConfig['defaultLanguageFlag'] ?? '')) {
2431 $modPageTsConfig['defaultLanguageFlag'] = str_replace(
2432 '.gif',
2433 '',
2434 $modPageTsConfig['defaultLanguageFlag']
2435 );
2436 }
2437 $languages = [
2438 0 => [
2439 'uid' => 0,
2440 'pid' => 0,
2441 'hidden' => 0,
2442 'title' => $modPageTsConfig['defaultLanguageLabel'] !== ''
2443 ? $modPageTsConfig['defaultLanguageLabel'] . ' (' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage') . ')'
2444 : $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage'),
2445 'flag' => $modPageTsConfig['defaultLanguageFlag']
2446 ]
2447 ];
2448
2449 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2450 ->getQueryBuilderForTable('sys_language');
2451
2452 $queryBuilder->select('s.uid', 's.pid', 's.hidden', 's.title', 's.flag')
2453 ->from('sys_language', 's')
2454 ->groupBy('s.uid', 's.pid', 's.hidden', 's.title', 's.flag', 's.sorting')
2455 ->orderBy('s.sorting');
2456
2457 if ($id) {
2458 $queryBuilder->getRestrictions()
2459 ->removeAll()
2460 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2461 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2462
2463 if (!$this->getBackendUser()->isAdmin()) {
2464 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(HiddenRestriction::class));
2465 }
2466
2467 $this->joinPagesTranslationsForActiveLanguage($queryBuilder, $table, $id);
2468 }
2469
2470 $result = $queryBuilder->execute();
2471 while ($row = $result->fetch()) {
2472 $languages[$row['uid']] = $row;
2473 }
2474
2475 return $languages;
2476 }
2477
2478 /**
2479 * @param QueryBuilder $queryBuilder
2480 * @param string $table
2481 * @param int $id
2482 */
2483 public function joinPagesTranslationsForActiveLanguage(QueryBuilder $queryBuilder, string $table, int $id)
2484 {
2485 // Add join with pages translations to only show active languages
2486 if ($table !== 'pages') {
2487 $queryBuilder->from('pages', 'o')
2488 ->where(
2489 $queryBuilder->expr()->eq(
2490 'o.' . $GLOBALS['TCA']['pages']['ctrl']['languageField'],
2491 $queryBuilder->quoteIdentifier('s.uid')
2492 ),
2493 $queryBuilder->expr()->eq(
2494 'o.' . $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2495 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
2496 )
2497 );
2498 }
2499 }
2500
2501 /**
2502 * Fix $this->editconf if versioning applies to any of the records
2503 *
2504 * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
2505 */
2506 protected function fixWSversioningInEditConf($mapArray = false): void
2507 {
2508 // Traverse the editConf array
2509 if (is_array($this->editconf)) {
2510 // Tables:
2511 foreach ($this->editconf as $table => $conf) {
2512 if (is_array($conf) && $GLOBALS['TCA'][$table]) {
2513 // Traverse the keys/comments of each table (keys can be a comma list of uids)
2514 $newConf = [];
2515 foreach ($conf as $cKey => $cmd) {
2516 if ($cmd === 'edit') {
2517 // Traverse the ids:
2518 $ids = GeneralUtility::trimExplode(',', $cKey, true);
2519 foreach ($ids as $idKey => $theUid) {
2520 if (is_array($mapArray)) {
2521 if ($mapArray[$table][$theUid]) {
2522 $ids[$idKey] = $mapArray[$table][$theUid];
2523 }
2524 } else {
2525 // Default, look for versions in workspace for record:
2526 $calcPRec = $this->getRecordForEdit((string)$table, (int)$theUid);
2527 if (is_array($calcPRec)) {
2528 // Setting UID again if it had changed, eg. due to workspace versioning.
2529 $ids[$idKey] = $calcPRec['uid'];
2530 }
2531 }
2532 }
2533 // Add the possibly manipulated IDs to the new-build newConf array:
2534 $newConf[implode(',', $ids)] = $cmd;
2535 } else {
2536 $newConf[$cKey] = $cmd;
2537 }
2538 }
2539 // Store the new conf array:
2540 $this->editconf[$table] = $newConf;
2541 }
2542 }
2543 }
2544 }
2545
2546 /**
2547 * Get record for editing.
2548 *
2549 * @param string $table Table name
2550 * @param int $theUid Record UID
2551 * @return array|false Returns record to edit, false if none
2552 */
2553 protected function getRecordForEdit(string $table, int $theUid)
2554 {
2555 // Fetch requested record:
2556 $reqRecord = BackendUtility::getRecord($table, $theUid, 'uid,pid');
2557 if (is_array($reqRecord)) {
2558 // If workspace is OFFLINE:
2559 if ($this->getBackendUser()->workspace != 0) {
2560 // Check for versioning support of the table:
2561 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
2562 // If the record is already a version of "something" pass it by.
2563 if ($reqRecord['pid'] == -1) {
2564 // (If it turns out not to be a version of the current workspace there will be trouble, but
2565 // that is handled inside DataHandler then and in the interface it would clearly be an error of
2566 // links if the user accesses such a scenario)
2567 return $reqRecord;
2568 }
2569 // The input record was online and an offline version must be found or made:
2570 // Look for version of this workspace:
2571 $versionRec = BackendUtility::getWorkspaceVersionOfRecord(
2572 $this->getBackendUser()->workspace,
2573 $table,
2574 $reqRecord['uid'],
2575 'uid,pid,t3ver_oid'
2576 );
2577 return is_array($versionRec) ? $versionRec : $reqRecord;
2578 }
2579 // This means that editing cannot occur on this record because it was not supporting versioning
2580 // which is required inside an offline workspace.
2581 return false;
2582 }
2583 // In ONLINE workspace, just return the originally requested record:
2584 return $reqRecord;
2585 }
2586 // Return FALSE because the table/uid was not found anyway.
2587 return false;
2588 }
2589
2590 /**
2591 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2592 *
2593 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2594 */
2595 public function compileStoreDat()
2596 {
2597 trigger_error('Method compileStoreDat() will be removed in TYPO3 v10.', E_USER_DEPRECATED);
2598 $this->compileStoreData();
2599 }
2600
2601 /**
2602 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2603 * to prepare 'open documents' urls
2604 */
2605 protected function compileStoreData(): void
2606 {
2607 // @todo: Refactor in v10: This GeneralUtility method fiddles with _GP()
2608 $this->storeArray = GeneralUtility::compileSelectedGetVarsFromArray(
2609 'edit,defVals,overrideVals,columnsOnly,noView,workspace',
2610 $this->R_URL_getvars
2611 );
2612 $this->storeUrl = GeneralUtility::implodeArrayForUrl('', $this->storeArray);
2613 $this->storeUrlMd5 = md5($this->storeUrl);
2614 }
2615
2616 /**
2617 * Function used to look for configuration of buttons in the form: Fx. disabling buttons or showing them at various
2618 * positions.
2619 *
2620 * @param string $table The table for which the configuration may be specific
2621 * @param string $key The option for look for. Default is checking if the saveDocNew button should be displayed.
2622 * @return string Return value fetched from USER TSconfig
2623 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2624 */
2625 public function getNewIconMode($table, $key = 'saveDocNew')
2626 {
2627 trigger_error('Method getNewIconMode() will be removed in TYPO3 v10.', E_USER_DEPRECATED);
2628 return $this->getTsConfigOption($table, $key);
2629 }
2630
2631 /**
2632 * Get a TSConfig 'option.' array, possibly for a specific table.
2633 *
2634 * @param string $table Table name
2635 * @param string $key Options key
2636 * @return string
2637 */
2638 protected function getTsConfigOption(string $table, string $key): string
2639 {
2640 return \trim((string)(
2641 $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table]
2642 ?? $this->getBackendUser()->getTSConfig()['options.'][$key]
2643 ?? ''
2644 ));
2645 }
2646
2647 /**
2648 * Handling the closing of a document
2649 * The argument $mode can be one of this values:
2650 * - 0/1 will redirect to $this->retUrl [self::DOCUMENT_CLOSE_MODE_DEFAULT || self::DOCUMENT_CLOSE_MODE_REDIRECT]
2651 * - 3 will clear the docHandler (thus closing all documents) [self::DOCUMENT_CLOSE_MODE_CLEAR_ALL]
2652 * - 4 will do no redirect [self::DOCUMENT_CLOSE_MODE_NO_REDIRECT]
2653 * - other values will call setDocument with ->retUrl
2654 *
2655 * @param int $mode the close mode: one of self::DOCUMENT_CLOSE_MODE_*
2656 * @param $request ServerRequestInterface Incoming request
2657 * @return ResponseInterface|null Redirect response if needed
2658 */
2659 public function closeDocument($mode = self::DOCUMENT_CLOSE_MODE_DEFAULT, ServerRequestInterface $request = null): ?ResponseInterface
2660 {
2661 // Foreign class call or missing argument? Method will be protected and $request mandatory in v10, giving core freedom to move stuff around
2662 $deprecatedCaller = false;
2663 if ($request === null) {
2664 // Set method signature in v10 to: "protected function closeDocument($mode, ServerRequestInterface $request): ?ResponseInterface"
2665 trigger_error('@deprecated since v9, this method will be set to protected in v10', E_USER_DEPRECATED);
2666 $request = $GLOBALS['TYPO3_REQUEST'];
2667 $deprecatedCaller = true;
2668 }
2669
2670 $mode = (int)$mode;
2671 // If current document is found in docHandler,
2672 // then unset it, possibly unset it ALL and finally, write it to the session data
2673 if (isset($this->docHandler[$this->storeUrlMd5])) {
2674 // add the closing document to the recent documents
2675 $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
2676 if (!is_array($recentDocs)) {
2677 $recentDocs = [];
2678 }
2679 $closedDoc = $this->docHandler[$this->storeUrlMd5];
2680 $recentDocs = array_merge([$this->storeUrlMd5 => $closedDoc], $recentDocs);
2681 if (count($recentDocs) > 8) {
2682 $recentDocs = array_slice($recentDocs, 0, 8);
2683 }
2684 // remove it from the list of the open documents
2685 unset($this->docHandler[$this->storeUrlMd5]);
2686 if ($mode === self::DOCUMENT_CLOSE_MODE_CLEAR_ALL) {
2687 $recentDocs = array_merge($this->docHandler, $recentDocs);
2688 $this->docHandler = [];
2689 }
2690 $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
2691 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->docDat[1]]);
2692 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
2693 }
2694 if ($mode === self::DOCUMENT_CLOSE_MODE_NO_REDIRECT) {
2695 return null;
2696 }
2697 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2698 // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by
2699 // other scripts, like wizard_add, to know which records was created or so...
2700 if ($this->returnEditConf && $this->retUrl != (string)$uriBuilder->buildUriFromRoute('dummy')) {
2701 $this->retUrl .= '&returnEditConf=' . rawurlencode(json_encode($this->editconf));
2702 }
2703 // If mode is NOT set (means 0) OR set to 1, then make a header location redirect to $this->retUrl
2704 if ($mode === self::DOCUMENT_CLOSE_MODE_DEFAULT || $mode === self::DOCUMENT_CLOSE_MODE_REDIRECT) {
2705 if ($deprecatedCaller) {
2706 // @deprecated fall back if method has been called from outside. This if can be removed in v10
2707 HttpUtility::redirect($this->retUrl);
2708 }
2709 return new RedirectResponse($this->retUrl, 303);
2710 }
2711 if ($this->retUrl === '') {
2712 return null;
2713 }
2714 $retUrl = $this->returnUrl;
2715 if (is_array($this->docHandler) && !empty($this->docHandler)) {
2716 if (!empty($setupArr[2])) {
2717 $sParts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
2718 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2719 }
2720 }
2721 if ($deprecatedCaller) {
2722 // @deprecated fall back if method has been called from outside. This if can be removed in v10
2723 HttpUtility::redirect($retUrl);
2724 }
2725 return new RedirectResponse($retUrl, 303);
2726 }
2727
2728 /**
2729 * Redirects to the document pointed to by $currentDocFromHandlerMD5 OR $retUrl,
2730 * depending on some internal calculations.
2731 *
2732 * @param string $currentDocFromHandlerMD5 Pointer to the document in the docHandler array
2733 * @param string $retUrl Alternative/Default retUrl
2734 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2735 */
2736 public function setDocument($currentDocFromHandlerMD5 = '', $retUrl = '')
2737 {
2738 trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
2739 if ($retUrl === '') {
2740 return;
2741 }
2742 if (is_array($this->docHandler) && !empty($this->docHandler)) {
2743 if (isset($this->docHandler[$currentDocFromHandlerMD5])) {
2744 $setupArr = $this->docHandler[$currentDocFromHandlerMD5];
2745 } else {
2746 $setupArr = reset($this->docHandler);
2747 }
2748 if ($setupArr[2]) {
2749 $sParts = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
2750 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2751 }
2752 }
2753 HttpUtility::redirect($retUrl);
2754 }
2755
2756 /**
2757 * Emits a signal after a function was executed
2758 *
2759 * @param string $signalName
2760 * @param ServerRequestInterface $request
2761 */
2762 protected function emitFunctionAfterSignal($signalName, ServerRequestInterface $request): void
2763 {
2764 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, $signalName . 'After', [$this, 'request' => $request]);
2765 }
2766
2767 /**
2768 * Get the SignalSlot dispatcher
2769 *
2770 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
2771 */
2772 protected function getSignalSlotDispatcher()
2773 {
2774 if (!isset($this->signalSlotDispatcher)) {
2775 $this->signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
2776 }
2777 return $this->signalSlotDispatcher;
2778 }
2779
2780 /**
2781 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2782 */
2783 protected function getBackendUser()
2784 {
2785 return $GLOBALS['BE_USER'];
2786 }
2787
2788 /**
2789 * Returns LanguageService
2790 *
2791 * @return \TYPO3\CMS\Core\Localization\LanguageService
2792 */
2793 protected function getLanguageService()
2794 {
2795 return $GLOBALS['LANG'];
2796 }
2797 }