Passed
Push — master ( 315991...0b00f0 )
by
unknown
14:31 queued 10s
created

isRecordCurrentBackendUser()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2569
     */
2570
    protected function getLanguageService()
2571
    {
2572
        return $GLOBALS['LANG'];
2573
    }
2574
}
2575