Passed
Push — master ( 05fafa...ed4f38 )
by
unknown
18:56 queued 05:42
created

EditDocumentController::updateInlineView()   C

Complexity

Conditions 13
Paths 14

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
c 0
b 0
f 0
dl 0
loc 34
rs 6.6166
cc 13
nc 14
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Controller;
19
20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use Psr\Http\Message\ResponseInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
use TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent;
24
use TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent;
25
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
26
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
27
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordWorkspaceDeletePlaceholderException;
28
use TYPO3\CMS\Backend\Form\FormDataCompiler;
29
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
30
use TYPO3\CMS\Backend\Form\FormResultCompiler;
31
use TYPO3\CMS\Backend\Form\NodeFactory;
32
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
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\Exception\SiteNotFoundException;
47
use TYPO3\CMS\Core\Http\HtmlResponse;
48
use TYPO3\CMS\Core\Http\RedirectResponse;
49
use TYPO3\CMS\Core\Imaging\Icon;
50
use TYPO3\CMS\Core\Imaging\IconFactory;
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\Routing\UnableToLinkToPageException;
55
use TYPO3\CMS\Core\Site\Entity\NullSite;
56
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
57
use TYPO3\CMS\Core\Site\SiteFinder;
58
use TYPO3\CMS\Core\Type\Bitmask\Permission;
59
use TYPO3\CMS\Core\Utility\GeneralUtility;
60
use TYPO3\CMS\Core\Utility\HttpUtility;
61
use TYPO3\CMS\Core\Utility\MathUtility;
62
use TYPO3\CMS\Core\Utility\PathUtility;
63
use TYPO3\CMS\Core\Versioning\VersionState;
64
65
/**
66
 * Main backend controller almost always used if some database record is edited in the backend.
67
 *
68
 * Main job of this controller is to evaluate and sanitize $request parameters,
69
 * call the DataHandler if records should be created or updated and
70
 * execute FormEngine for record rendering.
71
 */
72
class EditDocumentController
73
{
74
    protected const DOCUMENT_CLOSE_MODE_DEFAULT = 0;
75
    // works like DOCUMENT_CLOSE_MODE_DEFAULT
76
    protected const DOCUMENT_CLOSE_MODE_REDIRECT = 1;
77
    protected const DOCUMENT_CLOSE_MODE_CLEAR_ALL = 3;
78
    protected const DOCUMENT_CLOSE_MODE_NO_REDIRECT = 4;
79
80
    /**
81
     * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
82
     *
83
     * @var array<string,array>
84
     */
85
    protected $editconf = [];
86
87
    /**
88
     * Comma list of field names to edit. If specified, only those fields will be rendered.
89
     * Otherwise all (available) fields in the record are shown according to the TCA type.
90
     *
91
     * @var string|null
92
     */
93
    protected $columnsOnly;
94
95
    /**
96
     * Default values for fields
97
     *
98
     * @var array|null [table][field]
99
     */
100
    protected $defVals;
101
102
    /**
103
     * Array of values to force being set as hidden fields in FormEngine
104
     *
105
     * @var array|null [table][field]
106
     */
107
    protected $overrideVals;
108
109
    /**
110
     * If set, this value will be set in $this->retUrl as "returnUrl", if not,
111
     * $this->retUrl will link to dummy controller
112
     *
113
     * @var string|null
114
     */
115
    protected $returnUrl;
116
117
    /**
118
     * Prepared return URL. Contains the URL that we should return to from FormEngine if
119
     * close button is clicked. Usually passed along as 'returnUrl', but falls back to
120
     * "dummy" controller.
121
     *
122
     * @var string
123
     */
124
    protected $retUrl;
125
126
    /**
127
     * Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
128
     *
129
     * @var int
130
     */
131
    protected $closeDoc;
132
133
    /**
134
     * If true, the processing of incoming data will be performed as if a save-button is pressed.
135
     * Used in the forms as a hidden field which can be set through
136
     * JavaScript if the form is somehow submitted by JavaScript.
137
     *
138
     * @var bool
139
     */
140
    protected $doSave;
141
142
    /**
143
     * Main DataHandler datamap array
144
     *
145
     * @var array
146
     */
147
    protected $data;
148
149
    /**
150
     * Main DataHandler cmdmap array
151
     *
152
     * @var array
153
     */
154
    protected $cmd;
155
156
    /**
157
     * DataHandler 'mirror' input
158
     *
159
     * @var array
160
     */
161
    protected $mirror;
162
163
    /**
164
     * Boolean: If set, then the GET var "&id=" will be added to the
165
     * retUrl string so that the NEW id of something is returned to the script calling the form.
166
     *
167
     * @var bool
168
     */
169
    protected $returnNewPageId = false;
170
171
    /**
172
     * ID for displaying the page in the frontend, "save and view"
173
     *
174
     * @var int
175
     */
176
    protected $popViewId;
177
178
    /**
179
     * Alternative URL for viewing the frontend pages.
180
     *
181
     * @var string
182
     */
183
    protected $viewUrl;
184
185
    /**
186
     * @var string|null
187
     */
188
    protected $previewCode;
189
190
    /**
191
     * Alternative title for the document handler.
192
     *
193
     * @var string
194
     */
195
    protected $recTitle;
196
197
    /**
198
     * If set, then no save & view button is printed
199
     *
200
     * @var bool
201
     */
202
    protected $noView;
203
204
    /**
205
     * @var string
206
     */
207
    protected $perms_clause;
208
209
    /**
210
     * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
211
     *
212
     * @var bool
213
     */
214
    protected $returnEditConf;
215
216
    /**
217
     * parse_url() of current requested URI, contains ['path'] and ['query'] parts.
218
     *
219
     * @var array
220
     */
221
    protected $R_URL_parts;
222
223
    /**
224
     * Contains $request query parameters. This array is the foundation for creating
225
     * the R_URI internal var which becomes the url to which forms are submitted
226
     *
227
     * @var array
228
     */
229
    protected $R_URL_getvars;
230
231
    /**
232
     * Set to the URL of this script including variables which is needed to re-display the form.
233
     *
234
     * @var string
235
     */
236
    protected $R_URI;
237
238
    /**
239
     * @var array
240
     */
241
    protected $pageinfo;
242
243
    /**
244
     * Is loaded with the "title" of the currently "open document"
245
     * used for the open document toolbar
246
     *
247
     * @var string
248
     */
249
    protected $storeTitle = '';
250
251
    /**
252
     * Contains an array with key/value pairs of GET parameters needed to reach the
253
     * current document displayed - used in the 'open documents' toolbar.
254
     *
255
     * @var array
256
     */
257
    protected $storeArray;
258
259
    /**
260
     * $this->storeArray imploded to url
261
     *
262
     * @var string
263
     */
264
    protected $storeUrl;
265
266
    /**
267
     * md5 hash of storeURL, used to identify a single open document in backend user uc
268
     *
269
     * @var string
270
     */
271
    protected $storeUrlMd5;
272
273
    /**
274
     * Backend user session data of this module
275
     *
276
     * @var array
277
     */
278
    protected $docDat;
279
280
    /**
281
     * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
282
     * the various documents on the GET parameter list needed to open it. The values are
283
     * arrays with 0,1,2 keys with information about the document (see compileStoreData()).
284
     * The docHandler variable is stored in the $docDat session data, key "0".
285
     *
286
     * @var array
287
     */
288
    protected $docHandler;
289
290
    /**
291
     * Array of the elements to create edit forms for.
292
     *
293
     * @var array
294
     */
295
    protected $elementsData;
296
297
    /**
298
     * Pointer to the first element in $elementsData
299
     *
300
     * @var array
301
     */
302
    protected $firstEl;
303
304
    /**
305
     * Counter, used to count the number of errors (when users do not have edit permissions)
306
     *
307
     * @var int
308
     */
309
    protected $errorC;
310
311
    /**
312
     * Is set to the pid value of the last shown record - thus indicating which page to
313
     * show when clicking the SAVE/VIEW button
314
     *
315
     * @var int
316
     */
317
    protected $viewId;
318
319
    /**
320
     * @var FormResultCompiler
321
     */
322
    protected $formResultCompiler;
323
324
    /**
325
     * Used internally to disable the storage of the document reference (eg. new records)
326
     *
327
     * @var int
328
     */
329
    protected $dontStoreDocumentRef = 0;
330
331
    /**
332
     * Stores information needed to preview the currently saved record
333
     *
334
     * @var array
335
     */
336
    protected $previewData = [];
337
338
    /**
339
     * ModuleTemplate object
340
     *
341
     * @var ModuleTemplate
342
     */
343
    protected $moduleTemplate;
344
345
    /**
346
     * Check if a record has been saved
347
     *
348
     * @var bool
349
     */
350
    protected $isSavedRecord;
351
352
    /**
353
     * Check if a page in free translation mode
354
     *
355
     * @var bool
356
     */
357
    protected $isPageInFreeTranslationMode = false;
358
359
    protected EventDispatcherInterface $eventDispatcher;
360
    protected IconFactory $iconFactory;
361
    protected PageRenderer $pageRenderer;
362
    protected UriBuilder $uriBuilder;
363
    protected ModuleTemplateFactory $moduleTemplateFactory;
364
365
    public function __construct(
366
        EventDispatcherInterface $eventDispatcher,
367
        IconFactory $iconFactory,
368
        PageRenderer $pageRenderer,
369
        UriBuilder $uriBuilder,
370
        ModuleTemplateFactory $moduleTemplateFactory
371
    ) {
372
        $this->eventDispatcher = $eventDispatcher;
373
        $this->iconFactory = $iconFactory;
374
        $this->pageRenderer = $pageRenderer;
375
        $this->uriBuilder = $uriBuilder;
376
        $this->moduleTemplateFactory = $moduleTemplateFactory;
377
    }
378
379
    /**
380
     * Main dispatcher entry method registered as "record_edit" end point
381
     *
382
     * @param ServerRequestInterface $request the current request
383
     * @return ResponseInterface the response with the content
384
     */
385
    public function mainAction(ServerRequestInterface $request): ResponseInterface
386
    {
387
        $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
388
        $this->moduleTemplate->setUiBlock(true);
389
        $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
390
391
        // Unlock all locked records
392
        BackendUtility::lockRecords();
393
        if ($response = $this->preInit($request)) {
394
            return $response;
395
        }
396
397
        // Process incoming data via DataHandler?
398
        $parsedBody = $request->getParsedBody();
399
        if ($this->doSave
400
            || isset($parsedBody['_savedok'])
401
            || isset($parsedBody['_saveandclosedok'])
402
            || isset($parsedBody['_savedokview'])
403
            || isset($parsedBody['_savedoknew'])
404
            || isset($parsedBody['_duplicatedoc'])
405
        ) {
406
            if ($response = $this->processData($request)) {
407
                return $response;
408
            }
409
        }
410
411
        $this->init($request);
412
        $this->main($request);
413
414
        return new HtmlResponse($this->moduleTemplate->renderContent());
415
    }
416
417
    /**
418
     * First initialization, always called, even before processData() executes DataHandler processing.
419
     *
420
     * @param ServerRequestInterface $request
421
     * @return ResponseInterface Possible redirect response
422
     */
423
    protected function preInit(ServerRequestInterface $request): ?ResponseInterface
424
    {
425
        if ($response = $this->localizationRedirect($request)) {
426
            return $response;
427
        }
428
429
        $parsedBody = $request->getParsedBody();
430
        $queryParams = $request->getQueryParams();
431
432
        $this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
433
        $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
434
        $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
435
        $this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
436
        $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null);
437
        $this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
438
        $this->doSave = (bool)($parsedBody['doSave'] ?? $queryParams['doSave'] ?? false);
439
        $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
440
441
        // Set overrideVals as default values if defVals does not exist.
442
        // @todo: Why?
443
        if (!is_array($this->defVals) && is_array($this->overrideVals)) {
444
            $this->defVals = $this->overrideVals;
445
        }
446
        $this->addSlugFieldsToColumnsOnly($queryParams);
447
448
        // Set final return URL
449
        $this->retUrl = $this->returnUrl ?: (string)$this->uriBuilder->buildUriFromRoute('dummy');
450
451
        // Change $this->editconf if versioning applies to any of the records
452
        $this->fixWSversioningInEditConf();
453
454
        // Prepare R_URL (request url)
455
        $this->R_URL_parts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
456
        $this->R_URL_getvars = $queryParams;
457
        $this->R_URL_getvars['edit'] = $this->editconf;
458
459
        // Prepare 'open documents' url, this is later modified again various times
460
        $this->compileStoreData();
461
        // Backend user session data of this module
462
        $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
463
        $this->docHandler = $this->docDat[0];
464
465
        // Close document if a request for closing the document has been sent
466
        if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
467
            if ($response = $this->closeDocument($this->closeDoc, $request)) {
468
                return $response;
469
            }
470
        }
