EditDocumentController   F
last analyzed

Complexity

Total Complexity 365

Size/Duplication

Total Lines 2511
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 365
eloc 1135
c 2
b 0
f 1
dl 0
loc 2511
rs 1.06

51 Methods

Rating   Name   Duplication   Size   Complexity  
A registerSaveButtonToButtonBar() 0 13 1
A isInconsistentLanguageHandlingAllowed() 0 7 1
A setIsSavedRecord() 0 6 3
A setIsPageInFreeTranslationMode() 0 14 3
A getFreeTranslationMode() 0 12 3
B getButtons() 0 58 8
A getInfobox() 0 16 2
A getQueryBuilderForTranslationMode() 0 27 1
A registerCshButtonToButtonBar() 0 5 1
A getConnectedContentElementTranslationsCount() 0 13 1
A getStandAloneContentElementTranslationsCount() 0 13 1
A registerShortcutButtonToButtonBar() 0 24 4
F languageSwitch() 0 153 27
C registerViewButtonToButtonBar() 0 62 12
A registerHistoryButtonToButtonBar() 0 17 4
A generatePreviewCode() 0 17 3
B preInit() 0 52 9
A __construct() 0 12 1
A getLanguageService() 0 3 1
A registerColumnsOnlyButtonToButtonBar() 0 12 3
B registerDeleteButtonToButtonBar() 0 72 9
C getLanguages() 0 55 12
A compileStoreData() 0 15 3
C registerNewButtonToButtonBar() 0 45 12
A compileForm() 0 20 2
B getShortcutTitle() 0 43 9
B addSlugFieldsToColumnsOnly() 0 15 11
C mainAction() 0 48 13
A registerOpenInNewWindowButtonToButtonBar() 0 22 2
A getBackendUser() 0 3 1
B localizationRedirect() 0 52 6
B getRecordForEdit() 0 36 8
A registerCloseButtonToButtonBar() 0 12 1
B getPreviewPageId() 0 45 11
A getDisableDelete() 0 13 3
A resolveMetaInformation() 0 15 6
A getCloseUrl() 0 4 1
B main() 0 46 9
C getPreviewUrlParameters() 0 45 12
A isRecordCurrentBackendUser() 0 6 2
C closeDocument() 0 50 14
A init() 0 34 3
A parseAdditionalGetParameters() 0 9 3
B fixWSversioningInEditConf() 0 35 11
A getTsConfigOption() 0 6 1
C registerDuplicationButtonToButtonBar() 0 45 12
A getPreviewUrlAnchorSection() 0 6 4
F makeEditForm() 0 156 34
A resolvePreviewRecordId() 0 15 5
C updateInlineView() 0 34 13
F processData() 0 233 43

How to fix   Complexity   

Complex Class

Complex classes like EditDocumentController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EditDocumentController, and based on these observations, apply Extract Interface, too.

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