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