471
472
        $event = new BeforeFormEnginePageInitializedEvent($this, $request);
473
        $this->eventDispatcher->dispatch($event);
474
        return null;
475
    }
476
477
    /**
478
     * Always add required fields of slug field
479
     *
480
     * @param array $queryParams
481
     */
482
    protected function addSlugFieldsToColumnsOnly(array $queryParams): void
483
    {
484
        $data = $queryParams['edit'] ?? [];
485
        $data = array_keys($data);
486
        $table = reset($data);
487
        if ($this->columnsOnly && $table !== false && isset($GLOBALS['TCA'][$table])) {
488
            $fields = GeneralUtility::trimExplode(',', $this->columnsOnly, true);
489
            foreach ($fields as $field) {
490
                $postModifiers = $GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['postModifiers'] ?? [];
491
                if (isset($GLOBALS['TCA'][$table]['columns'][$field])
492
                    && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'slug'
493
                    && (!is_array($postModifiers) || $postModifiers === [])
494
                ) {
495
                    foreach ($GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['fields'] ?? [] as $fields) {
496
                        $this->columnsOnly .= ',' . (is_array($fields) ? implode(',', $fields) : $fields);
497
                    }
498
                }
499
            }
500
        }
501
    }
502
503
    /**
504
     * Do processing of data, submitting it to DataHandler. May return a RedirectResponse
505
     *
506
     * @param ServerRequestInterface $request
507
     * @return ResponseInterface|null
508
     */
509
    protected function processData(ServerRequestInterface $request): ?ResponseInterface
510
    {
511
        $parsedBody = $request->getParsedBody();
512
        $queryParams = $request->getQueryParams();
513
514
        $beUser = $this->getBackendUser();
515
516
        // Processing related GET / POST vars
517
        $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];
518
        $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];
519
        $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];
520
        $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);
521
522
        // Only options related to $this->data submission are included here
523
        $tce = GeneralUtility::makeInstance(DataHandler::class);
524
525
        $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);
526
527
        // Set internal vars
528
        if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {
529
            $tce->neverHideAtCopy = 1;
530
        }
531
532
        // Set default values fetched previously from GET / POST vars
533
        if (is_array($this->defVals) && $this->defVals !== [] && is_array($tce->defaultValues)) {
534
            $tce->defaultValues = array_merge_recursive($this->defVals, $tce->defaultValues);
535
        }
536
537
        // Load DataHandler with data
538
        $tce->start($this->data, $this->cmd);
539
        if (is_array($this->mirror)) {
540
            $tce->setMirror($this->mirror);
541
        }
542
543
        // Perform the saving operation with DataHandler:
544
        if ($this->doSave === true) {
545
            $tce->process_datamap();
546
            $tce->process_cmdmap();
547
548
            // Update the module menu for the current backend user, as they updated their UI language
549
            $currentUserId = (int)($beUser->user[$beUser->userid_column] ?? 0);
550
            if ($currentUserId
551
                && (string)($this->data['be_users'][$currentUserId]['lang'] ?? '') !== ''
552
                && $this->data['be_users'][$currentUserId]['lang'] !== $beUser->user['lang']
553
            ) {
554
                $newLanguageKey = $this->data['be_users'][$currentUserId]['lang'];
555
                // Update the current backend user language as well
556
                $beUser->user['lang'] = $newLanguageKey;
557
                // Re-create LANG to have the current request updated the translated page as well
558
                $this->getLanguageService()->init($newLanguageKey);
559
                $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
560
                BackendUtility::setUpdateSignal('updateModuleMenu');
561
                BackendUtility::setUpdateSignal('updateTopbar');
562
            }
563
        }
564
        // If pages are being edited, we set an instruction about updating the page tree after this operation.
565
        if ($tce->pagetreeNeedsRefresh
566
            && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
567
        ) {
568
            BackendUtility::setUpdateSignal('updatePageTree');
569
        }
570
        // If there was saved any new items, load them:
571
        if (!empty($tce->substNEWwithIDs_table)) {
572
            // Save the expanded/collapsed states for new inline records, if any
573
            $this->updateInlineView($request->getParsedBody()['uc'] ?? $request->getQueryParams()['uc'] ?? null, $tce);
574
            $newEditConf = [];
575
            foreach ($this->editconf as $tableName => $tableCmds) {
576
                $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
577
                if (!empty($keys)) {
578
                    foreach ($keys as $key) {
579
                        $editId = $tce->substNEWwithIDs[$key];
580
                        // Check if the $editId isn't a child record of an IRRE action
581
                        if (!(is_array($tce->newRelatedIDs[$tableName])
582
                            && in_array($editId, $tce->newRelatedIDs[$tableName]))
583
                        ) {
584
                            // Translate new id to the workspace version
585
                            if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
586
                                $beUser->workspace,
587
                                $tableName,
588
                                $editId,
589
                                'uid'
590
                            )) {
591
                                $editId = $versionRec['uid'];
592
                            }
593
                            $newEditConf[$tableName][$editId] = 'edit';
594
                        }
595
                        // Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
596
                        if ($tableName === 'pages'
597
                            && $this->retUrl !== (string)$this->uriBuilder->buildUriFromRoute('dummy')
598
                            && $this->retUrl !== $this->getCloseUrl()
599
                            && $this->returnNewPageId
600
                        ) {
601
                            $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
602
                        }
603
                    }
604
                } else {
605
                    $newEditConf[$tableName] = $tableCmds;
606
                }
607
            }
608
            // Reset editconf if newEditConf has values
609
            if (!empty($newEditConf)) {
610
                $this->editconf = $newEditConf;
611
            }
612
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
613
            $this->R_URL_getvars['edit'] = $this->editconf;
614
            // Unset default values since we don't need them anymore.
615
            unset($this->R_URL_getvars['defVals']);
616
            // Recompile the store* values since editconf changed
617
            $this->compileStoreData();
618
        }
619
        // See if any records was auto-created as new versions?
620
        if (!empty($tce->autoVersionIdMap)) {
621
            $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
622
        }
623
        // If a document is saved and a new one is created right after.
624
        if (isset($parsedBody['_savedoknew']) && is_array($this->editconf)) {
625
            if ($redirect = $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request)) {
626
                return $redirect;
627
            }
628
            // Find the current table
629
            reset($this->editconf);
630
            $nTable = (string)key($this->editconf);
631
            // Finding the first id, getting the records pid+uid
632
            reset($this->editconf[$nTable]);
633
            $nUid = (int)key($this->editconf[$nTable]);
634
            $recordFields = 'pid,uid';
635
            if (BackendUtility::isTableWorkspaceEnabled($nTable)) {
636
                $recordFields .= ',t3ver_oid';
637
            }
638
            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
639
            // Determine insertion mode: 'top' is self-explaining,
640
            // otherwise new elements are inserted after one using a negative uid
641
            $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
642
            // Setting a blank editconf array for a new record:
643
            $this->editconf = [];
644
            // Determine related page ID for regular live context
645
            if ((int)($nRec['t3ver_oid'] ?? 0) === 0) {
646
                if ($insertRecordOnTop) {
647
                    $relatedPageId = $nRec['pid'];
648
                } else {
649
                    $relatedPageId = -$nRec['uid'];
650
                }
651
            } else {
652
                // Determine related page ID for workspace context
653
                if ($insertRecordOnTop) {
654
                    // Fetch live version of workspace version since the pid value is always -1 in workspaces
655
                    $liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
656
                    $relatedPageId = $liveRecord['pid'];
657
                } else {
658
                    // Use uid of live version of workspace version
659
                    $relatedPageId = -$nRec['t3ver_oid'];
660
                }
661
            }
662
            $this->editconf[$nTable][$relatedPageId] = 'new';
663
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
664
            $this->R_URL_getvars['edit'] = $this->editconf;
665
            // Recompile the store* values since editconf changed...
666
            $this->compileStoreData();
667
        }
668
        // If a document should be duplicated.
669
        if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf)) {
670
            $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request);
671
            // Find current table
672
            reset($this->editconf);
673
            $nTable = (string)key($this->editconf);
674
            // Find the first id, getting the records pid+uid
675
            reset($this->editconf[$nTable]);
676
            $nUid = key($this->editconf[$nTable]);
677
            if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
678
                $nUid = $tce->substNEWwithIDs[$nUid];
679
            }
680
681
            $recordFields = 'pid,uid';
682
            if (BackendUtility::isTableWorkspaceEnabled($nTable)) {
683
                $recordFields .= ',t3ver_oid';
684
            }
685
            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
686
687
            // Setting a blank editconf array for a new record:
688
            $this->editconf = [];
689
690
            if ((int)($nRec['t3ver_oid'] ?? 0) === 0) {
691
                $relatedPageId = -$nRec['uid'];
692
            } else {
693
                $relatedPageId = -$nRec['t3ver_oid'];
694
            }
695
696
            /** @var DataHandler $duplicateTce */
697
            $duplicateTce = GeneralUtility::makeInstance(DataHandler::class);
698
699
            $duplicateCmd = [
700
                $nTable => [
701
                    $nUid => [
702
                        'copy' => $relatedPageId
703
                    ]
704
                ]
705
            ];
706
707
            $duplicateTce->start([], $duplicateCmd);
708
            $duplicateTce->process_cmdmap();
709
710
            $duplicateMappingArray = $duplicateTce->copyMappingArray;
711
            $duplicateUid = $duplicateMappingArray[$nTable][$nUid];
712
713
            if ($nTable === 'pages') {
714
                BackendUtility::setUpdateSignal('updatePageTree');
715
            }
716
717
            $this->editconf[$nTable][$duplicateUid] = 'edit';
718
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
719
            $this->R_URL_getvars['edit'] = $this->editconf;
720
            // Recompile the store* values since editconf changed...
721
            $this->compileStoreData();
