[TASK] Use trait for public method access deprecation
[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 function jumpToUrl(URL,formEl) {
946 if (!TBE_EDITOR.isFormChanged()) {
947 window.location.href = URL;
948 } else if (formEl && formEl.type=="checkbox") {
949 formEl.checked = formEl.checked ? 0 : 1;
950 }
951 }
952 // Info view:
953 function launchView(table,uid) {
954 console.warn(\'Calling launchView() has been deprecated in v9 and will be removed in v10.0\');
955 var thePreviewWindow = window.open(
956 ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('show_item') . '&table=') . ' + encodeURIComponent(table) + "&uid=" + encodeURIComponent(uid),
957 "ShowItem" + Math.random().toString(16).slice(2),
958 "height=300,width=410,status=0,menubar=0,resizable=0,location=0,directories=0,scrollbars=1,toolbar=0"
959 );
960 if (thePreviewWindow && thePreviewWindow.focus) {
961 thePreviewWindow.focus();
962 }
963 }
964 function deleteRecord(table,id,url) {
965 window.location.href = ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('tce_db') . '&cmd[') . '+table+"]["+id+"][delete]=1&redirect="+escape(url);
966 }
967 ' . (isset($parsedBody['_savedokview']) && $this->popViewId ? $this->generatePreviewCode() : '')
968 );
969 // Set context sensitive menu
970 $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
971
972 $this->emitFunctionAfterSignal('init', $request);
973 }
974
975 /**
976 * Generate the Javascript for opening the preview window
977 *
978 * @return string
979 */
980 protected function generatePreviewCode(): string
981 {
982 $previewPageId = $this->getPreviewPageId();
983 $previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
984 $anchorSection = $this->getPreviewUrlAnchorSection();
985 $previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
986
987 return '
988 if (window.opener) {
989 '
990 . BackendUtility::viewOnClick(
991 $previewPageId,
992 '',
993 $previewPageRootLine,
994 $anchorSection,
995 $this->viewUrl,
996 $previewUrlParameters,
997 false
998 )
999 . '
1000 } else {
1001 '
1002 . BackendUtility::viewOnClick(
1003 $previewPageId,
1004 '',
1005 $previewPageRootLine,
1006 $anchorSection,
1007 $this->viewUrl,
1008 $previewUrlParameters
1009 )
1010 . '
1011 }';
1012 }
1013
1014 /**
1015 * Returns the parameters for the preview URL
1016 *
1017 * @param int $previewPageId
1018 * @return string
1019 */
1020 protected function getPreviewUrlParameters(int $previewPageId): string
1021 {
1022 $linkParameters = [];
1023 $table = $this->previewData['table'] ?: $this->firstEl['table'];
1024 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
1025 $previewConfiguration = $pageTsConfig['TCEMAIN.']['preview.'][$table . '.'] ?? [];
1026 $recordArray = BackendUtility::getRecord($table, $recordId);
1027
1028 // language handling
1029 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
1030 if ($languageField && !empty($recordArray[$languageField])) {
1031 $l18nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
1032 if ($l18nPointer && !empty($recordArray[$l18nPointer])
1033 && isset($previewConfiguration['useDefaultLanguageRecord'])
1034 && !$previewConfiguration['useDefaultLanguageRecord']
1035 ) {
1036 // use parent record
1037 $recordId = $recordArray[$l18nPointer];
1038 }
1039 $linkParameters['L'] = $recordArray[$languageField];
1040 }
1041
1042 // map record data to GET parameters
1043 if (isset($previewConfiguration['fieldToParameterMap.'])) {
1044 foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
1045 $value = $recordArray[$field];
1046 if ($field === 'uid') {
1047 $value = $recordId;
1048 }
1049 $linkParameters[$parameterName] = $value;
1050 }
1051 }
1052
1053 // add/override parameters by configuration
1054 if (isset($previewConfiguration['additionalGetParameters.'])) {
1055 $additionalGetParameters = [];
1056 $this->parseAdditionalGetParameters(
1057 $additionalGetParameters,
1058 $previewConfiguration['additionalGetParameters.']
1059 );
1060 $linkParameters = array_replace($linkParameters, $additionalGetParameters);
1061 }
1062
1063 if (!empty($previewConfiguration['useCacheHash'])) {
1064 $cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
1065 $fullLinkParameters = GeneralUtility::implodeArrayForUrl('', array_merge($linkParameters, ['id' => $previewPageId]));
1066 $cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters);
1067 $linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters);
1068 } else {
1069 $linkParameters['no_cache'] = 1;
1070 }
1071
1072 return GeneralUtility::implodeArrayForUrl('', $linkParameters, '', false, true);
1073 }
1074
1075 /**
1076 * Returns the anchor section for the preview url
1077 *
1078 * @return string
1079 */
1080 protected function getPreviewUrlAnchorSection(): string
1081 {
1082 $table = $this->previewData['table'] ?: $this->firstEl['table'];
1083 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
1084
1085 return $table === 'tt_content' ? '#c' . (int)$recordId : '';
1086 }
1087
1088 /**
1089 * Returns the preview page id
1090 *
1091 * @return int
1092 */
1093 protected function getPreviewPageId(): int
1094 {
1095 $previewPageId = 0;
1096 $table = $this->previewData['table'] ?: $this->firstEl['table'];
1097 $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
1098 $pageId = $this->popViewId ?: $this->viewId;
1099
1100 if ($table === 'pages') {
1101 $currentPageId = (int)$recordId;
1102 } else {
1103 $currentPageId = MathUtility::convertToPositiveInteger($pageId);
1104 }
1105
1106 $previewConfiguration = $pageTsConfig['TCEMAIN.']['preview.'][$table . '.'] ?? [];
1107
1108 if (isset($previewConfiguration['previewPageId'])) {
1109 $previewPageId = $previewConfiguration['previewPageId'];
1110 }
1111 // if no preview page was configured
1112 if (!$previewPageId) {
1113 $rootPageData = null;
1114 $rootLine = BackendUtility::BEgetRootLine($currentPageId);
1115 $currentPage = reset($rootLine);
1116 // Allow all doktypes below 200
1117 // This makes custom doktype work as well with opening a frontend page.
1118 if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) {
1119 // try the current page
1120 $previewPageId = $currentPageId;
1121 } else {
1122 // or search for the root page
1123 foreach ($rootLine as $page) {
1124 if ($page['is_siteroot']) {
1125 $rootPageData = $page;
1126 break;
1127 }
1128 }
1129 $previewPageId = isset($rootPageData)
1130 ? (int)$rootPageData['uid']
1131 : $currentPageId;
1132 }
1133 }
1134
1135 $this->popViewId = $previewPageId;
1136
1137 return $previewPageId;
1138 }
1139
1140 /**
1141 * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
1142 *
1143 * This basically removes the trailing dots of sub-array keys in TypoScript.
1144 * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
1145 *
1146 * @param array $parameters Should be an empty array by default
1147 * @param array $typoScript The TypoScript configuration
1148 */
1149 protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
1150 {
1151 foreach ($typoScript as $key => $value) {
1152 if (is_array($value)) {
1153 $key = rtrim($key, '.');
1154 $parameters[$key] = [];
1155 $this->parseAdditionalGetParameters($parameters[$key], $value);
1156 } else {
1157 $parameters[$key] = $value;
1158 }
1159 }
1160 }
1161
1162 /**
1163 * Main module operation
1164 *
1165 * @param $request ServerRequestInterface
1166 */
1167 public function main(ServerRequestInterface $request = null): void
1168 {
1169 if ($request === null) {
1170 // Set method signature in v10 to: "protected function main(ServerRequestInterface $request): void"
1171 trigger_error('@deprecated since v9, this method will be set to protected in v10', E_USER_DEPRECATED);
1172 $request = $GLOBALS['TYPO3_REQUEST'];
1173 }
1174
1175 $body = '';
1176 // Begin edit
1177 if (is_array($this->editconf)) {
1178 $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
1179
1180 // Creating the editing form, wrap it with buttons, document selector etc.
1181 $editForm = $this->makeEditForm();
1182 if ($editForm) {
1183 $this->firstEl = reset($this->elementsData);
1184 // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
1185 if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5]))
1186 && !$this->dontStoreDocumentRef
1187 ) {
1188 $this->docHandler[$this->storeUrlMd5] = [
1189 $this->storeTitle,
1190 $this->storeArray,
1191 $this->storeUrl,
1192 $this->firstEl
1193 ];
1194 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
1195 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
1196 }
1197 $body = $this->formResultCompiler->addCssFiles();
1198 $body .= $this->compileForm($editForm);
1199 $body .= $this->formResultCompiler->printNeededJSFunctions();
1200 $body .= '</form>';
1201 }
1202 }
1203 // Access check...
1204 // The page will show only if there is a valid page and if this page may be viewed by the user
1205 $this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
1206 if ($this->pageinfo) {
1207 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
1208 }
1209 // Setting up the buttons and markers for doc header
1210 $this->getButtons($request);
1211 $this->languageSwitch(
1212 (string)($this->firstEl['table'] ?? ''),
1213 (int)($this->firstEl['uid'] ?? 0),
1214 isset($this->firstEl['pid']) ? (int)$this->firstEl['pid'] : null
1215 );
1216 $this->moduleTemplate->setContent($body);
1217 }
1218
1219 /**
1220 * Creates the editing form with FormEngine, based on the input from GPvars.
1221 *
1222 * @return string HTML form elements wrapped in tables
1223 */
1224 protected function makeEditForm(): string
1225 {
1226 // Initialize variables
1227 $this->elementsData = [];
1228 $this->errorC = 0;
1229 $this->newC = 0;
1230 $editForm = '';
1231 $trData = null;
1232 $beUser = $this->getBackendUser();
1233 // Traverse the GPvar edit array tables
1234 foreach ($this->editconf as $table => $conf) {
1235 if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
1236 // Traverse the keys/comments of each table (keys can be a comma list of uids)
1237 foreach ($conf as $cKey => $command) {
1238 if ($command === 'edit' || $command === 'new') {
1239 // Get the ids:
1240 $ids = GeneralUtility::trimExplode(',', $cKey, true);
1241 // Traverse the ids:
1242 foreach ($ids as $theUid) {
1243 // Don't save this document title in the document selector if the document is new.
1244 if ($command === 'new') {
1245 $this->dontStoreDocumentRef = 1;
1246 }
1247
1248 try {
1249 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
1250 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
1251 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
1252
1253 // Reset viewId - it should hold data of last entry only
1254 $this->viewId = 0;
1255 $this->viewId_addParams = '';
1256
1257 $formDataCompilerInput = [
1258 'tableName' => $table,
1259 'vanillaUid' => (int)$theUid,
1260 'command' => $command,
1261 'returnUrl' => $this->R_URI,
1262 ];
1263 if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
1264 $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
1265 }
1266 if (!empty($this->defVals) && is_array($this->defVals)) {
1267 $formDataCompilerInput['defaultValues'] = $this->defVals;
1268 }
1269
1270 $formData = $formDataCompiler->compile($formDataCompilerInput);
1271
1272 // Set this->viewId if possible
1273 if ($command === 'new'
1274 && $table !== 'pages'
1275 && !empty($formData['parentPageRow']['uid'])
1276 ) {
1277 $this->viewId = $formData['parentPageRow']['uid'];
1278 } else {
1279 if ($table === 'pages') {
1280 $this->viewId = $formData['databaseRow']['uid'];
1281 } elseif (!empty($formData['parentPageRow']['uid'])) {
1282 $this->viewId = $formData['parentPageRow']['uid'];
1283 // Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
1284 if (!empty($formData['processedTca']['ctrl']['languageField'])
1285 && is_array($formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']])
1286 && $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0] > 0
1287 ) {
1288 $this->viewId_addParams = '&L=' . $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0];
1289 }
1290 }
1291 }
1292
1293 // Determine if delete button can be shown
1294 $deleteAccess = false;
1295 if (
1296 $command === 'edit'
1297 || $command === 'new'
1298 ) {
1299 $permission = $formData['userPermissionOnPage'];
1300 if ($formData['tableName'] === 'pages') {
1301 $deleteAccess = $permission & Permission::PAGE_DELETE ? true : false;
1302 } else {
1303 $deleteAccess = $permission & Permission::CONTENT_EDIT ? true : false;
1304 }
1305 }
1306
1307 // Display "is-locked" message
1308 if ($command === 'edit') {
1309 $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
1310 if ($lockInfo) {
1311 $flashMessage = GeneralUtility::makeInstance(
1312 FlashMessage::class,
1313 $lockInfo['msg'],
1314 '',
1315 FlashMessage::WARNING
1316 );
1317 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1318 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1319 $defaultFlashMessageQueue->enqueue($flashMessage);
1320 }
1321 }
1322
1323 // Record title
1324 if (!$this->storeTitle) {
1325 $this->storeTitle = $this->recTitle
1326 ? htmlspecialchars($this->recTitle)
1327 : BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($formData['databaseRow']), true);
1328 }
1329
1330 $this->elementsData[] = [
1331 'table' => $table,
1332 'uid' => $formData['databaseRow']['uid'],
1333 'pid' => $formData['databaseRow']['pid'],
1334 'cmd' => $command,
1335 'deleteAccess' => $deleteAccess
1336 ];
1337
1338 if ($command !== 'new') {
1339 BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
1340 }
1341
1342 // Set list if only specific fields should be rendered. This will trigger
1343 // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1344 if ($this->columnsOnly) {
1345 if (is_array($this->columnsOnly)) {
1346 $formData['fieldListToRender'] = $this->columnsOnly[$table];
1347 } else {
1348 $formData['fieldListToRender'] = $this->columnsOnly;
1349 }
1350 }
1351
1352 $formData['renderType'] = 'outerWrapContainer';
1353 $formResult = $nodeFactory->create($formData)->render();
1354
1355 $html = $formResult['html'];
1356
1357 $formResult['html'] = '';
1358 $formResult['doSaveFieldName'] = 'doSave';
1359
1360 // @todo: Put all the stuff into FormEngine as final "compiler" class
1361 // @todo: This is done here for now to not rewrite addCssFiles()
1362 // @todo: and printNeededJSFunctions() now
1363 $this->formResultCompiler->mergeResult($formResult);
1364
1365 // Seems the pid is set as hidden field (again) at end?!
1366 if ($command === 'new') {
1367 // @todo: looks ugly
1368 $html .= LF
1369 . '<input type="hidden"'
1370 . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($formData['databaseRow']['uid']) . '][pid]"'
1371 . ' value="' . (int)$formData['databaseRow']['pid'] . '" />';
1372 $this->newC++;
1373 }
1374
1375 $editForm .= $html;
1376 } catch (AccessDeniedException $e) {
1377 $this->errorC++;
1378 // Try to fetch error message from "recordInternals" be user object
1379 // @todo: This construct should be logged and localized and de-uglified
1380 $message = $beUser->errorMsg;
1381 if (empty($message)) {
1382 // Create message from exception.
1383 $message = $e->getMessage() . ' ' . $e->getCode();
1384 }
1385 $editForm .= htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission'))
1386 . '<br /><br />' . htmlspecialchars($message) . '<br /><br />';
1387 } catch (DatabaseRecordException $e) {
1388 $editForm = '<div class="alert alert-warning">' . htmlspecialchars($e->getMessage()) . '</div>';
1389 }
1390 } // End of for each uid
1391 }
1392 }
1393 }
1394 }
1395 return $editForm;
1396 }
1397
1398 /**
1399 * Create the panel of buttons for submitting the form or otherwise perform operations.
1400 *
1401 * @param $request ServerRequestInterface
1402 */
1403 protected function getButtons(ServerRequestInterface $request): void
1404 {
1405 $record = BackendUtility::getRecord($this->firstEl['table'], $this->firstEl['uid']);
1406 $TCActrl = $GLOBALS['TCA'][$this->firstEl['table']]['ctrl'];
1407
1408 $this->setIsSavedRecord();
1409
1410 $sysLanguageUid = 0;
1411 if (
1412 $this->isSavedRecord
1413 && isset($TCActrl['languageField'], $record[$TCActrl['languageField']])
1414 ) {
1415 $sysLanguageUid = (int)$record[$TCActrl['languageField']];
1416 } elseif (isset($this->defVals['sys_language_uid'])) {
1417 $sysLanguageUid = (int)$this->defVals['sys_language_uid'];
1418 }
1419
1420 $l18nParent = isset($TCActrl['transOrigPointerField'], $record[$TCActrl['transOrigPointerField']])
1421 ? (int)$record[$TCActrl['transOrigPointerField']]
1422 : 0;
1423
1424 $this->setIsPageInFreeTranslationMode($record, $sysLanguageUid);
1425
1426 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1427
1428 $this->registerCloseButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 1);
1429
1430 // Show buttons when table is not read-only
1431 if (
1432 !$this->errorC
1433 && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']
1434 ) {
1435 $this->registerSaveButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 2);
1436 $this->registerViewButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 3);
1437 $this->registerNewButtonToButtonBar(
1438 $buttonBar,
1439 ButtonBar::BUTTON_POSITION_LEFT,
1440 4,
1441 $sysLanguageUid,
1442 $l18nParent
1443 );
1444 $this->registerDuplicationButtonToButtonBar(
1445 $buttonBar,
1446 ButtonBar::BUTTON_POSITION_LEFT,
1447 5,
1448 $sysLanguageUid,
1449 $l18nParent
1450 );
1451 $this->registerDeleteButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 6);
1452 $this->registerColumnsOnlyButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 7);
1453 $this->registerHistoryButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1454 }
1455
1456 $this->registerOpenInNewWindowButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 2);
1457 $this->registerShortcutButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 3);
1458 $this->registerCshButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 4);
1459 }
1460
1461 /**
1462 * Set the boolean to check if the record is saved
1463 */
1464 protected function setIsSavedRecord()
1465 {
1466 if (!is_bool($this->isSavedRecord)) {
1467 $this->isSavedRecord = (
1468 $this->firstEl['cmd'] !== 'new'
1469 && MathUtility::canBeInterpretedAsInteger($this->firstEl['uid'])
1470 );
1471 }
1472 }
1473
1474 /**
1475 * Returns if inconsistent language handling is allowed
1476 *
1477 * @return bool
1478 */
1479 protected function isInconsistentLanguageHandlingAllowed(): bool
1480 {
1481 $allowInconsistentLanguageHandling = BackendUtility::getPagesTSconfig(
1482 $this->pageinfo['uid']
1483 )['mod']['web_layout']['allowInconsistentLanguageHandling'];
1484
1485 return $allowInconsistentLanguageHandling['value'] === '1';
1486 }
1487
1488 /**
1489 * Set the boolean to check if the page is in free translation mode
1490 *
1491 * @param array|null $record
1492 * @param int $sysLanguageUid
1493 */
1494 protected function setIsPageInFreeTranslationMode($record, int $sysLanguageUid)
1495 {
1496 if ($this->firstEl['table'] === 'tt_content') {
1497 if (!$this->isSavedRecord) {
1498 $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1499 (int)$this->pageinfo['uid'],
1500 (int)$this->defVals['colPos'],
1501 $sysLanguageUid
1502 );
1503 } else {
1504 $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1505 (int)$this->pageinfo['uid'],
1506 (int)$record['colPos'],
1507 $sysLanguageUid
1508 );
1509 }
1510 }
1511 }
1512
1513 /**
1514 * Check if the page is in free translation mode
1515 *
1516 * @param int $page
1517 * @param int $column
1518 * @param int $language
1519 * @return bool
1520 */
1521 protected function getFreeTranslationMode(int $page, int $column, int $language): bool
1522 {
1523 $freeTranslationMode = false;
1524
1525 if (
1526 $this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0
1527 && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0
1528 ) {
1529 $freeTranslationMode = true;
1530 }
1531
1532 return $freeTranslationMode;
1533 }
1534
1535 /**
1536 * Register the close button to the button bar
1537 *
1538 * @param ButtonBar $buttonBar
1539 * @param string $position
1540 * @param int $group
1541 */
1542 protected function registerCloseButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1543 {
1544 $closeButton = $buttonBar->makeLinkButton()
1545 ->setHref('#')
1546 ->setClasses('t3js-editform-close')
1547 ->setTitle($this->getLanguageService()->sL(
1548 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'
1549 ))
1550 ->setShowLabelText(true)
1551 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1552 'actions-close',
1553 Icon::SIZE_SMALL
1554 ));
1555
1556 $buttonBar->addButton($closeButton, $position, $group);
1557 }
1558
1559 /**
1560 * Register the save button to the button bar
1561 *
1562 * @param ButtonBar $buttonBar
1563 * @param string $position
1564 * @param int $group
1565 */
1566 protected function registerSaveButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1567 {
1568 $saveButton = $buttonBar->makeInputButton()
1569 ->setForm('EditDocumentController')
1570 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1571 ->setName('_savedok')
1572 ->setShowLabelText(true)
1573 ->setTitle($this->getLanguageService()->sL(
1574 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'
1575 ))
1576 ->setValue('1');
1577
1578 $buttonBar->addButton($saveButton, $position, $group);
1579 }
1580
1581 /**
1582 * Register the view button to the button bar
1583 *
1584 * @param ButtonBar $buttonBar
1585 * @param string $position
1586 * @param int $group
1587 */
1588 protected function registerViewButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1589 {
1590 if (
1591 $this->viewId // Pid to show the record
1592 && !$this->noView // Passed parameter
1593 && !empty($this->firstEl['table']) // No table
1594
1595 // @TODO: TsConfig option should change to viewDoc
1596 && $this->getTsConfigOption($this->firstEl['table'], 'saveDocView')
1597 ) {
1598 $classNames = 't3js-editform-view';
1599
1600 $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
1601
1602 if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1603 $excludeDokTypes = GeneralUtility::intExplode(
1604 ',',
1605 $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
1606 true
1607 );
1608 } else {
1609 // exclude sysfolders, spacers and recycler by default
1610 $excludeDokTypes = [
1611 PageRepository::DOKTYPE_RECYCLER,
1612 PageRepository::DOKTYPE_SYSFOLDER,
1613 PageRepository::DOKTYPE_SPACER
1614 ];
1615 }
1616
1617 if (
1618 !in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)
1619 || isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl['table'] . '.']['previewPageId'])
1620 ) {
1621 $previewPageId = $this->getPreviewPageId();
1622 $previewUrl = BackendUtility::getPreviewUrl(
1623 $previewPageId,
1624 '',
1625 BackendUtility::BEgetRootLine($previewPageId),
1626 $this->getPreviewUrlAnchorSection(),
1627 $this->viewUrl,
1628 $this->getPreviewUrlParameters($previewPageId)
1629 );
1630
1631 $viewButton = $buttonBar->makeLinkButton()
1632 ->setHref($previewUrl)
1633 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1634 'actions-view',
1635 Icon::SIZE_SMALL
1636 ))
1637 ->setShowLabelText(true)
1638 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc'));
1639
1640 if (!$this->isSavedRecord) {
1641 if ($this->firstEl['table'] === 'pages') {
1642 $viewButton->setDataAttributes(['is-new' => '']);
1643 }
1644 }
1645
1646 if ($classNames !== '') {
1647 $viewButton->setClasses($classNames);
1648 }
1649
1650 $buttonBar->addButton($viewButton, $position, $group);
1651 }
1652 }
1653 }
1654
1655 /**
1656 * Register the new button to the button bar
1657 *
1658 * @param ButtonBar $buttonBar
1659 * @param string $position
1660 * @param int $group
1661 * @param int $sysLanguageUid
1662 * @param int $l18nParent
1663 */
1664 protected function registerNewButtonToButtonBar(
1665 ButtonBar $buttonBar,
1666 string $position,
1667 int $group,
1668 int $sysLanguageUid,
1669 int $l18nParent
1670 ) {
1671 if (
1672 $this->firstEl['table'] !== 'sys_file_metadata'
1673 && !empty($this->firstEl['table'])
1674 && (
1675 (
1676 (
1677 $this->isInconsistentLanguageHandlingAllowed()
1678 || $this->isPageInFreeTranslationMode
1679 )
1680 && $this->firstEl['table'] === 'tt_content'
1681 )
1682 || (
1683 $this->firstEl['table'] !== 'tt_content'
1684 && (
1685 $sysLanguageUid === 0
1686 || $l18nParent === 0
1687 )
1688 )
1689 )
1690 ) {
1691 $classNames = 't3js-editform-new';
1692
1693 $newButton = $buttonBar->makeLinkButton()
1694 ->setHref('#')
1695 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1696 'actions-add',
1697 Icon::SIZE_SMALL
1698 ))
1699 ->setShowLabelText(true)
1700 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc'));
1701
1702 if (!$this->isSavedRecord) {
1703 $newButton->setDataAttributes(['is-new' => '']);
1704 }
1705
1706 if ($classNames !== '') {
1707 $newButton->setClasses($classNames);
1708 }
1709
1710 $buttonBar->addButton($newButton, $position, $group);
1711 }
1712 }
1713
1714 /**
1715 * Register the duplication button to the button bar
1716 *
1717 * @param ButtonBar $buttonBar
1718 * @param string $position
1719 * @param int $group
1720 * @param int $sysLanguageUid
1721 * @param int $l18nParent
1722 */
1723 protected function registerDuplicationButtonToButtonBar(
1724 ButtonBar $buttonBar,
1725 string $position,
1726 int $group,
1727 int $sysLanguageUid,
1728 int $l18nParent
1729 ) {
1730 if (
1731 $this->firstEl['table'] !== 'sys_file_metadata'
1732 && !empty($this->firstEl['table'])
1733 && (
1734 (
1735 (
1736 $this->isInconsistentLanguageHandlingAllowed()
1737 || $this->isPageInFreeTranslationMode
1738 )
1739 && $this->firstEl['table'] === 'tt_content'
1740 )
1741 || (
1742 $this->firstEl['table'] !== 'tt_content'
1743 && (
1744 $sysLanguageUid === 0
1745 || $l18nParent === 0
1746 )
1747 )
1748 )
1749 && $this->getTsConfigOption($this->firstEl['table'], 'showDuplicate')
1750 ) {
1751 $classNames = 't3js-editform-duplicate';
1752
1753 $duplicateButton = $buttonBar->makeLinkButton()
1754 ->setHref('#')
1755 ->setShowLabelText(true)
1756 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc'))
1757 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1758 'actions-document-duplicates-select',
1759 Icon::SIZE_SMALL
1760 ));
1761
1762 if (!$this->isSavedRecord) {
1763 $duplicateButton->setDataAttributes(['is-new' => '']);
1764 }
1765
1766 if ($classNames !== '') {
1767 $duplicateButton->setClasses($classNames);
1768 }
1769
1770 $buttonBar->addButton($duplicateButton, $position, $group);
1771 }
1772 }
1773
1774 /**
1775 * Register the delete button to the button bar
1776 *
1777 * @param ButtonBar $buttonBar
1778 * @param string $position
1779 * @param int $group
1780 */
1781 protected function registerDeleteButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1782 {
1783 if (
1784 $this->firstEl['deleteAccess']
1785 && !$this->getDisableDelete()
1786 && $this->isSavedRecord
1787 && count($this->elementsData) === 1
1788 ) {
1789 $classNames = 't3js-editform-delete-record';
1790
1791 $returnUrl = $this->retUrl;
1792 if ($this->firstEl['table'] === 'pages') {
1793 parse_str((string)parse_url($returnUrl, PHP_URL_QUERY), $queryParams);
1794 if (
1795 isset($queryParams['route'], $queryParams['id'])
1796 && (string)$this->firstEl['uid'] === (string)$queryParams['id']
1797 ) {
1798
1799 /** @var UriBuilder $uriBuilder */
1800 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1801
1802 // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
1803 // tree from the outside to be able to mark the pid as active
1804 $returnUrl = (string)$uriBuilder->buildUriFromRoutePath($queryParams['route'], ['id' => 0]);
1805 }
1806 }
1807
1808 /** @var ReferenceIndex $referenceIndex */
1809 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1810 $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords(
1811 $this->firstEl['table'],
1812 (int)$this->firstEl['uid']
1813 );
1814
1815 $referenceCountMessage = BackendUtility::referenceCount(
1816 $this->firstEl['table'],
1817 (int)$this->firstEl['uid'],
1818 $this->getLanguageService()->sL(
1819 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'
1820 ),
1821 $numberOfReferences
1822 );
1823 $translationCountMessage = BackendUtility::translationCount(
1824 $this->firstEl['table'],
1825 (int)$this->firstEl['uid'],
1826 $this->getLanguageService()->sL(
1827 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'
1828 )
1829 );
1830
1831 $deleteButton = $buttonBar->makeLinkButton()
1832 ->setClasses($classNames)
1833 ->setDataAttributes([
1834 'return-url' => $returnUrl,
1835 'uid' => $this->firstEl['uid'],
1836 'table' => $this->firstEl['table'],
1837 'reference-count-message' => $referenceCountMessage,
1838 'translation-count-message' => $translationCountMessage
1839 ])
1840 ->setHref('#')
1841 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1842 'actions-edit-delete',
1843 Icon::SIZE_SMALL
1844 ))
1845 ->setShowLabelText(true)
1846 ->setTitle($this->getLanguageService()->getLL('deleteItem'));
1847
1848 $buttonBar->addButton($deleteButton, $position, $group);
1849 }
1850 }
1851
1852 /**
1853 * Register the history button to the button bar
1854 *
1855 * @param ButtonBar $buttonBar
1856 * @param string $position
1857 * @param int $group
1858 */
1859 protected function registerHistoryButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1860 {
1861 if (
1862 count($this->elementsData) === 1
1863 && !empty($this->firstEl['table'])
1864 && $this->getTsConfigOption($this->firstEl['table'], 'showHistory')
1865 ) {
1866 /** @var UriBuilder $uriBuilder */
1867 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1868
1869 $historyButtonOnClick = 'window.location.href=' .
1870 GeneralUtility::quoteJSvalue(
1871 (string)$uriBuilder->buildUriFromRoute(
1872 'record_history',
1873 [
1874 'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1875 'returnUrl' => $this->R_URI,
1876 ]
1877 )
1878 ) . '; return false;';
1879
1880 $historyButton = $buttonBar->makeLinkButton()
1881 ->setHref('#')
1882 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1883 'actions-document-history-open',
1884 Icon::SIZE_SMALL
1885 ))
1886 ->setOnClick($historyButtonOnClick)
1887 ->setTitle('Open history of this record')
1888 ;
1889
1890 $buttonBar->addButton($historyButton, $position, $group);
1891 }
1892 }
1893
1894 /**
1895 * Register the columns only button to the button bar
1896 *
1897 * @param ButtonBar $buttonBar
1898 * @param string $position
1899 * @param int $group
1900 */
1901 protected function registerColumnsOnlyButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1902 {
1903 if (
1904 $this->columnsOnly
1905 && count($this->elementsData) === 1
1906 ) {
1907 $columnsOnlyButton = $buttonBar->makeLinkButton()
1908 ->setHref($this->R_URI . '&columnsOnly=')
1909 ->setTitle($this->getLanguageService()->getLL('editWholeRecord'))
1910 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1911 'actions-open',
1912 Icon::SIZE_SMALL
1913 ));
1914
1915 $buttonBar->addButton($columnsOnlyButton, $position, $group);
1916 }
1917 }
1918
1919 /**
1920 * Register the open in new window button to the button bar
1921 *
1922 * @param ButtonBar $buttonBar
1923 * @param string $position
1924 * @param int $group
1925 */
1926 protected function registerOpenInNewWindowButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1927 {
1928 $closeUrl = $this->getCloseUrl();
1929 if ($this->returnUrl !== $closeUrl) {
1930 $requestUri = GeneralUtility::linkThisScript([
1931 'returnUrl' => $closeUrl,
1932 ]);
1933 $aOnClick = 'vHWin=window.open('
1934 . GeneralUtility::quoteJSvalue($requestUri) . ','
1935 . GeneralUtility::quoteJSvalue(md5($this->R_URI))
1936 . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1937
1938 $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
1939 ->makeLinkButton()
1940 ->setHref('#')
1941 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
1942 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL))
1943 ->setOnClick($aOnClick);
1944
1945 $buttonBar->addButton($openInNewWindowButton, $position, $group);
1946 }
1947 }
1948
1949 /**
1950 * Register the shortcut button to the button bar
1951 *
1952 * @param ButtonBar $buttonBar
1953 * @param string $position
1954 * @param int $group
1955 */
1956 protected function registerShortcutButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1957 {
1958 if ($this->returnUrl !== $this->getCloseUrl()) {
1959 $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
1960 $shortCutButton->setModuleName('xMOD_alt_doc.php')
1961 ->setGetVariables([
1962 'returnUrl',
1963 'edit',
1964 'defVals',
1965 'overrideVals',
1966 'columnsOnly',
1967 'returnNewPageId',
1968 'noView']);
1969
1970 $buttonBar->addButton($shortCutButton, $position, $group);
1971 }
1972 }
1973
1974 /**
1975 * Register the CSH button to the button bar
1976 *
1977 * @param ButtonBar $buttonBar
1978 * @param string $position
1979 * @param int $group
1980 */
1981 protected function registerCshButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1982 {
1983 $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('TCEforms');
1984
1985 $buttonBar->addButton($cshButton, $position, $group);
1986 }
1987
1988 /**
1989 * Get the count of connected translated content elements
1990 *
1991 * @param int $page
1992 * @param int $column
1993 * @param int $language
1994 * @return int
1995 */
1996 protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int
1997 {
1998 $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1999
2000 return (int)$queryBuilder
2001 ->andWhere(
2002 $queryBuilder->expr()->gt(
2003 $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
2004 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2005 )
2006 )
2007 ->execute()
2008 ->fetchColumn(0);
2009 }
2010
2011 /**
2012 * Get the count of standalone translated content elements
2013 *
2014 * @param int $page
2015 * @param int $column
2016 * @param int $language
2017 * @return int
2018 */
2019 protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int
2020 {
2021 $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
2022
2023 return (int)$queryBuilder
2024 ->andWhere(
2025 $queryBuilder->expr()->eq(
2026 $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
2027 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2028 )
2029 )
2030 ->execute()
2031 ->fetchColumn(0);
2032 }
2033
2034 /**
2035 * Get the query builder for the translation mode
2036 *
2037 * @param int $page
2038 * @param int $column
2039 * @param int $language
2040 * @return QueryBuilder
2041 */
2042 protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder
2043 {
2044 $languageField = $GLOBALS['TCA']['tt_content']['ctrl']['languageField'];
2045
2046 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2047 ->getQueryBuilderForTable('tt_content');
2048
2049 $queryBuilder->getRestrictions()
2050 ->removeAll()
2051 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2052 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2053
2054 return $queryBuilder
2055 ->count('uid')
2056 ->from('tt_content')
2057 ->where(
2058 $queryBuilder->expr()->eq(
2059 'pid',
2060 $queryBuilder->createNamedParameter($page, \PDO::PARAM_INT)
2061 ),
2062 $queryBuilder->expr()->eq(
2063 $languageField,
2064 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
2065 ),
2066 $queryBuilder->expr()->eq(
2067 'colPos',
2068 $queryBuilder->createNamedParameter($column, \PDO::PARAM_INT)
2069 )
2070 );
2071 }
2072
2073 /**
2074 * Put together the various elements (buttons, selectors, form) into a table
2075 *
2076 * @param string $editForm HTML form.
2077 * @return string Composite HTML
2078 */
2079 protected function compileForm(string $editForm): string
2080 {
2081 $formContent = '
2082 <form
2083 action="' . htmlspecialchars($this->R_URI) . '"
2084 method="post"
2085 enctype="multipart/form-data"
2086 name="editform"
2087 id="EditDocumentController"
2088 onsubmit="TBE_EDITOR.checkAndDoSubmit(1); return false;"
2089 >
2090 ' . $editForm . '
2091 <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
2092 <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />
2093 <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$this->viewId) . '" />
2094 <input type="hidden" name="closeDoc" value="0" />
2095 <input type="hidden" name="doSave" value="0" />
2096 <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
2097 <input type="hidden" name="_scrollPosition" value="" />';
2098 if ($this->returnNewPageId) {
2099 $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
2100 }
2101 if ($this->viewId_addParams) {
2102 $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams) . '" />';
2103 }
2104 return $formContent;
2105 }
2106
2107 /**
2108 * Create shortcut icon
2109 *
2110 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2111 */
2112 public function shortCutLink()
2113 {
2114 trigger_error('Method shortCutLink() will be removed in v10', E_USER_DEPRECATED);
2115
2116 if ($this->returnUrl !== $this->getCloseUrl()) {
2117 $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
2118 $shortCutButton->setModuleName('xMOD_alt_doc.php')
2119 ->setGetVariables([
2120 'returnUrl',
2121 'edit',
2122 'defVals',
2123 'overrideVals',
2124 'columnsOnly',
2125 'returnNewPageId',
2126 'noView']);
2127 $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->addButton($shortCutButton);
2128 }
2129 }
2130
2131 /**
2132 * Creates open-in-window link
2133 *
2134 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2135 */
2136 public function openInNewWindowLink()
2137 {
2138 trigger_error('Method openInNewWindowLink() will be removed in v10', E_USER_DEPRECATED);
2139
2140 $closeUrl = $this->getCloseUrl();
2141 if ($this->returnUrl !== $closeUrl) {
2142 $aOnClick = 'vHWin=window.open(' . GeneralUtility::quoteJSvalue(GeneralUtility::linkThisScript(
2143 ['returnUrl' => $closeUrl]
2144 ))
2145 . ','
2146 . GeneralUtility::quoteJSvalue(md5($this->R_URI))
2147 . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
2148 $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
2149 ->makeLinkButton()
2150 ->setHref('#')
2151 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
2152 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL))
2153 ->setOnClick($aOnClick);
2154 $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->addButton(
2155 $openInNewWindowButton,
2156 ButtonBar::BUTTON_POSITION_RIGHT
2157 );
2158 }
2159 }
2160
2161 /**
2162 * Returns if delete for the current table is disabled by configuration.
2163 * For sys_file_metadata in default language delete is always disabled.
2164 *
2165 * @return bool
2166 */
2167 protected function getDisableDelete(): bool
2168 {
2169 $disableDelete = false;
2170 if ($this->firstEl['table'] === 'sys_file_metadata') {
2171 $row = BackendUtility::getRecord('sys_file_metadata', $this->firstEl['uid'], 'sys_language_uid');
2172 $languageUid = $row['sys_language_uid'];
2173 if ($languageUid === 0) {
2174 $disableDelete = true;
2175 }
2176 } else {
2177 $disableDelete = (bool)$this->getTsConfigOption($this->firstEl['table'] ?? '', 'disableDelete');
2178 }
2179 return $disableDelete;
2180 }
2181
2182 /**
2183 * Returns the URL (usually for the "returnUrl") which closes the current window.
2184 * Used when editing a record in a popup.
2185 *
2186 * @return string
2187 */
2188 protected function getCloseUrl(): string
2189 {
2190 $closeUrl = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
2191 return PathUtility::getAbsoluteWebPath($closeUrl);
2192 }
2193
2194 /***************************
2195 *
2196 * Localization stuff
2197 *
2198 ***************************/
2199 /**
2200 * Make selector box for creating new translation for a record or switching to edit the record in an existing
2201 * language.
2202 * Displays only languages which are available for the current page.
2203 *
2204 * @param string $table Table name
2205 * @param int $uid Uid for which to create a new language
2206 * @param int $pid|null Pid of the record
2207 */
2208 protected function languageSwitch(string $table, int $uid, $pid = null)
2209 {
2210 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
2211 $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2212 /** @var UriBuilder $uriBuilder */
2213 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2214
2215 // Table editable and activated for languages?
2216 if ($this->getBackendUser()->check('tables_modify', $table)
2217 && $languageField
2218 && $transOrigPointerField
2219 ) {
2220 if ($pid === null) {
2221 $row = BackendUtility::getRecord($table, $uid, 'pid');
2222 $pid = $row['pid'];
2223 }
2224 // Get all available languages for the page
2225 // If editing a page, the translations of the current UID need to be fetched
2226 if ($table === 'pages') {
2227 $row = BackendUtility::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
2228 // Ensure the check is always done against the default language page
2229 $langRows = $this->getLanguages(
2230 (int)$row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $uid,
2231 $table
2232 );
2233 } else {
2234 $langRows = $this->getLanguages((int)$pid, $table);
2235 }
2236 // Page available in other languages than default language?
2237 if (is_array($langRows) && count($langRows) > 1) {
2238 $rowsByLang = [];
2239 $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
2240 // Get record in current language
2241 $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
2242 if (!is_array($rowCurrent)) {
2243 $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
2244 }
2245 $currentLanguage = (int)$rowCurrent[$languageField];
2246 // Disabled for records with [all] language!
2247 if ($currentLanguage > -1) {
2248 // Get record in default language if needed
2249 if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
2250 $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
2251 $table,
2252 $rowCurrent[$transOrigPointerField],
2253 $fetchFields
2254 );
2255 if (!is_array($rowsByLang[0])) {
2256 $rowsByLang[0] = BackendUtility::getRecord(
2257 $table,
2258 $rowCurrent[$transOrigPointerField],
2259 $fetchFields
2260 );
2261 }
2262 } else {
2263 $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
2264 }
2265 if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
2266 // Get record in other languages to see what's already available
2267
2268 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2269 ->getQueryBuilderForTable($table);
2270
2271 $queryBuilder->getRestrictions()
2272 ->removeAll()
2273 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2274 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2275
2276 $result = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fetchFields, true))
2277 ->from($table)
2278 ->where(
2279 $queryBuilder->expr()->eq(
2280 'pid',
2281 $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
2282 ),
2283 $queryBuilder->expr()->gt(
2284 $languageField,
2285 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2286 ),
2287 $queryBuilder->expr()->eq(
2288 $transOrigPointerField,
2289 $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO::PARAM_INT)
2290 )
2291 )
2292 ->execute();
2293
2294 while ($row = $result->fetch()) {
2295 $rowsByLang[$row[$languageField]] = $row;
2296 }
2297 }
2298 $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
2299 $languageMenu->setIdentifier('_langSelector');
2300 foreach ($langRows as $lang) {
2301 if ($this->getBackendUser()->checkLanguageAccess($lang['uid'])) {
2302 $newTranslation = isset($rowsByLang[$lang['uid']]) ? '' : ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
2303 // Create url for creating a localized record
2304 $addOption = true;
2305 $href = '';
2306 if ($newTranslation) {
2307 $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2308 'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $lang['uid'],
2309 'returnUrl' => $this->retUrl
2310 ]);
2311
2312 if (array_key_exists(0, $rowsByLang)) {
2313 $href = BackendUtility::getLinkToDataHandlerAction(
2314 '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $lang['uid'],
2315 $redirectUrl
2316 );
2317 } else {
2318 $addOption = false;
2319 }
2320 } else {
2321 $href = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2322 'edit[' . $table . '][' . $rowsByLang[$lang['uid']]['uid'] . ']' => 'edit',
2323 'returnUrl' => $this->retUrl
2324 ]);
2325 }
2326 if ($addOption) {
2327 $menuItem = $languageMenu->makeMenuItem()
2328 ->setTitle($lang['title'] . $newTranslation)
2329 ->setHref($href);
2330 if ((int)$lang['uid'] === $currentLanguage) {
2331 $menuItem->setActive(true);
2332 }
2333 $languageMenu->addMenuItem($menuItem);
2334 }
2335 }
2336 }
2337 $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
2338 }
2339 }
2340 }
2341 }
2342
2343 /**
2344 * Redirects to FormEngine with new parameters to edit a just created localized record
2345 *
2346 * @param ServerRequestInterface $request Incoming request object
2347 * @return ResponseInterface|null Possible redirect response
2348 */
2349 public function localizationRedirect(ServerRequestInterface $request = null): ?ResponseInterface
2350 {
2351 $deprecatedCaller = false;
2352 if (!$request instanceof ServerRequestInterface) {
2353 // @deprecated since TYPO3 v9
2354 // Method signature in v10: protected function localizationRedirect(ServerRequestInterface $request): ?ResponseInterface
2355 trigger_error('Method localizationRedirect() will be set to protected in v10. Do not call from other extension', E_USER_DEPRECATED);
2356 $justLocalized = $request;
2357 $request = $GLOBALS['TYPO3_REQUEST'];
2358 $deprecatedCaller = true;
2359 } else {
2360 $justLocalized = $request->getQueryParams()['justLocalized'];
2361 }
2362
2363 if (empty($justLocalized)) {
2364 return null;
2365 }
2366
2367 list($table, $origUid, $language) = explode(':', $justLocalized);
2368
2369 if ($GLOBALS['TCA'][$table]
2370 && $GLOBALS['TCA'][$table]['ctrl']['languageField']
2371 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
2372 ) {
2373 $parsedBody = $request->getParsedBody();
2374 $queryParams = $request->getQueryParams();
2375
2376 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2377 $queryBuilder->getRestrictions()
2378 ->removeAll()
2379 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2380 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2381 $localizedRecord = $queryBuilder->select('uid')
2382 ->from($table)
2383 ->where(
2384 $queryBuilder->expr()->eq(
2385 $GLOBALS['TCA'][$table]['ctrl']['languageField'],
2386 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
2387 ),
2388 $queryBuilder->expr()->eq(
2389 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2390 $queryBuilder->createNamedParameter($origUid, \PDO::PARAM_INT)
2391 )
2392 )
2393 ->execute()
2394 ->fetch();
2395 $returnUrl = $parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '';
2396 if (is_array($localizedRecord)) {
2397 if ($deprecatedCaller) {
2398 // @deprecated fall back if method has been called from outside. This if can be removed in v10
2399 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2400 $location = (string)$uriBuilder->buildUriFromRoute('record_edit', [
2401 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2402 'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2403 ]);
2404 HttpUtility::redirect($location);
2405 }
2406 // Create redirect response to self to edit just created record
2407 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2408 return new RedirectResponse(
2409 (string)$uriBuilder->buildUriFromRoute(
2410 'record_edit',
2411 [
2412 'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2413 'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2414 ]
2415 ),
2416 303
2417 );
2418 }
2419 }
2420 return null;
2421 }
2422
2423 /**
2424 * Returns sys_language records available for record translations on given page.
2425 *
2426 * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
2427 * hidden. If set to another value, the query will select all sys_language records that has a
2428 * translation record on that page (and is not hidden, unless you are admin user)
2429 * @param string $table For pages we want all languages, for other records the languages of the page translations
2430 * @return array Language records including faked record for default language
2431 */
2432 protected function getLanguages(int $id, string $table = ''): array
2433 {
2434 $languageService = $this->getLanguageService();
2435 $modPageTsConfig = BackendUtility::getPagesTSconfig($id)['mod.']['SHARED.'] ?? [];
2436 // Fallback non sprite-configuration
2437 if (preg_match('/\\.gif$/', $modPageTsConfig['defaultLanguageFlag'] ?? '')) {
2438 $modPageTsConfig['defaultLanguageFlag'] = str_replace(
2439 '.gif',
2440 '',
2441 $modPageTsConfig['defaultLanguageFlag']
2442 );
2443 }
2444 $languages = [
2445 0 => [
2446 'uid' => 0,
2447 'pid' => 0,
2448 'hidden' => 0,
2449 'title' => $modPageTsConfig['defaultLanguageLabel'] !== ''
2450 ? $modPageTsConfig['defaultLanguageLabel'] . ' (' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage') . ')'
2451 : $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage'),
2452 'flag' => $modPageTsConfig['defaultLanguageFlag']
2453 ]
2454 ];
2455
2456 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2457 ->getQueryBuilderForTable('sys_language');
2458
2459 $queryBuilder->select('s.uid', 's.pid', 's.hidden', 's.title', 's.flag')
2460 ->from('sys_language', 's')
2461 ->groupBy('s.uid', 's.pid', 's.hidden', 's.title', 's.flag', 's.sorting')
2462 ->orderBy('s.sorting');
2463
2464 if ($id) {
2465 $queryBuilder->getRestrictions()
2466 ->removeAll()
2467 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2468 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2469
2470 if (!$this->getBackendUser()->isAdmin()) {
2471 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(HiddenRestriction::class));
2472 }
2473
2474 $this->joinPagesTranslationsForActiveLanguage($queryBuilder, $table, $id);
2475 }
2476
2477 $result = $queryBuilder->execute();
2478 while ($row = $result->fetch()) {
2479 $languages[$row['uid']] = $row;
2480 }
2481
2482 return $languages;
2483 }
2484
2485 /**
2486 * @param QueryBuilder $queryBuilder
2487 * @param string $table
2488 * @param int $id
2489 */
2490 public function joinPagesTranslationsForActiveLanguage(QueryBuilder $queryBuilder, string $table, int $id)
2491 {
2492 // Add join with pages translations to only show active languages
2493 if ($table !== 'pages') {
2494 $queryBuilder->from('pages', 'o')
2495 ->where(
2496 $queryBuilder->expr()->eq(
2497 'o.' . $GLOBALS['TCA']['pages']['ctrl']['languageField'],
2498 $queryBuilder->quoteIdentifier('s.uid')
2499 ),
2500 $queryBuilder->expr()->eq(
2501 'o.' . $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2502 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
2503 )
2504 );
2505 }
2506 }
2507
2508 /**
2509 * Fix $this->editconf if versioning applies to any of the records
2510 *
2511 * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
2512 */
2513 protected function fixWSversioningInEditConf($mapArray = false): void
2514 {
2515 // Traverse the editConf array
2516 if (is_array($this->editconf)) {
2517 // Tables:
2518 foreach ($this->editconf as $table => $conf) {
2519 if (is_array($conf) && $GLOBALS['TCA'][$table]) {
2520 // Traverse the keys/comments of each table (keys can be a comma list of uids)
2521 $newConf = [];
2522 foreach ($conf as $cKey => $cmd) {
2523 if ($cmd === 'edit') {
2524 // Traverse the ids:
2525 $ids = GeneralUtility::trimExplode(',', $cKey, true);
2526 foreach ($ids as $idKey => $theUid) {
2527 if (is_array($mapArray)) {
2528 if ($mapArray[$table][$theUid]) {
2529 $ids[$idKey] = $mapArray[$table][$theUid];
2530 }
2531 } else {
2532 // Default, look for versions in workspace for record:
2533 $calcPRec = $this->getRecordForEdit((string)$table, (int)$theUid);
2534 if (is_array($calcPRec)) {
2535 // Setting UID again if it had changed, eg. due to workspace versioning.
2536 $ids[$idKey] = $calcPRec['uid'];
2537 }
2538 }
2539 }
2540 // Add the possibly manipulated IDs to the new-build newConf array:
2541 $newConf[implode(',', $ids)] = $cmd;
2542 } else {
2543 $newConf[$cKey] = $cmd;
2544 }
2545 }
2546 // Store the new conf array:
2547 $this->editconf[$table] = $newConf;
2548 }
2549 }
2550 }
2551 }
2552
2553 /**
2554 * Get record for editing.
2555 *
2556 * @param string $table Table name
2557 * @param int $theUid Record UID
2558 * @return array|false Returns record to edit, false if none
2559 */
2560 protected function getRecordForEdit(string $table, int $theUid)
2561 {
2562 // Fetch requested record:
2563 $reqRecord = BackendUtility::getRecord($table, $theUid, 'uid,pid');
2564 if (is_array($reqRecord)) {
2565 // If workspace is OFFLINE:
2566 if ($this->getBackendUser()->workspace != 0) {
2567 // Check for versioning support of the table:
2568 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
2569 // If the record is already a version of "something" pass it by.
2570 if ($reqRecord['pid'] == -1) {
2571 // (If it turns out not to be a version of the current workspace there will be trouble, but
2572 // that is handled inside DataHandler then and in the interface it would clearly be an error of
2573 // links if the user accesses such a scenario)
2574 return $reqRecord;
2575 }
2576 // The input record was online and an offline version must be found or made:
2577 // Look for version of this workspace:
2578 $versionRec = BackendUtility::getWorkspaceVersionOfRecord(
2579 $this->getBackendUser()->workspace,
2580 $table,
2581 $reqRecord['uid'],
2582 'uid,pid,t3ver_oid'
2583 );
2584 return is_array($versionRec) ? $versionRec : $reqRecord;
2585 }
2586 // This means that editing cannot occur on this record because it was not supporting versioning
2587 // which is required inside an offline workspace.
2588 return false;
2589 }
2590 // In ONLINE workspace, just return the originally requested record:
2591 return $reqRecord;
2592 }
2593 // Return FALSE because the table/uid was not found anyway.
2594 return false;
2595 }
2596
2597 /**
2598 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2599 *
2600 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2601 */
2602 public function compileStoreDat()
2603 {
2604 trigger_error('Method compileStoreDat() will be removed in TYPO3 v10.', E_USER_DEPRECATED);
2605 $this->compileStoreData();
2606 }
2607
2608 /**
2609 * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2610 * to prepare 'open documents' urls
2611 */
2612 protected function compileStoreData(): void
2613 {
2614 // @todo: Refactor in v10: This GeneralUtility method fiddles with _GP()
2615 $this->storeArray = GeneralUtility::compileSelectedGetVarsFromArray(
2616 'edit,defVals,overrideVals,columnsOnly,noView,workspace',
2617 $this->R_URL_getvars
2618 );
2619 $this->storeUrl = GeneralUtility::implodeArrayForUrl('', $this->storeArray);
2620 $this->storeUrlMd5 = md5($this->storeUrl);
2621 }
2622
2623 /**
2624 * Function used to look for configuration of buttons in the form: Fx. disabling buttons or showing them at various
2625 * positions.
2626 *
2627 * @param string $table The table for which the configuration may be specific
2628 * @param string $key The option for look for. Default is checking if the saveDocNew button should be displayed.
2629 * @return string Return value fetched from USER TSconfig
2630 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2631 */
2632 public function getNewIconMode($table, $key = 'saveDocNew')
2633 {
2634 trigger_error('Method getNewIconMode() will be removed in TYPO3 v10.', E_USER_DEPRECATED);
2635 return $this->getTsConfigOption($table, $key);
2636 }
2637
2638 /**
2639 * Get a TSConfig 'option.' array, possibly for a specific table.
2640 *
2641 * @param string $table Table name
2642 * @param string $key Options key
2643 * @return string
2644 */
2645 protected function getTsConfigOption(string $table, string $key): string
2646 {
2647 return \trim((string)(
2648 $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table]
2649 ?? $this->getBackendUser()->getTSConfig()['options.'][$key]
2650 ?? ''
2651 ));
2652 }
2653
2654 /**
2655 * Handling the closing of a document
2656 * The argument $mode can be one of this values:
2657 * - 0/1 will redirect to $this->retUrl [self::DOCUMENT_CLOSE_MODE_DEFAULT || self::DOCUMENT_CLOSE_MODE_REDIRECT]
2658 * - 3 will clear the docHandler (thus closing all documents) [self::DOCUMENT_CLOSE_MODE_CLEAR_ALL]
2659 * - 4 will do no redirect [self::DOCUMENT_CLOSE_MODE_NO_REDIRECT]
2660 * - other values will call setDocument with ->retUrl
2661 *
2662 * @param int $mode the close mode: one of self::DOCUMENT_CLOSE_MODE_*
2663 * @param $request ServerRequestInterface Incoming request
2664 * @return ResponseInterface|null Redirect response if needed
2665 */
2666 public function closeDocument($mode = self::DOCUMENT_CLOSE_MODE_DEFAULT, ServerRequestInterface $request = null): ?ResponseInterface
2667 {
2668 // Foreign class call or missing argument? Method will be protected and $request mandatory in v10, giving core freedom to move stuff around
2669 $deprecatedCaller = false;
2670 if ($request === null) {
2671 // Set method signature in v10 to: "protected function closeDocument($mode, ServerRequestInterface $request): ?ResponseInterface"
2672 trigger_error('@deprecated since v9, this method will be set to protected in v10', E_USER_DEPRECATED);
2673 $request = $GLOBALS['TYPO3_REQUEST'];
2674 $deprecatedCaller = true;
2675 }
2676
2677 $mode = (int)$mode;
2678 // If current document is found in docHandler,
2679 // then unset it, possibly unset it ALL and finally, write it to the session data
2680 if (isset($this->docHandler[$this->storeUrlMd5])) {
2681 // add the closing document to the recent documents
2682 $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
2683 if (!is_array($recentDocs)) {
2684 $recentDocs = [];
2685 }
2686 $closedDoc = $this->docHandler[$this->storeUrlMd5];
2687 $recentDocs = array_merge([$this->storeUrlMd5 => $closedDoc], $recentDocs);
2688 if (count($recentDocs) > 8) {
2689 $recentDocs = array_slice($recentDocs, 0, 8);
2690 }
2691 // remove it from the list of the open documents
2692 unset($this->docHandler[$this->storeUrlMd5]);
2693 if ($mode === self::DOCUMENT_CLOSE_MODE_CLEAR_ALL) {
2694 $recentDocs = array_merge($this->docHandler, $recentDocs);
2695 $this->docHandler = [];
2696 }
2697 $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
2698 $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->docDat[1]]);
2699 BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
2700 }
2701 if ($mode === self::DOCUMENT_CLOSE_MODE_NO_REDIRECT) {
2702 return null;
2703 }
2704 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2705 // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by
2706 // other scripts, like wizard_add, to know which records was created or so...
2707 if ($this->returnEditConf && $this->retUrl != (string)$uriBuilder->buildUriFromRoute('dummy')) {
2708 $this->retUrl .= '&returnEditConf=' . rawurlencode(json_encode($this->editconf));
2709 }
2710 // If mode is NOT set (means 0) OR set to 1, then make a header location redirect to $this->retUrl
2711 if ($mode === self::DOCUMENT_CLOSE_MODE_DEFAULT || $mode === self::DOCUMENT_CLOSE_MODE_REDIRECT) {
2712 if ($deprecatedCaller) {
2713 // @deprecated fall back if method has been called from outside. This if can be removed in v10
2714 HttpUtility::redirect($this->retUrl);
2715 }
2716 return new RedirectResponse($this->retUrl, 303);
2717 }
2718 if ($this->retUrl === '') {
2719 return null;
2720 }
2721 $retUrl = $this->returnUrl;
2722 if (is_array($this->docHandler) && !empty($this->docHandler)) {
2723 if (!empty($setupArr[2])) {
2724 $sParts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
2725 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2726 }
2727 }
2728 if ($deprecatedCaller) {
2729 // @deprecated fall back if method has been called from outside. This if can be removed in v10
2730 HttpUtility::redirect($retUrl);
2731 }
2732 return new RedirectResponse($retUrl, 303);
2733 }
2734
2735 /**
2736 * Redirects to the document pointed to by $currentDocFromHandlerMD5 OR $retUrl,
2737 * depending on some internal calculations.
2738 *
2739 * @param string $currentDocFromHandlerMD5 Pointer to the document in the docHandler array
2740 * @param string $retUrl Alternative/Default retUrl
2741 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
2742 */
2743 public function setDocument($currentDocFromHandlerMD5 = '', $retUrl = '')
2744 {
2745 trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
2746 if ($retUrl === '') {
2747 return;
2748 }
2749 if (is_array($this->docHandler) && !empty($this->docHandler)) {
2750 if (isset($this->docHandler[$currentDocFromHandlerMD5])) {
2751 $setupArr = $this->docHandler[$currentDocFromHandlerMD5];
2752 } else {
2753 $setupArr = reset($this->docHandler);
2754 }
2755 if ($setupArr[2]) {
2756 $sParts = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
2757 $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2758 }
2759 }
2760 HttpUtility::redirect($retUrl);
2761 }
2762
2763 /**
2764 * Emits a signal after a function was executed
2765 *
2766 * @param string $signalName
2767 * @param ServerRequestInterface $request
2768 */
2769 protected function emitFunctionAfterSignal($signalName, ServerRequestInterface $request): void
2770 {
2771 $this->getSignalSlotDispatcher()->dispatch(__CLASS__, $signalName . 'After', [$this, 'request' => $request]);
2772 }
2773
2774 /**
2775 * Get the SignalSlot dispatcher
2776 *
2777 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
2778 */
2779 protected function getSignalSlotDispatcher()
2780 {
2781 if (!isset($this->signalSlotDispatcher)) {
2782 $this->signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
2783 }
2784 return $this->signalSlotDispatcher;
2785 }
2786
2787 /**
2788 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2789 */
2790 protected function getBackendUser()
2791 {
2792 return $GLOBALS['BE_USER'];
2793 }
2794
2795 /**
2796 * Returns LanguageService
2797 *
2798 * @return \TYPO3\CMS\Core\Localization\LanguageService
2799 */
2800 protected function getLanguageService()
2801 {
2802 return $GLOBALS['LANG'];
2803 }
2804 }