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