722
723
            // Inform the user of the duplication
724
            $flashMessage = GeneralUtility::makeInstance(
725
                FlashMessage::class,
726
                $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
727
                '',
728
                FlashMessage::OK
729
            );
730
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
731
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
732
            $defaultFlashMessageQueue->enqueue($flashMessage);
733
        }
734
        // If a preview is requested
735
        if (isset($parsedBody['_savedokview'])) {
736
            $array_keys = array_keys($this->data);
737
            // Get the first table and id of the data array from DataHandler
738
            $table = reset($array_keys);
739
            $array_keys = array_keys($this->data[$table]);
740
            $id = reset($array_keys);
741
            if (!MathUtility::canBeInterpretedAsInteger($id)) {
742
                $id = $tce->substNEWwithIDs[$id];
743
            }
744
            // Store this information for later use
745
            $this->previewData['table'] = $table;
746
            $this->previewData['id'] = $id;
747
        }
748
        $tce->printLogErrorMessages();
749
750
        if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
751
            || isset($parsedBody['_saveandclosedok'])
752
        ) {
753
            // Redirect if element should be closed after save
754
            return $this->closeDocument((int)abs($this->closeDoc), $request);
755
        }
756
        return null;
757
    }
758
759
    /**
760
     * Initialize the view part of the controller logic.
761
     *
762
     * @param ServerRequestInterface $request
763
     */
764
    protected function init(ServerRequestInterface $request): void
765
    {
766
        $parsedBody = $request->getParsedBody();
767
        $queryParams = $request->getQueryParams();
768
769
        $beUser = $this->getBackendUser();
770
771
        $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);
772
        $this->viewUrl = (string)($parsedBody['viewUrl'] ?? $queryParams['viewUrl'] ?? '');
773
        $this->recTitle = (string)($parsedBody['recTitle'] ?? $queryParams['recTitle'] ?? '');
774
        $this->noView = (bool)($parsedBody['noView'] ?? $queryParams['noView'] ?? false);
775
        $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
776
        // Set other internal variables:
777
        $this->R_URL_getvars['returnUrl'] = $this->retUrl;
778
        $this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
779
780
        $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
781
782
        if (isset($parsedBody['_savedokview']) && $this->popViewId) {
783
            $this->previewCode = $this->generatePreviewCode();
784
        }
785
        // Set context sensitive menu
786
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
787
788
        $event = new AfterFormEnginePageInitializedEvent($this, $request);
789
        $this->eventDispatcher->dispatch($event);
790
    }
791
792
    /**
793
     * Generates markup for immediate action dispatching.
794
     *
795
     * @return string
796
     */
797
    protected function generatePreviewCode(): string
798
    {
799
        $previewPageId = $this->getPreviewPageId();
800
        $anchorSection = $this->getPreviewUrlAnchorSection();
801
        $previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
802
        $previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
803
804
        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...
805
            ->withRootLine($previewPageRootLine)
806
            ->withSection($anchorSection)
807
            ->withAdditionalQueryParameters($previewUrlParameters)
808
            ->buildImmediateActionElement([PreviewUriBuilder::OPTION_SWITCH_FOCUS => null]);
809
    }
810
811
    /**
812
     * Returns the parameters for the preview URL
813
     *
814
     * @param int $previewPageId
815
     * @return string
816
     */
817
    protected function getPreviewUrlParameters(int $previewPageId): string
818
    {
819
        $linkParameters = [];
820
        $table = $this->previewData['table'] ?: $this->firstEl['table'];
821
        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
822
        $previewConfiguration = BackendUtility::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
823
        $recordArray = BackendUtility::getRecord($table, $recordId);
824
825
        // language handling
826
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
827
        if ($languageField && !empty($recordArray[$languageField])) {
828
            $recordId = $this->resolvePreviewRecordId($table, $recordArray, $previewConfiguration);
829
            $language = $recordArray[$languageField];
830
            if ($language > 0) {
831
                $linkParameters['L'] = $language;
832
            }
833
        }
834
835
        // Always use live workspace record uid for the preview
836
        if (BackendUtility::isTableWorkspaceEnabled($table) && ($recordArray['t3ver_oid'] ?? 0) > 0) {
837
            $recordId = $recordArray['t3ver_oid'];
838
        }
839
840
        // map record data to GET parameters
841
        if (isset($previewConfiguration['fieldToParameterMap.'])) {
842
            foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
843
                $value = $recordArray[$field];
844
                if ($field === 'uid') {
845
                    $value = $recordId;
846
                }
847
                $linkParameters[$parameterName] = $value;
848
            }
849
        }
850
851
        // add/override parameters by configuration
852
        if (isset($previewConfiguration['additionalGetParameters.'])) {
853
            $additionalGetParameters = [];
854
            $this->parseAdditionalGetParameters(
855
                $additionalGetParameters,
856
                $previewConfiguration['additionalGetParameters.']
857
            );
858
            $linkParameters = array_replace($linkParameters, $additionalGetParameters);
859
        }
860
861
        return HttpUtility::buildQueryString($linkParameters, '&');
862
    }
863
864
    /**
865
     * @param string $table
866
     * @param array $recordArray
867
     * @param array $previewConfiguration
868
     *
869
     * @return int
870
     */
871
    protected function resolvePreviewRecordId(string $table, array $recordArray, array $previewConfiguration): int
872
    {
873
        $l10nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
874
        if ($l10nPointer
875
            && !empty($recordArray[$l10nPointer])
876
            && (
877
                // not set -> default to true
878
                !isset($previewConfiguration['useDefaultLanguageRecord'])
879
                // or set -> use value
880
                || $previewConfiguration['useDefaultLanguageRecord']
881
            )
882
        ) {
883
            return (int)$recordArray[$l10nPointer];
884
        }
885
        return (int)$recordArray['uid'];
886
    }
887
888
    /**
889
     * Returns the anchor section for the preview url
890
     *
891
     * @return string
892
     */
893
    protected function getPreviewUrlAnchorSection(): string
894
    {
895
        $table = $this->previewData['table'] ?: $this->firstEl['table'];
896
        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
897
898
        return $table === 'tt_content' ? '#c' . (int)$recordId : '';
899
    }
900
901
    /**
902
     * Returns the preview page id
903
     *
904
     * @return int
905
     */
906
    protected function getPreviewPageId(): int
907
    {
908
        $previewPageId = 0;
909
        $table = $this->previewData['table'] ?: $this->firstEl['table'];
910
        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
911
        $pageId = $this->popViewId ?: $this->viewId;
912
913
        if ($table === 'pages') {
914
            $currentPageId = (int)$recordId;
915
        } else {
916
            $currentPageId = MathUtility::convertToPositiveInteger($pageId);
917
        }
918
919
        $previewConfiguration = BackendUtility::getPagesTSconfig($currentPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
920
921
        if (isset($previewConfiguration['previewPageId'])) {
922
            $previewPageId = (int)$previewConfiguration['previewPageId'];
923
        }
924
        // if no preview page was configured
925
        if (!$previewPageId) {
926
            $rootPageData = null;
927
            $rootLine = BackendUtility::BEgetRootLine($currentPageId);
928
            $currentPage = reset($rootLine);
929
            // Allow all doktypes below 200
930
            // This makes custom doktype work as well with opening a frontend page.
931
            if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) {
932
                // try the current page
933
                $previewPageId = $currentPageId;
934
            } else {
935
                // or search for the root page
936
                foreach ($rootLine as $page) {
937
                    if ($page['is_siteroot']) {
938
                        $rootPageData = $page;
939
                        break;
940
                    }
941
                }
942
                $previewPageId = isset($rootPageData)
943
                    ? (int)$rootPageData['uid']
944
                    : $currentPageId;
945
            }
946
        }
947
948
        $this->popViewId = $previewPageId;
949
950
        return $previewPageId;
951
    }
952
953
    /**
954
     * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
955
     *
956
     * This basically removes the trailing dots of sub-array keys in TypoScript.
957
     * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
958
     *
959
     * @param array $parameters Should be an empty array by default
960
     * @param array $typoScript The TypoScript configuration
961
     */
962
    protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
963
    {
964
        foreach ($typoScript as $key => $value) {
965
            if (is_array($value)) {
966
                $key = rtrim($key, '.');
967
                $parameters[$key] = [];
968
                $this->parseAdditionalGetParameters($parameters[$key], $value);
969
            } else {
970
                $parameters[$key] = $value;
971
            }
972
        }
973
    }
974
975
    /**
976
     * Main module operation
977
     *
978
     * @param ServerRequestInterface $request
979
     */
980
    protected function main(ServerRequestInterface $request): void
981
    {
982
        $body = $this->previewCode ?? '';
983
        // Begin edit
984
        if (is_array($this->editconf)) {
0 ignored issues
show
introduced by
The condition is_array($this->editconf) is always true.
Loading history...
985
            $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
986
987
            // Creating the editing form, wrap it with buttons, document selector etc.
988
            $editForm = $this->makeEditForm();
989
            if ($editForm) {
990
                $this->firstEl = reset($this->elementsData);
991
                // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
992
                if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5]))
993
                    && !$this->dontStoreDocumentRef
994
                ) {
995
                    $this->docHandler[$this->storeUrlMd5] = [
996
                        $this->storeTitle,
997
                        $this->storeArray,
998
                        $this->storeUrl,
999
                        $this->firstEl
1000
                    ];
1001
                    $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->storeUrlMd5]);
1002
                    BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
1003
                }
1004
                $body .= $this->formResultCompiler->addCssFiles();
1005
                $body .= $this->compileForm($editForm);
1006
                $body .= $this->formResultCompiler->printNeededJSFunctions();
1007
                $body .= '</form>';
1008
            }
1009
        }
1010
        // Access check...
1011
        // The page will show only if there is a valid page and if this page may be viewed by the user
1012
        $this->pageinfo = BackendUtility::readPageAccess($this->viewId, $this->perms_clause);
0 ignored issues
show
Documentation Bug introduced by
It seems like TYPO3\CMS\Backend\Utilit...d, $this->perms_clause) can also be of type false. However, the property $pageinfo is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1013
        if ($this->pageinfo) {
1014
            $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
1015
        }
1016
        // Setting up the buttons and markers for doc header
1017
        $this->getButtons($request);
1018
1019
        // Create language switch options if the record is already persisted
1020
        if ($this->isSavedRecord) {
1021
            $this->languageSwitch(
1022
                (string)($this->firstEl['table'] ?? ''),
1023
                (int)($this->firstEl['uid'] ?? 0),
1024
                isset($this->firstEl['pid']) ? (int)$this->firstEl['pid'] : null
1025
            );
1026
        }
1027
        $this->moduleTemplate->setContent($body);
1028
    }
1029
1030
    /**
1031
     * Creates the editing form with FormEngine, based on the input from GPvars.
1032
     *
1033
     * @return string HTML form elements wrapped in tables
1034
     */
1035
    protected function makeEditForm(): string
1036
    {
1037
        // Initialize variables
1038
        $this->elementsData = [];
1039
        $this->errorC = 0;
1040
        $editForm = '';
1041
        $beUser = $this->getBackendUser();
1042
        // Traverse the GPvar edit array tables
1043
        foreach ($this->editconf as $table => $conf) {
1044
            if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
1045
                // Traverse the keys/comments of each table (keys can be a comma list of uids)
1046
                foreach ($conf as $cKey => $command) {
1047
                    if ($command === 'edit' || $command === 'new') {
1048
                        // Get the ids:
1049
                        $ids = GeneralUtility::trimExplode(',', $cKey, true);
1050
                        // Traverse the ids:
1051
                        foreach ($ids as $theUid) {
1052
                            // Don't save this document title in the document selector if the document is new.
1053
                            if ($command === 'new') {
1054
                                $this->dontStoreDocumentRef = 1;
1055
                            }
1056
1057
                            try {
1058
                                $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
1059
                                $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
1060
                                $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
1061
1062
                                // Reset viewId - it should hold data of last entry only
1063
                                $this->viewId = 0;
1064
1065
                                $formDataCompilerInput = [
1066
                                    'tableName' => $table,
1067
                                    'vanillaUid' => (int)$theUid,
1068
                                    'command' => $command,
1069
                                    'returnUrl' => $this->R_URI,
1070
                                ];
1071
                                if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
1072
                                    $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
1073
                                }
1074
                                if (!empty($this->defVals) && is_array($this->defVals)) {
1075
                                    $formDataCompilerInput['defaultValues'] = $this->defVals;
1076
                                }
1077
1078
                                $formData = $formDataCompiler->compile($formDataCompilerInput);
1079
1080
                                // Set this->viewId if possible
1081
                                if ($command === 'new'
1082
                                    && $table !== 'pages'
1083
                                    && !empty($formData['parentPageRow']['uid'])
1084
                                ) {
1085
                                    $this->viewId = $formData['parentPageRow']['uid'];
1086
                                } else {
1087
                                    if ($table === 'pages') {
1088
                                        $this->viewId = $formData['databaseRow']['uid'];
1089
                                    } elseif (!empty($formData['parentPageRow']['uid'])) {
1090
                                        $this->viewId = $formData['parentPageRow']['uid'];
1091
                                    }
1092
                                }
1093
1094
                                // Determine if delete button can be shown
1095
                                $deleteAccess = false;
1096
                                if (
1097
                                    $command === 'edit'
1098
                                    || $command === 'new'
1099
                                ) {
1100
                                    $permission = new Permission($formData['userPermissionOnPage']);
1101
                                    if ($formData['tableName'] === 'pages') {
1102
                                        $deleteAccess = $permission->get(Permission::PAGE_DELETE);
1103
                                    } else {
1104
                                        $deleteAccess = $permission->get(Permission::CONTENT_EDIT);
1105
                                    }
1106
                                }
1107
1108
                                // Display "is-locked" message
1109
                                if ($command === 'edit') {
1110
                                    $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
1111
                                    if ($lockInfo) {
1112
                                        $flashMessage = GeneralUtility::makeInstance(
1113
                                            FlashMessage::class,
1114
                                            $lockInfo['msg'],
1115
                                            '',
1116
                                            FlashMessage::WARNING
1117
                                        );
1118
                                        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1119
                                        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1120
                                        $defaultFlashMessageQueue->enqueue($flashMessage);
1121
                                    }
1122
                                }
1123
1124
                                // Record title
1125
                                if (!$this->storeTitle) {
1126
                                    $this->storeTitle = $this->recTitle
1127
                                        ? htmlspecialchars($this->recTitle)
1128
                                        : BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($formData['databaseRow']), true);
1129
                                }
1130
1131
                                $this->elementsData[] = [
1132
                                    'table' => $table,
1133
                                    'uid' => $formData['databaseRow']['uid'],
1134
                                    'pid' => $formData['databaseRow']['pid'],
1135
                                    'cmd' => $command,
1136
                                    'deleteAccess' => $deleteAccess
1137
                                ];
1138
1139
                                if ($command !== 'new') {
1140
                                    BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
1141
                                }
1142
1143
                                // Set list if only specific fields should be rendered. This will trigger
1144
                                // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1145
                                if ($this->columnsOnly) {
1146
                                    if (is_array($this->columnsOnly)) {
1147
                                        $formData['fieldListToRender'] = $this->columnsOnly[$table];
1148
                                    } else {
1149
                                        $formData['fieldListToRender'] = $this->columnsOnly;
1150
                                    }
1151
                                }
1152
1153
                                $formData['renderType'] = 'outerWrapContainer';
1154
                                $formResult = $nodeFactory->create($formData)->render();
1155
1156
                                $html = $formResult['html'];
1157
1158
                                $formResult['html'] = '';
1159
                                $formResult['doSaveFieldName'] = 'doSave';
1160
1161
                                // @todo: Put all the stuff into FormEngine as final "compiler" class
1162
                                // @todo: This is done here for now to not rewrite addCssFiles()
1163
                                // @todo: and printNeededJSFunctions() now
1164
                                $this->formResultCompiler->mergeResult($formResult);
1165
1166
                                // Seems the pid is set as hidden field (again) at end?!
1167
                                if ($command === 'new') {
1168
                                    // @todo: looks ugly
1169
                                    $html .= LF
1170
                                        . '<input type="hidden"'
1171
                                        . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($formData['databaseRow']['uid']) . '][pid]"'
1172
                                        . ' value="' . (int)$formData['databaseRow']['pid'] . '" />';
1173
                                }
1174
1175
                                $editForm .= $html;
1176
                            } catch (AccessDeniedException $e) {
1177
                                $this->errorC++;
1178
                                // Try to fetch error message from "recordInternals" be user object
1179
                                // @todo: This construct should be logged and localized and de-uglified
1180
                                $message = (!empty($beUser->errorMsg)) ? $beUser->errorMsg : $e->getMessage() . ' ' . $e->getCode();
1181
                                $title = $this->getLanguageService()
1182
                                    ->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission');
1183
                                $editForm .= $this->getInfobox($message, $title);
1184
                            } catch (DatabaseRecordException | DatabaseRecordWorkspaceDeletePlaceholderException $e) {
1185
                                $editForm .= $this->getInfobox($e->getMessage());
1186
                            }
1187
                        } // End of for each uid
1188
                    }
1189
                }
1190
            }
1191
        }
1192
        return $editForm;
1193
    }
1194
1195
    /**
1196
     * Helper function for rendering an Infobox
1197
     *
1198
     * @param string $message
1199
     * @param string|null $title
1200
     * @return string
1201
     */
1202
    protected function getInfobox(string $message, ?string $title = null): string
1203
    {
1204
        return '<div class="callout callout-danger">' .
1205
                '<div class="media">' .
1206
                    '<div class="media-left">' .
1207
                        '<span class="fa-stack fa-lg callout-icon">' .
1208
                            '<i class="fa fa-circle fa-stack-2x"></i>' .
1209
                            '<i class="fa fa-times fa-stack-1x"></i>' .
1210
                        '</span>' .
1211
                    '</div>' .
1212
                    '<div class="media-body">' .
1213
                        ($title ? '<h4 class="callout-title">' . htmlspecialchars($title) . '</h4>' : '') .
1214
                        '<div class="callout-body">' . htmlspecialchars($message) . '</div>' .
1215
                    '</div>' .
1216
                '</div>' .
1217
            '</div>';
1218
    }
1219
1220
    /**
1221
     * Create the panel of buttons for submitting the form or otherwise perform operations.
1222
     *
1223
     * @param ServerRequestInterface $request
1224
     */
1225
    protected function getButtons(ServerRequestInterface $request): void
1226
    {
1227
        $record = BackendUtility::getRecord($this->firstEl['table'], $this->firstEl['uid']);
1228
        $TCActrl = $GLOBALS['TCA'][$this->firstEl['table']]['ctrl'];
1229
1230
        $this->setIsSavedRecord();
1231
1232
        $sysLanguageUid = 0;
1233
        if (
1234
            $this->isSavedRecord
1235
            && isset($TCActrl['languageField'], $record[$TCActrl['languageField']])
1236
        ) {
1237
            $sysLanguageUid = (int)$record[$TCActrl['languageField']];
1238
        } elseif (isset($this->defVals['sys_language_uid'])) {
1239
            $sysLanguageUid = (int)$this->defVals['sys_language_uid'];
1240
        }
1241
1242
        $l18nParent = isset($TCActrl['transOrigPointerField'], $record[$TCActrl['transOrigPointerField']])
1243
            ? (int)$record[$TCActrl['transOrigPointerField']]
1244
            : 0;
1245
1246
        $this->setIsPageInFreeTranslationMode($record, $sysLanguageUid);
1247
1248
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1249
1250
        $this->registerCloseButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 1);
1251
1252
        // Show buttons when table is not read-only
1253
        if (
1254
            !$this->errorC
1255
            && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']
1256
        ) {
1257
            $this->registerSaveButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 2);
1258
            $this->registerViewButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 3);
1259
            if ($this->firstEl['cmd'] !== 'new') {
1260
                $this->registerNewButtonToButtonBar(
1261
                    $buttonBar,
1262
                    ButtonBar::BUTTON_POSITION_LEFT,
1263
                    4,
1264
                    $sysLanguageUid,
1265
                    $l18nParent
1266
                );
1267
                $this->registerDuplicationButtonToButtonBar(
1268
                    $buttonBar,
1269
                    ButtonBar::BUTTON_POSITION_LEFT,
1270
                    5,
1271
                    $sysLanguageUid,
1272
                    $l18nParent
1273
                );
1274
            }
1275
            $this->registerDeleteButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 6);
1276
            $this->registerColumnsOnlyButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 7);
1277
            $this->registerHistoryButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1278
        }
1279
1280
        $this->registerOpenInNewWindowButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 2);
1281
        $this->registerShortcutButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 3, $request);
1282
        $this->registerCshButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 4);
1283
    }
1284
1285
    /**
1286
     * Set the boolean to check if the record is saved
1287
     */
1288
    protected function setIsSavedRecord()
1289
    {
1290
        if (!is_bool($this->isSavedRecord)) {
0 ignored issues
show
introduced by
The condition is_bool($this->isSavedRecord) is always true.
Loading history...
1291
            $this->isSavedRecord = (
1292
                $this->firstEl['cmd'] !== 'new'
1293
                && MathUtility::canBeInterpretedAsInteger($this->firstEl['uid'])
1294
            );
1295
        }
1296
    }
1297
1298
    /**
1299
     * Returns if inconsistent language handling is allowed
1300
     *
1301
     * @return bool
1302
     */
1303
    protected function isInconsistentLanguageHandlingAllowed(): bool
1304
    {
1305
        $allowInconsistentLanguageHandling = BackendUtility::getPagesTSconfig(
1306
            $this->pageinfo['uid']
1307
        )['mod']['web_layout']['allowInconsistentLanguageHandling'];
1308
1309
        return $allowInconsistentLanguageHandling['value'] === '1';
1310
    }
1311
1312
    /**
1313
     * Set the boolean to check if the page is in free translation mode
1314
     *
1315
     * @param array|null $record
1316
     * @param int $sysLanguageUid
1317
     */
1318
    protected function setIsPageInFreeTranslationMode($record, int $sysLanguageUid)
1319
    {
1320
        if ($this->firstEl['table'] === 'tt_content') {
1321
            if (!$this->isSavedRecord) {
1322
                $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1323
                    (int)$this->pageinfo['uid'],
1324
                    (int)$this->defVals['colPos'],
1325
                    $sysLanguageUid
1326
                );
1327
            } else {
1328
                $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1329
                    (int)$this->pageinfo['uid'],
1330
                    (int)$record['colPos'],
1331
                    $sysLanguageUid
1332
                );
1333
            }
1334
        }
1335
    }
1336
1337
    /**
1338
     * Check if the page is in free translation mode
1339
     *
1340
     * @param int $page
1341
     * @param int $column
1342
     * @param int $language
1343
     * @return bool
1344
     */
1345
    protected function getFreeTranslationMode(int $page, int $column, int $language): bool
1346
    {
1347
        $freeTranslationMode = false;
1348
1349
        if (
1350
            $this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0
1351
            && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0
1352
        ) {
1353
            $freeTranslationMode = true;
1354
        }
1355
1356
        return $freeTranslationMode;
1357
    }
1358
1359
    /**
1360
     * Register the close button to the button bar
1361
     *
1362
     * @param ButtonBar $buttonBar
1363
     * @param string $position
1364
     * @param int $group
1365
     */
1366
    protected function registerCloseButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1367
    {
1368
        $closeButton = $buttonBar->makeLinkButton()
1369
            ->setHref('#')
1370
            ->setClasses('t3js-editform-close')
1371
            ->setTitle($this->getLanguageService()->sL(
1372
                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'
1373
            ))
1374
            ->setShowLabelText(true)
1375
            ->setIcon($this->iconFactory->getIcon('actions-close', Icon::SIZE_SMALL));
1376
1377
        $buttonBar->addButton($closeButton, $position, $group);
1378
    }
1379
1380
    /**
1381
     * Register the save button to the button bar
1382
     *
1383
     * @param ButtonBar $buttonBar
1384
     * @param string $position
1385
     * @param int $group
1386
     */
1387
    protected function registerSaveButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1388
    {
1389
        $saveButton = $buttonBar->makeInputButton()
1390
            ->setForm('EditDocumentController')
1391
            ->setIcon($this->iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL))
1392
            ->setName('_savedok')
1393
            ->setShowLabelText(true)
1394
            ->setTitle($this->getLanguageService()->sL(
1395
                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'
1396
            ))
1397
            ->setValue('1');
1398
1399
        $buttonBar->addButton($saveButton, $position, $group);
1400
    }
1401
1402
    /**
1403
     * Register the view button to the button bar
1404
     *
1405
     * @param ButtonBar $buttonBar
1406
     * @param string $position
1407
     * @param int $group
1408
     */
1409
    protected function registerViewButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1410
    {
1411
        if (
1412
            $this->viewId // Pid to show the record
1413
            && !$this->noView // Passed parameter
1414
            && !empty($this->firstEl['table']) // No table
1415
1416
            // @TODO: TsConfig option should change to viewDoc
1417
            && $this->getTsConfigOption($this->firstEl['table'], 'saveDocView')
1418
        ) {
1419
            $classNames = 't3js-editform-view';
1420
1421
            $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
1422
1423
            if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1424
                $excludeDokTypes = GeneralUtility::intExplode(
1425
                    ',',
1426
                    $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
1427
                    true
1428
                );
1429
            } else {
1430
                // exclude sysfolders, spacers and recycler by default
1431
                $excludeDokTypes = [
1432
                    PageRepository::DOKTYPE_RECYCLER,
1433
                    PageRepository::DOKTYPE_SYSFOLDER,
1434
                    PageRepository::DOKTYPE_SPACER
1435
                ];
1436
            }
1437
1438
            if (
1439
                !in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)
1440
                || isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl['table'] . '.']['previewPageId'])
1441
            ) {
1442
                $previewPageId = $this->getPreviewPageId();
1443
                try {
1444
                    $previewUrl = BackendUtility::getPreviewUrl(
1445
                        $previewPageId,
1446
                        '',
1447
                        BackendUtility::BEgetRootLine($previewPageId),
1448
                        $this->getPreviewUrlAnchorSection(),
1449
                        $this->viewUrl,
1450
                        $this->getPreviewUrlParameters($previewPageId)
1451
                    );
1452
1453
                    $viewButton = $buttonBar->makeLinkButton()
1454
                        ->setHref($previewUrl)
1455
                        ->setIcon($this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL))
1456
                        ->setShowLabelText(true)
1457
                        ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc'));
1458
1459
                    if (!$this->isSavedRecord) {
1460
                        if ($this->firstEl['table'] === 'pages') {
1461
                            $viewButton->setDataAttributes(['is-new' => '']);
1462
                        }
1463
                    }
1464
1465
                    if ($classNames !== '') {
0 ignored issues
show
introduced by
The condition $classNames !== '' is always true.
Loading history...
1466
                        $viewButton->setClasses($classNames);
1467
                    }
1468
1469
                    $buttonBar->addButton($viewButton, $position, $group);
1470
                } catch (UnableToLinkToPageException $e) {
1471
                    // Do not add any button
1472
                }
1473
            }
1474
        }
1475
    }
1476
1477
    /**
1478
     * Register the new button to the button bar
1479
     *
1480
     * @param ButtonBar $buttonBar
1481
     * @param string $position
1482
     * @param int $group
1483
     * @param int $sysLanguageUid
1484
     * @param int $l18nParent
1485
     */
1486
    protected function registerNewButtonToButtonBar(
1487
        ButtonBar $buttonBar,
1488
        string $position,
1489
        int $group,
1490
        int $sysLanguageUid,
1491
        int $l18nParent
1492
    ) {
1493
        if (
1494
            $this->firstEl['table'] !== 'sys_file_metadata'
1495
            && !empty($this->firstEl['table'])
1496
            && (
1497
                (
1498
                    (
1499
                        $this->isInconsistentLanguageHandlingAllowed()
1500
                        || $this->isPageInFreeTranslationMode
1501
                    )
1502
                    && $this->firstEl['table'] === 'tt_content'
1503
                )
1504
                || (
1505
                    $this->firstEl['table'] !== 'tt_content'
1506
                    && (
1507
                        $sysLanguageUid === 0
1508
                        || $l18nParent === 0
1509
                    )
1510
                )
1511
            )
1512
            && $this->getTsConfigOption($this->firstEl['table'], 'saveDocNew')
1513
        ) {
1514
            $classNames = 't3js-editform-new';
1515
1516
            $newButton = $buttonBar->makeLinkButton()
1517
                ->setHref('#')
1518
                ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL))
1519
                ->setShowLabelText(true)
1520
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc'));
1521
1522
            if (!$this->isSavedRecord) {
1523
                $newButton->setDataAttributes(['is-new' => '']);
1524
            }
1525
1526
            if ($classNames !== '') {
0 ignored issues
show
introduced by
The condition $classNames !== '' is always true.
Loading history...
1527
                $newButton->setClasses($classNames);
1528
            }
1529
1530
            $buttonBar->addButton($newButton, $position, $group);
1531
        }
1532
    }
1533
1534
    /**
1535
     * Register the duplication button to the button bar
1536
     *
1537
     * @param ButtonBar $buttonBar
1538
     * @param string $position
1539
     * @param int $group
1540
     * @param int $sysLanguageUid
1541
     * @param int $l18nParent
1542
     */
1543
    protected function registerDuplicationButtonToButtonBar(
1544
        ButtonBar $buttonBar,
1545
        string $position,
1546
        int $group,
1547
        int $sysLanguageUid,
1548
        int $l18nParent
1549
    ) {
1550
        if (
1551
            $this->firstEl['table'] !== 'sys_file_metadata'
1552
            && !empty($this->firstEl['table'])
1553
            && (
1554
                (
1555
                    (
1556
                        $this->isInconsistentLanguageHandlingAllowed()
1557
                        || $this->isPageInFreeTranslationMode
1558
                    )
1559
                    && $this->firstEl['table'] === 'tt_content'
1560
                )
1561
                || (
1562
                    $this->firstEl['table'] !== 'tt_content'
1563
                    && (
1564
                        $sysLanguageUid === 0
1565
                        || $l18nParent === 0
1566
                    )
1567
                )
1568
            )
1569
            && $this->getTsConfigOption($this->firstEl['table'], 'showDuplicate')
1570
        ) {
1571
            $classNames = 't3js-editform-duplicate';
1572
1573
            $duplicateButton = $buttonBar->makeLinkButton()
1574
                ->setHref('#')
1575
                ->setShowLabelText(true)
1576
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc'))
1577
                ->setIcon($this->iconFactory->getIcon('actions-document-duplicates-select', Icon::SIZE_SMALL));
1578
1579
            if (!$this->isSavedRecord) {
1580
                $duplicateButton->setDataAttributes(['is-new' => '']);
1581
            }
1582
1583
            if ($classNames !== '') {
0 ignored issues
show
introduced by
The condition $classNames !== '' is always true.
Loading history...
1584
                $duplicateButton->setClasses($classNames);
1585
            }
1586
1587
            $buttonBar->addButton($duplicateButton, $position, $group);
1588
        }
1589
    }
1590
1591
    /**
1592
     * Register the delete button to the button bar
1593
     *
1594
     * @param ButtonBar $buttonBar
1595
     * @param string $position
1596
     * @param int $group
1597
     */
1598
    protected function registerDeleteButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1599
    {
1600
        if (
1601
            $this->firstEl['deleteAccess']
1602
            && !$this->getDisableDelete()
1603
            && $this->isSavedRecord
1604
            && count($this->elementsData) === 1
1605
        ) {
1606
            $classNames = 't3js-editform-delete-record';
1607
            $returnUrl = $this->retUrl;
1608
            if ($this->firstEl['table'] === 'pages') {
1609
                parse_str((string)parse_url($returnUrl, PHP_URL_QUERY), $queryParams);
1610
                if (
1611
                    isset($queryParams['route'], $queryParams['id'])
1612
                    && (string)$this->firstEl['uid'] === (string)$queryParams['id']
1613
                ) {
1614
                    // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
1615
                    // tree from the outside to be able to mark the pid as active
1616
                    $returnUrl = (string)$this->uriBuilder->buildUriFromRoutePath($queryParams['route'], ['id' => 0]);
1617
                }
1618
            }
1619
1620
            /** @var ReferenceIndex $referenceIndex */
1621
            $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1622
            $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords(
1623
                $this->firstEl['table'],
1624
                (int)$this->firstEl['uid']
1625
            );
1626
1627
            $referenceCountMessage = BackendUtility::referenceCount(
1628
                $this->firstEl['table'],
1629
                (string)(int)$this->firstEl['uid'],
1630
                $this->getLanguageService()->sL(
1631
                    'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'
1632
                ),
1633
                (string)$numberOfReferences
1634
            );
1635
            $translationCountMessage = BackendUtility::translationCount(
1636
                $this->firstEl['table'],
1637
                (string)(int)$this->firstEl['uid'],
1638
                $this->getLanguageService()->sL(
1639
                    'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'
1640
                )
1641
            );
1642
1643
            $deleteUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [
1644
                'cmd' => [
1645
                    $this->firstEl['table'] => [
1646
                        $this->firstEl['uid'] => [
1647
                            'delete' => '1'
1648
                        ]
1649
                    ]
1650
                ],
1651
                'redirect' => $this->retUrl
1652
            ]);
1653
1654
            $deleteButton = $buttonBar->makeLinkButton()
1655
                ->setClasses($classNames)
1656
                ->setDataAttributes([
1657
                    'return-url' => $returnUrl,
1658
                    'uid' => $this->firstEl['uid'],
1659
                    'table' => $this->firstEl['table'],
1660
                    'reference-count-message' => $referenceCountMessage,
1661
                    'translation-count-message' => $translationCountMessage
1662
                ])
1663
                ->setHref($deleteUrl)
1664
                ->setIcon($this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL))
1665
                ->setShowLabelText(true)
1666
                ->setTitle($this->getLanguageService()->getLL('deleteItem'));
1667
1668
            $buttonBar->addButton($deleteButton, $position, $group);
1669
        }
1670
    }
1671
1672
    /**
1673
     * Register the history button to the button bar
1674
     *
1675
     * @param ButtonBar $buttonBar
1676
     * @param string $position
1677
     * @param int $group
1678
     */
1679
    protected function registerHistoryButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1680
    {
1681
        if (
1682
            count($this->elementsData) === 1
1683
            && !empty($this->firstEl['table'])
1684
            && $this->getTsConfigOption($this->firstEl['table'], 'showHistory')
1685
        ) {
1686
            $historyUrl = (string)$this->uriBuilder->buildUriFromRoute('record_history', [
1687
                'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1688
                'returnUrl' => $this->R_URI,
1689
            ]);
1690
            $historyButton = $buttonBar->makeLinkButton()
1691
                ->setHref($historyUrl)
1692
                ->setTitle('Open history of this record')
1693
                ->setIcon($this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL));
1694
1695
            $buttonBar->addButton($historyButton, $position, $group);
1696
        }
1697
    }
1698
1699
    /**
1700
     * Register the columns only button to the button bar
1701
     *
1702
     * @param ButtonBar $buttonBar
1703
     * @param string $position
1704
     * @param int $group
1705
     */
1706
    protected function registerColumnsOnlyButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1707
    {
1708
        if (
1709
            $this->columnsOnly
1710
            && count($this->elementsData) === 1
1711
        ) {
1712
            $columnsOnlyButton = $buttonBar->makeLinkButton()
1713
                ->setHref($this->R_URI . '&columnsOnly=')
1714
                ->setTitle($this->getLanguageService()->getLL('editWholeRecord'))
1715
                ->setIcon($this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL));
1716
1717
            $buttonBar->addButton($columnsOnlyButton, $position, $group);
1718
        }
1719
    }
1720
1721
    /**
1722
     * Register the open in new window button to the button bar
1723
     *
1724
     * @param ButtonBar $buttonBar
1725
     * @param string $position
1726
     * @param int $group
1727
     */
1728
    protected function registerOpenInNewWindowButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1729
    {
1730
        $closeUrl = $this->getCloseUrl();
1731
        if ($this->returnUrl !== $closeUrl) {
1732
            $requestUri = GeneralUtility::linkThisScript([
1733
                'returnUrl' => $closeUrl,
1734
            ]);
1735
            $aOnClick = 'vHWin=window.open('
1736
                . GeneralUtility::quoteJSvalue($requestUri) . ','
1737
                . GeneralUtility::quoteJSvalue(md5($this->R_URI))
1738
                . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1739
1740
            $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
1741
                ->makeLinkButton()
1742
                ->setHref('#')
1743
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
1744
                ->setIcon($this->iconFactory->getIcon('actions-window-open', Icon::SIZE_SMALL))
1745
                ->setOnClick($aOnClick);
1746
1747
            $buttonBar->addButton($openInNewWindowButton, $position, $group);
1748
        }
1749
    }
1750
1751
    /**
1752
     * Register the shortcut button to the button bar
1753
     *
1754
     * @param ButtonBar $buttonBar
1755
     * @param string $position
1756
     * @param int $group
1757
     * @param ServerRequestInterface $request
1758
     */
1759
    protected function registerShortcutButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group, ServerRequestInterface $request)
1760
    {
1761
        if ($this->returnUrl !== $this->getCloseUrl()) {
1762
            $queryParams = $request->getQueryParams();
1763
            $potentialArguments = [
1764
                'edit',
1765
                'defVals',
1766
                'overrideVals',
1767
                'columnsOnly',
1768
                'returnNewPageId',
1769
                'noView'
1770
            ];
1771
            $arguments = [];
1772
            foreach ($potentialArguments as $argument) {
1773
                if (!empty($queryParams[$argument])) {
1774
                    $arguments[$argument] = $queryParams[$argument];
1775
                }
1776
            }
1777
            $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
1778
            $shortCutButton
1779
                ->setRouteIdentifier('record_edit')
1780
                ->setDisplayName($this->getShortcutTitle($request))
1781
                ->setArguments($arguments);
1782
            $buttonBar->addButton($shortCutButton, $position, $group);
1783
        }
1784
    }
1785
1786
    /**
1787
     * Register the CSH button to the button bar
1788
     *
1789
     * @param ButtonBar $buttonBar
1790
     * @param string $position
1791
     * @param int $group
1792
     */
1793
    protected function registerCshButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1794
    {
1795
        $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('TCEforms');
1796
1797
        $buttonBar->addButton($cshButton, $position, $group);
1798
    }
1799
1800
    /**
1801
     * Get the count of connected translated content elements
1802
     *
1803
     * @param int $page
1804
     * @param int $column
1805
     * @param int $language
1806
     * @return int
1807
     */
1808
    protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int
1809
    {
1810
        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1811
1812
        return (int)$queryBuilder
1813
            ->andWhere(
1814
                $queryBuilder->expr()->gt(
1815
                    $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1816
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1817
                )
1818
            )
1819
            ->execute()
1820
            ->fetchColumn(0);
1821
    }
1822
1823
    /**
1824
     * Get the count of standalone translated content elements
1825
     *
1826
     * @param int $page
1827
     * @param int $column
1828
     * @param int $language
1829
     * @return int
1830
     */
1831
    protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int
1832
    {
1833
        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1834
1835
        return (int)$queryBuilder
1836
            ->andWhere(
1837
                $queryBuilder->expr()->eq(
1838
                    $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1839
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1840
                )
1841
            )
1842
            ->execute()
1843
            ->fetchColumn(0);
1844
    }
1845
1846
    /**
1847
     * Get the query builder for the translation mode
1848
     *
1849
     * @param int $page
1850
     * @param int $column
1851
     * @param int $language
1852
     * @return QueryBuilder
1853
     */
1854
    protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder
1855
    {
1856
        $languageField = $GLOBALS['TCA']['tt_content']['ctrl']['languageField'];
1857
1858
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1859
            ->getQueryBuilderForTable('tt_content');
1860
1861
        $queryBuilder->getRestrictions()
1862
            ->removeAll()
1863
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1864
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
1865
1866
        return $queryBuilder
1867
            ->count('uid')
1868
            ->from('tt_content')
1869
            ->where(
1870
                $queryBuilder->expr()->eq(
1871
                    'pid',
1872
                    $queryBuilder->createNamedParameter($page, \PDO::PARAM_INT)
1873
                ),
1874
                $queryBuilder->expr()->eq(
1875
                    $languageField,
1876
                    $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
1877
                ),
1878
                $queryBuilder->expr()->eq(
1879
                    'colPos',
1880
                    $queryBuilder->createNamedParameter($column, \PDO::PARAM_INT)
1881
                )
1882
            );
1883
    }
1884
1885
    /**
1886
     * Put together the various elements (buttons, selectors, form) into a table
1887
     *
1888
     * @param string $editForm HTML form.
1889
     * @return string Composite HTML
1890
     */
1891
    protected function compileForm(string $editForm): string
1892
    {
1893
        $formContent = '
1894
            <form
1895
                action="' . htmlspecialchars($this->R_URI) . '"
1896
                method="post"
1897
                enctype="multipart/form-data"
1898
                name="editform"
1899
                id="EditDocumentController"
1900
            >
1901
            ' . $editForm . '
1902
            <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
1903
            <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />
1904
            <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$this->viewId) . '" />
1905
            <input type="hidden" name="closeDoc" value="0" />
1906
            <input type="hidden" name="doSave" value="0" />';
1907
        if ($this->returnNewPageId) {
1908
            $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
1909
        }
1910
        return $formContent;
1911
    }
1912
1913
    /**
1914
     * Update expanded/collapsed states on new inline records if any within backendUser->uc.
1915
     *
1916
     * @param array|null $uc The uc array to be processed and saved - uc[inlineView][...]
1917
     * @param DataHandler $dataHandler Instance of DataHandler that saved data before
1918
     */
1919
    protected function updateInlineView($uc, DataHandler $dataHandler)
1920
    {
1921
        $backendUser = $this->getBackendUser();
1922
        if (!isset($uc['inlineView']) || !is_array($uc['inlineView'])) {
1923
            return;
1924
        }
1925
        $inlineView = (array)json_decode(is_string($backendUser->uc['inlineView']) ? $backendUser->uc['inlineView'] : '', true);
1926
        foreach ($uc['inlineView'] as $topTable => $topRecords) {
1927
            foreach ($topRecords as $topUid => $childElements) {
1928
                foreach ($childElements as $childTable => $childRecords) {
1929
                    $uids = array_keys($dataHandler->substNEWwithIDs_table, $childTable);
1930
                    if (!empty($uids)) {
1931
                        $newExpandedChildren = [];
1932
                        foreach ($childRecords as $childUid => $state) {
1933
                            if ($state && in_array($childUid, $uids)) {
1934
                                $newChildUid = $dataHandler->substNEWwithIDs[$childUid];
1935
                                $newExpandedChildren[] = $newChildUid;
1936
                            }
1937
                        }
1938
                        // Add new expanded child records to UC (if any):
1939
                        if (!empty($newExpandedChildren)) {
1940
                            $inlineViewCurrent = &$inlineView[$topTable][$topUid][$childTable];
1941
                            if (is_array($inlineViewCurrent)) {
1942
                                $inlineViewCurrent = array_unique(array_merge($inlineViewCurrent, $newExpandedChildren));
1943
                            } else {
1944
                                $inlineViewCurrent = $newExpandedChildren;
1945
                            }
1946
                        }
1947
                    }
1948
                }
1949
            }
1950
        }
1951
        $backendUser->uc['inlineView'] = json_encode($inlineView);
1952
        $backendUser->writeUC();
1953
    }
1954
    /**
1955
     * Returns if delete for the current table is disabled by configuration.
1956
     * For sys_file_metadata in default language delete is always disabled.
1957
     *
1958
     * @return bool
1959
     */
1960
    protected function getDisableDelete(): bool
1961
    {
1962
        $disableDelete = false;
1963
        if ($this->firstEl['table'] === 'sys_file_metadata') {
1964
            $row = BackendUtility::getRecord('sys_file_metadata', $this->firstEl['uid'], 'sys_language_uid');
1965
            $languageUid = $row['sys_language_uid'];
1966
            if ($languageUid === 0) {
1967
                $disableDelete = true;
1968
            }
1969
        } else {
1970
            $disableDelete = (bool)$this->getTsConfigOption($this->firstEl['table'] ?? '', 'disableDelete');
1971
        }
1972
        return $disableDelete;
1973
    }
1974
1975
    /**
1976
     * Returns the URL (usually for the "returnUrl") which closes the current window.
1977
     * Used when editing a record in a popup.
1978
     *
1979
     * @return string
1980
     */
1981
    protected function getCloseUrl(): string
1982
    {
1983
        $closeUrl = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
1984
        return PathUtility::getAbsoluteWebPath($closeUrl);
1985
    }
1986
1987
    /***************************
1988
     *
1989
     * Localization stuff
1990
     *
1991
     ***************************/
1992
    /**
1993
     * Make selector box for creating new translation for a record or switching to edit the record
1994
     * in an existing language. Displays only languages which are available for the current page.
1995
     *
1996
     * @param string $table Table name
1997
     * @param int $uid Uid for which to create a new language
1998
     * @param int|null $pid Pid of the record
1999
     */
2000
    protected function languageSwitch(string $table, int $uid, $pid = null)
2001
    {
2002
        $backendUser = $this->getBackendUser();
2003
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
2004
        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
2005
        // Table editable and activated for languages?
2006
        if ($backendUser->check('tables_modify', $table)
2007
            && $languageField
2008
            && $transOrigPointerField
2009
        ) {
2010
            if ($pid === null) {
2011
                $row = BackendUtility::getRecord($table, $uid, 'pid');
2012
                $pid = $row['pid'];
2013
            }
2014
            // Get all available languages for the page
2015
            // If editing a page, the translations of the current UID need to be fetched
2016
            if ($table === 'pages') {
2017
                $row = BackendUtility::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
2018
                // Ensure the check is always done against the default language page
2019
                $availableLanguages = $this->getLanguages(
2020
                    (int)$row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $uid,
2021
                    $table
2022
                );
2023
            } else {
2024
                $availableLanguages = $this->getLanguages((int)$pid, $table);
2025
            }
2026
            // Page available in other languages than default language?
2027
            if (count($availableLanguages) > 1) {
2028
                $rowsByLang = [];
2029
                $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
2030
                // Get record in current language
2031
                $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
2032
                if (!is_array($rowCurrent)) {
2033
                    $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
2034
                }
2035
                $currentLanguage = (int)$rowCurrent[$languageField];
2036
                // Disabled for records with [all] language!
2037
                if ($currentLanguage > -1) {
2038
                    // Get record in default language if needed
2039
                    if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
2040
                        $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
2041
                            $table,
2042
                            $rowCurrent[$transOrigPointerField],
2043
                            $fetchFields
2044
                        );
2045
                        if (!is_array($rowsByLang[0])) {
2046
                            $rowsByLang[0] = BackendUtility::getRecord(
2047
                                $table,
2048
                                $rowCurrent[$transOrigPointerField],
2049
                                $fetchFields
2050
                            );
2051
                        }
2052
                    } else {
2053
                        $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
2054
                    }
2055
                    // List of language id's that should not be added to the selector
2056
                    $noAddOption = [];
2057
                    if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
2058
                        // Get record in other languages to see what's already available
2059
2060
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2061
                            ->getQueryBuilderForTable($table);
2062
2063
                        $queryBuilder->getRestrictions()
2064
                            ->removeAll()
2065
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2066
                            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace));
2067
2068
                        $result = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fetchFields, true))
2069
                            ->from($table)
2070
                            ->where(
2071
                                $queryBuilder->expr()->eq(
2072
                                    'pid',
2073
                                    $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
2074
                                ),
2075
                                $queryBuilder->expr()->gt(
2076
                                    $languageField,
2077
                                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2078
                                ),
2079
                                $queryBuilder->expr()->eq(
2080
                                    $transOrigPointerField,
2081
                                    $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO::PARAM_INT)
2082
                                )
2083
                            )
2084
                            ->execute();
2085
2086
                        while ($row = $result->fetch()) {
2087
                            if ($backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
2088
                                $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $table, $row['uid'], 'uid,t3ver_state');
2089
                                if (!empty($workspaceVersion)) {
2090
                                    $versionState = VersionState::cast($workspaceVersion['t3ver_state']);
2091
                                    if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
2092
                                        // If a workspace delete placeholder exists for this translation: Mark
2093
                                        // this language as "don't add to selector" and continue with next row,
2094
                                        // otherwise an edit link to a delete placeholder would be created, which
2095
                                        // does not make sense.
2096
                                        $noAddOption[] = (int)$row[$languageField];
2097
                                        continue;
2098
                                    }
2099
                                }
2100
                            }
2101
                            $rowsByLang[$row[$languageField]] = $row;
2102
                        }
2103
                    }
2104
                    $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
2105
                    $languageMenu->setIdentifier('_langSelector');
2106
                    foreach ($availableLanguages as $language) {
2107
                        $languageId = $language->getLanguageId();
2108
                        $selectorOptionLabel = $language->getTitle();
2109
                        // Create url for creating a localized record
2110
                        $addOption = true;
2111
                        $href = '';
2112
                        if (!isset($rowsByLang[$languageId])) {
2113
                            // Translation in this language does not exist
2114
                            $selectorOptionLabel .= ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
2115
                            $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
2116
                                'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $languageId,
2117
                                'returnUrl' => $this->retUrl
2118
                            ]);
2119
2120
                            if (array_key_exists(0, $rowsByLang)) {
2121
                                $href = BackendUtility::getLinkToDataHandlerAction(
2122
                                    '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $languageId,
2123
                                    $redirectUrl
2124
                                );
2125
                            } else {
2126
                                $addOption = false;
2127
                            }
2128
                        } else {
2129
                            $params = [
2130
                                'edit[' . $table . '][' . $rowsByLang[$languageId]['uid'] . ']' => 'edit',
2131
                                'returnUrl' => $this->retUrl
2132
                            ];
2133
                            if ($table === 'pages') {
2134
                                // Disallow manual adjustment of the language field for pages
2135
                                $params['overrideVals'] = [
2136
                                    'pages' => [
2137
                                        'sys_language_uid' => $languageId
2138
                                    ]
2139
                                ];
2140
                            }
2141
                            $href = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params);
2142
                        }
2143
                        if ($addOption && !in_array($languageId, $noAddOption, true)) {
2144
                            $menuItem = $languageMenu->makeMenuItem()
2145
                                ->setTitle($selectorOptionLabel)
2146
                                ->setHref($href);
2147
                            if ($languageId === $currentLanguage) {
2148
                                $menuItem->setActive(true);
2149
                            }
2150
                            $languageMenu->addMenuItem($menuItem);
2151
                        }
2152
                    }
2153
                    $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
2154
                }
2155
            }
2156
        }
2157
    }
2158
2159
    /**
2160
     * Redirects to FormEngine with new parameters to edit a just created localized record
2161
     *
2162
     * @param ServerRequestInterface $request Incoming request object
2163
     * @return ResponseInterface|null Possible redirect response
2164
     */
2165
    protected function localizationRedirect(ServerRequestInterface $request): ?ResponseInterface
2166
    {
2167
        $justLocalized = $request->getQueryParams()['justLocalized'];
2168
2169
        if (empty($justLocalized)) {
2170
            return null;
2171
        }
2172
2173
        [$table, $origUid, $language] = explode(':', $justLocalized);
2174
2175
        if ($GLOBALS['TCA'][$table]
2176
            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
2177
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
2178
        ) {
2179
            $parsedBody = $request->getParsedBody();
2180
            $queryParams = $request->getQueryParams();
2181
2182
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2183
            $queryBuilder->getRestrictions()
2184
                ->removeAll()
2185
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2186
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
2187
            $localizedRecord = $queryBuilder->select('uid')
2188
                ->from($table)
2189
                ->where(
2190
                    $queryBuilder->expr()->eq(
2191
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
2192
                        $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
2193
                    ),
2194
                    $queryBuilder->expr()->eq(
2195
                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2196
                        $queryBuilder->createNamedParameter($origUid, \PDO::PARAM_INT)
2197
                    )
2198
                )
2199
                ->execute()
2200
                ->fetch();
2201
            $returnUrl = $parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '';
2202
            if (is_array($localizedRecord)) {
2203
                // Create redirect response to self to edit just created record
2204
                return new RedirectResponse(
2205
                    (string)$this->uriBuilder->buildUriFromRoute(
2206
                        'record_edit',
2207
                        [
2208
                            'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2209
                            'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2210
                        ]
2211
                    ),
2212
                    303
2213
                );
2214
            }
2215
        }
2216
        return null;
2217
    }
2218
2219
    /**
2220
     * Returns languages  available for record translations on given page.
2221
     *
2222
     * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
2223
     *                hidden. If set to another value, the query will select all sys_language records that has a
2224
     *                translation record on that page (and is not hidden, unless you are admin user)
2225
     * @param string $table For pages we want all languages, for other records the languages of the page translations
2226
     * @return SiteLanguage[] Language
2227
     */
2228
    protected function getLanguages(int $id, string $table): array
2229
    {
2230
        // This usually happens when a non-pages record is added after another, so we are fetching the proper page ID
2231
        if ($id < 0 && $table !== 'pages') {
2232
            $pageId = $this->pageinfo['uid'] ?? null;
2233
            if ($pageId !== null) {
2234
                $pageId = (int)$pageId;
2235
            } else {
2236
                $fullRecord = BackendUtility::getRecord($table, abs($id));
0 ignored issues
show
Bug introduced by
It seems like abs($id) can also be of type double; however, parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getRecord() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2236
                $fullRecord = BackendUtility::getRecord($table, /** @scrutinizer ignore-type */ abs($id));
Loading history...
2237
                $pageId = (int)$fullRecord['pid'];
2238
            }
2239
        } else {
2240
            if ($table === 'pages' && $id > 0) {
2241
                $fullRecord = BackendUtility::getRecordWSOL('pages', $id);
2242
                $id = (int)($fullRecord['t3ver_oid'] ?: $fullRecord['uid']);
2243
            }
2244
            $pageId = $id;
2245
        }
2246
        try {
2247
            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId);
2248
        } catch (SiteNotFoundException $e) {
2249
            $site = new NullSite();
2250
        }
2251
2252
        // Fetch the current translations of this page, to only show the ones where there is a page translation
2253
        $allLanguages = $site->getAvailableLanguages($this->getBackendUser(), false, $pageId);
2254
        if ($table !== 'pages' && $id > 0) {
2255
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
2256
            $queryBuilder->getRestrictions()->removeAll()
2257
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2258
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
2259
            $statement = $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
2260
                ->from('pages')
2261
                ->where(
2262
                    $queryBuilder->expr()->eq(
2263
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2264
                        $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
2265
                    )
2266
                )
2267
                ->execute();
2268
2269
            $availableLanguages = [];
2270
2271
            if ($allLanguages[0] ?? false) {
2272
                $availableLanguages = [
2273
                    0 => $allLanguages[0]
2274
                ];
2275
            }
2276
2277
            while ($row = $statement->fetch()) {
2278
                $languageId = (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
2279
                if (isset($allLanguages[$languageId])) {
2280
                    $availableLanguages[$languageId] = $allLanguages[$languageId];
2281
                }
2282
            }
2283
            return $availableLanguages;
2284
        }
2285
        return $allLanguages;
2286
    }
2287
2288
    /**
2289
     * Fix $this->editconf if versioning applies to any of the records
2290
     *
2291
     * @param array|bool $mapArray Mapping between old and new ids if auto-versioning has been performed.
2292
     */
2293
    protected function fixWSversioningInEditConf($mapArray = false): void
2294
    {
2295
        // Traverse the editConf array
2296
        if (is_array($this->editconf)) {
0 ignored issues
show
introduced by
The condition is_array($this->editconf) is always true.
Loading history...
2297
            // Tables:
2298
            foreach ($this->editconf as $table => $conf) {
2299
                if (is_array($conf) && $GLOBALS['TCA'][$table]) {
2300
                    // Traverse the keys/comments of each table (keys can be a comma list of uids)
2301
                    $newConf = [];
2302
                    foreach ($conf as $cKey => $cmd) {
2303
                        if ($cmd === 'edit') {
2304
                            // Traverse the ids:
2305
                            $ids = GeneralUtility::trimExplode(',', $cKey, true);
2306
                            foreach ($ids as $idKey => $theUid) {
2307
                                if (is_array($mapArray)) {
2308
                                    if ($mapArray[$table][$theUid]) {
2309
                                        $ids[$idKey] = $mapArray[$table][$theUid];
2310
                                    }
2311
                                } else {
2312
                                    // Default, look for versions in workspace for record:
2313
                                    $calcPRec = $this->getRecordForEdit((string)$table, (int)$theUid);
2314
                                    if (is_array($calcPRec)) {
2315
                                        // Setting UID again if it had changed, eg. due to workspace versioning.
2316
                                        $ids[$idKey] = $calcPRec['uid'];
2317
                                    }
2318
                                }
2319
                            }
2320
                            // Add the possibly manipulated IDs to the new-build newConf array:
2321
                            $newConf[implode(',', $ids)] = $cmd;
2322
                        } else {
2323
                            $newConf[$cKey] = $cmd;
2324
                        }
2325
                    }
2326
                    // Store the new conf array:
2327
                    $this->editconf[$table] = $newConf;
2328
                }
2329
            }
2330
        }
2331
    }
2332
2333
    /**
2334
     * Get record for editing.
2335
     *
2336
     * @param string $table Table name
2337
     * @param int $theUid Record UID
2338
     * @return array|false Returns record to edit, false if none
2339
     */
2340
    protected function getRecordForEdit(string $table, int $theUid)
2341
    {
2342
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
2343
        // Fetch requested record:
2344
        $reqRecord = BackendUtility::getRecord($table, $theUid, 'uid,pid' . ($tableSupportsVersioning ? ',t3ver_oid' : ''));
2345
        if (is_array($reqRecord)) {
2346
            // If workspace is OFFLINE:
2347
            if ($this->getBackendUser()->workspace != 0) {
2348
                // Check for versioning support of the table:
2349
                if ($tableSupportsVersioning) {
2350
                    // If the record is already a version of "something" pass it by.
2351
                    if ($reqRecord['t3ver_oid'] > 0 || (int)$reqRecord['t3ver_state'] === VersionState::NEW_PLACEHOLDER) {
2352
                        // (If it turns out not to be a version of the current workspace there will be trouble, but
2353
                        // that is handled inside DataHandler then and in the interface it would clearly be an error of
2354
                        // links if the user accesses such a scenario)
2355
                        return $reqRecord;
2356
                    }
2357
                    // The input record was online and an offline version must be found or made:
2358
                    // Look for version of this workspace:
2359
                    $versionRec = BackendUtility::getWorkspaceVersionOfRecord(
2360
                        $this->getBackendUser()->workspace,
2361
                        $table,
2362
                        $reqRecord['uid'],
2363
                        'uid,pid,t3ver_oid'
2364
                    );
2365
                    return is_array($versionRec) ? $versionRec : $reqRecord;
2366
                }
2367
                // This means that editing cannot occur on this record because it was not supporting versioning
2368
                // which is required inside an offline workspace.
2369
                return false;
2370
            }
2371
            // In ONLINE workspace, just return the originally requested record:
2372
            return $reqRecord;
2373
        }
2374
        // Return FALSE because the table/uid was not found anyway.
2375
        return false;
2376
    }
2377
2378
    /**
2379
     * Populates the variables $this->storeArray, $this->storeUrl, $this->storeUrlMd5
2380
     * to prepare 'open documents' urls
2381
     */
2382
    protected function compileStoreData(): void
2383
    {
2384
        // @todo: Refactor in TYPO3 v10: This GeneralUtility method fiddles with _GP()
2385
        $this->storeArray = GeneralUtility::compileSelectedGetVarsFromArray(
2386
            'edit,defVals,overrideVals,columnsOnly,noView',
2387
            $this->R_URL_getvars
2388
        );
2389
        $this->storeUrl = HttpUtility::buildQueryString($this->storeArray, '&');
2390
        $this->storeUrlMd5 = md5($this->storeUrl);
2391
    }
2392
2393
    /**
2394
     * Get a TSConfig 'option.' array, possibly for a specific table.
2395
     *
2396
     * @param string $table Table name
2397
     * @param string $key Options key
2398
     * @return string
2399
     */
2400
    protected function getTsConfigOption(string $table, string $key): string
2401
    {
2402
        return \trim((string)(
2403
            $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table]
2404
            ?? $this->getBackendUser()->getTSConfig()['options.'][$key]
2405
            ?? ''
2406
        ));
2407
    }
2408
2409
    /**
2410
     * Handling the closing of a document
2411
     * The argument $mode can be one of this values:
2412
     * - 0/1 will redirect to $this->retUrl [self::DOCUMENT_CLOSE_MODE_DEFAULT || self::DOCUMENT_CLOSE_MODE_REDIRECT]
2413
     * - 3 will clear the docHandler (thus closing all documents) [self::DOCUMENT_CLOSE_MODE_CLEAR_ALL]
2414
     * - 4 will do no redirect [self::DOCUMENT_CLOSE_MODE_NO_REDIRECT]
2415
     * - other values will call setDocument with ->retUrl
2416
     *
2417
     * @param int $mode the close mode: one of self::DOCUMENT_CLOSE_MODE_*
2418
     * @param ServerRequestInterface $request Incoming request
2419
     * @return ResponseInterface|null Redirect response if needed
2420
     */
2421
    protected function closeDocument($mode, ServerRequestInterface $request): ?ResponseInterface
2422
    {
2423
        $setupArr = [];
2424
        $mode = (int)$mode;
2425
        // If current document is found in docHandler,
2426
        // then unset it, possibly unset it ALL and finally, write it to the session data
2427
        if (isset($this->docHandler[$this->storeUrlMd5])) {
2428
            // add the closing document to the recent documents
2429
            $recentDocs = $this->getBackendUser()->getModuleData('opendocs::recent');
2430
            if (!is_array($recentDocs)) {
2431
                $recentDocs = [];
2432
            }
2433
            $closedDoc = $this->docHandler[$this->storeUrlMd5];
2434
            $recentDocs = array_merge([$this->storeUrlMd5 => $closedDoc], $recentDocs);
2435
            if (count($recentDocs) > 8) {
2436
                $recentDocs = array_slice($recentDocs, 0, 8);
2437
            }
2438
            // remove it from the list of the open documents
2439
            unset($this->docHandler[$this->storeUrlMd5]);
2440
            if ($mode === self::DOCUMENT_CLOSE_MODE_CLEAR_ALL) {
2441
                $recentDocs = array_merge($this->docHandler, $recentDocs);
2442
                $this->docHandler = [];
2443
            }
2444
            $this->getBackendUser()->pushModuleData('opendocs::recent', $recentDocs);
2445
            $this->getBackendUser()->pushModuleData('FormEngine', [$this->docHandler, $this->docDat[1]]);
2446
            BackendUtility::setUpdateSignal('OpendocsController::updateNumber', count($this->docHandler));
2447
        }
2448
        if ($mode === self::DOCUMENT_CLOSE_MODE_NO_REDIRECT) {
2449
            return null;
2450
        }
2451
        // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by
2452
        // other scripts, like wizard_add, to know which records was created or so...
2453
        if ($this->returnEditConf && $this->retUrl != (string)$this->uriBuilder->buildUriFromRoute('dummy')) {
2454
            $this->retUrl .= '&returnEditConf=' . rawurlencode((string)json_encode($this->editconf));
2455
        }
2456
        // If mode is NOT set (means 0) OR set to 1, then make a header location redirect to $this->retUrl
2457
        if ($mode === self::DOCUMENT_CLOSE_MODE_DEFAULT || $mode === self::DOCUMENT_CLOSE_MODE_REDIRECT) {
2458
            return new RedirectResponse($this->retUrl, 303);
2459
        }
2460
        if ($this->retUrl === '') {
2461
            return null;
2462
        }
2463
        $retUrl = (string)$this->returnUrl;
2464
        if (is_array($this->docHandler) && !empty($this->docHandler)) {
2465
            if (!empty($setupArr[2])) {
2466
                $sParts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
2467
                $retUrl = $sParts['path'] . '?' . $setupArr[2] . '&returnUrl=' . rawurlencode($retUrl);
2468
            }
2469
        }
2470
        return new RedirectResponse($retUrl, 303);
2471
    }
2472
2473
    /**
2474
     * Returns the shortcut title for the current element
2475
     *
2476
     * @param ServerRequestInterface $request
2477
     * @return string
2478
     */
2479
    protected function getShortcutTitle(ServerRequestInterface $request): string
2480
    {
2481
        $queryParameters = $request->getQueryParams();
2482
        $languageService = $this->getLanguageService();
2483
2484
        if (!is_array($queryParameters['edit'] ?? false)) {
2485
            return '';
2486
        }
2487
2488
        // @todo There may be a more efficient way in using FormEngine FormData.
2489
        // @todo Therefore, the button initialization however has to take place at a later stage.
2490
2491
        $table = (string)key($queryParameters['edit']);
2492
        $tableTitle = $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title'] ?? '') ?: $table;
2493
        $recordId = (int)key($queryParameters['edit'][$table]);
2494
        $action = (string)$queryParameters['edit'][$table][$recordId];
2495
2496
        if ($action === 'new') {
2497
            return $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNew') . ' ' . $tableTitle;
2498
        }
2499
2500
        if ($action === 'edit') {
2501
            $record = BackendUtility::getRecord($table, $recordId) ?? [];
2502
            $recordTitle = BackendUtility::getRecordTitle($table, $record) ?? '';
2503
            if ($table === 'pages') {
2504
                return sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editPage'), $tableTitle, $recordTitle);
2505
            }
2506
            if (!isset($record['pid'])) {
2507
                return $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.edit');
2508
            }
2509
            $pageId = (int)$record['pid'];
2510
            if ($pageId === 0) {
2511
                return sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecordRootLevel'), $tableTitle, $recordTitle);
2512
            }
2513
            $pageRow = BackendUtility::getRecord('pages', $pageId) ?? [];
2514
            $pageTitle = BackendUtility::getRecordTitle('pages', $pageRow);
2515
            if ($recordTitle !== '') {
2516
                return sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecord'), $tableTitle, $recordTitle, $pageTitle);
2517
            }
2518
            return sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecordNoTitle'), $tableTitle, $pageTitle);
2519
        }
2520
2521
        return '';
2522
    }
2523
2524
    /**
2525
     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2526
     */
2527
    protected function getBackendUser()
2528
    {
2529
        return $GLOBALS['BE_USER'];
2530
    }
2531
2532
    /**
2533
     * Returns LanguageService
2534
     *
2535
     * @return \TYPO3\CMS\Core\Localization\LanguageService
2536
     */
2537
    protected function getLanguageService()
2538
    {
2539
        return $GLOBALS['LANG'];
2540
    }
2541
}
2542