Passed
Push — master ( ff1a76...9dbd27 )
by
unknown
13:17
created

getPreviewUrlAnchorSection()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Controller;
19
20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use Psr\Http\Message\ResponseInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
use TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent;
24
use TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent;
25
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
26
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
27
use TYPO3\CMS\Backend\Form\FormDataCompiler;
28
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
29
use TYPO3\CMS\Backend\Form\FormResultCompiler;
30
use TYPO3\CMS\Backend\Form\NodeFactory;
31
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
32
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
33
use TYPO3\CMS\Backend\Routing\UriBuilder;
34
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
35
use TYPO3\CMS\Backend\Template\ModuleTemplate;
36
use TYPO3\CMS\Backend\Utility\BackendUtility;
37
use TYPO3\CMS\Core\Database\ConnectionPool;
38
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
39
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
40
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
41
use TYPO3\CMS\Core\Database\ReferenceIndex;
42
use TYPO3\CMS\Core\DataHandling\DataHandler;
43
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
44
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
45
use TYPO3\CMS\Core\Http\HtmlResponse;
46
use TYPO3\CMS\Core\Http\RedirectResponse;
47
use TYPO3\CMS\Core\Imaging\Icon;
48
use TYPO3\CMS\Core\Messaging\FlashMessage;
49
use TYPO3\CMS\Core\Messaging\FlashMessageService;
50
use TYPO3\CMS\Core\Page\PageRenderer;
51
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
52
use TYPO3\CMS\Core\Site\Entity\NullSite;
53
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
54
use TYPO3\CMS\Core\Site\SiteFinder;
55
use TYPO3\CMS\Core\Type\Bitmask\Permission;
56
use TYPO3\CMS\Core\Utility\GeneralUtility;
57
use TYPO3\CMS\Core\Utility\HttpUtility;
58
use TYPO3\CMS\Core\Utility\MathUtility;
59
use TYPO3\CMS\Core\Utility\PathUtility;
60
use TYPO3\CMS\Core\Versioning\VersionState;
61
62
/**
63
 * Main backend controller almost always used if some database record is edited in the backend.
64
 *
65
 * Main job of this controller is to evaluate and sanitize $request parameters,
66
 * call the DataHandler if records should be created or updated and
67
 * execute FormEngine for record rendering.
68
 */
69
class EditDocumentController
70
{
71
    protected const DOCUMENT_CLOSE_MODE_DEFAULT = 0;
72
    // works like DOCUMENT_CLOSE_MODE_DEFAULT
73
    protected const DOCUMENT_CLOSE_MODE_REDIRECT = 1;
74
    protected const DOCUMENT_CLOSE_MODE_CLEAR_ALL = 3;
75
    protected const DOCUMENT_CLOSE_MODE_NO_REDIRECT = 4;
76
77
    /**
78
     * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
79
     *
80
     * @var array<string,array>
81
     */
82
    protected $editconf = [];
83
84
    /**
85
     * Comma list of field names to edit. If specified, only those fields will be rendered.
86
     * Otherwise all (available) fields in the record are shown according to the TCA type.
87
     *
88
     * @var string|null
89
     */
90
    protected $columnsOnly;
91
92
    /**
93
     * Default values for fields
94
     *
95
     * @var array|null [table][field]
96
     */
97
    protected $defVals;
98
99
    /**
100
     * Array of values to force being set as hidden fields in FormEngine
101
     *
102
     * @var array|null [table][field]
103
     */
104
    protected $overrideVals;
105
106
    /**
107
     * If set, this value will be set in $this->retUrl as "returnUrl", if not,
108
     * $this->retUrl will link to dummy controller
109
     *
110
     * @var string|null
111
     */
112
    protected $returnUrl;
113
114
    /**
115
     * Prepared return URL. Contains the URL that we should return to from FormEngine if
116
     * close button is clicked. Usually passed along as 'returnUrl', but falls back to
117
     * "dummy" controller.
118
     *
119
     * @var string
120
     */
121
    protected $retUrl;
122
123
    /**
124
     * Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
125
     *
126
     * @var int
127
     */
128
    protected $closeDoc;
129
130
    /**
131
     * If true, the processing of incoming data will be performed as if a save-button is pressed.
132
     * Used in the forms as a hidden field which can be set through
133
     * JavaScript if the form is somehow submitted by JavaScript.
134
     *
135
     * @var bool
136
     */
137
    protected $doSave;
138
139
    /**
140
     * Main DataHandler datamap array
141
     *
142
     * @var array
143
     */
144
    protected $data;
145
146
    /**
147
     * Main DataHandler cmdmap array
148
     *
149
     * @var array
150
     */
151
    protected $cmd;
152
153
    /**
154
     * DataHandler 'mirror' input
155
     *
156
     * @var array
157
     */
158
    protected $mirror;
159
160
    /**
161
     * Boolean: If set, then the GET var "&id=" will be added to the
162
     * retUrl string so that the NEW id of something is returned to the script calling the form.
163
     *
164
     * @var bool
165
     */
166
    protected $returnNewPageId = false;
167
168
    /**
169
     * Updated values for backendUser->uc. Used for new inline records to mark them
170
     * as expanded: uc[inlineView][...]
171
     *
172
     * @var array|null
173
     */
174
    protected $uc;
175
176
    /**
177
     * ID for displaying the page in the frontend, "save and view"
178
     *
179
     * @var int
180
     */
181
    protected $popViewId;
182
183
    /**
184
     * Alternative URL for viewing the frontend pages.
185
     *
186
     * @var string
187
     */
188
    protected $viewUrl;
189
190
    /**
191
     * @var string|null
192
     */
193
    protected $previewCode;
194
195
    /**
196
     * Alternative title for the document handler.
197
     *
198
     * @var string
199
     */
200
    protected $recTitle;
201
202
    /**
203
     * If set, then no save & view button is printed
204
     *
205
     * @var bool
206
     */
207
    protected $noView;
208
209
    /**
210
     * @var string
211
     */
212
    protected $perms_clause;
213
214
    /**
215
     * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
216
     *
217
     * @var bool
218
     */
219
    protected $returnEditConf;
220
221
    /**
222
     * parse_url() of current requested URI, contains ['path'] and ['query'] parts.
223
     *
224
     * @var array
225
     */
226
    protected $R_URL_parts;
227
228
    /**
229
     * Contains $request query parameters. This array is the foundation for creating
230
     * the R_URI internal var which becomes the url to which forms are submitted
231
     *
232
     * @var array
233
     */
234
    protected $R_URL_getvars;
235
236
    /**
237
     * Set to the URL of this script including variables which is needed to re-display the form.
238
     *
239
     * @var string
240
     */
241
    protected $R_URI;
242
243
    /**
244
     * @var array
245
     */
246
    protected $pageinfo;
247
248
    /**
249
     * Is loaded with the "title" of the currently "open document"
250
     * used for the open document toolbar
251
     *
252
     * @var string
253
     */
254
    protected $storeTitle = '';
255
256
    /**
257
     * Contains an array with key/value pairs of GET parameters needed to reach the
258
     * current document displayed - used in the 'open documents' toolbar.
259
     *
260
     * @var array
261
     */
262
    protected $storeArray;
263
264
    /**
265
     * $this->storeArray imploded to url
266
     *
267
     * @var string
268
     */
269
    protected $storeUrl;
270
271
    /**
272
     * md5 hash of storeURL, used to identify a single open document in backend user uc
273
     *
274
     * @var string
275
     */
276
    protected $storeUrlMd5;
277
278
    /**
279
     * Backend user session data of this module
280
     *
281
     * @var array
282
     */
283
    protected $docDat;
284
285
    /**
286
     * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
287
     * the various documents on the GET parameter list needed to open it. The values are
288
     * arrays with 0,1,2 keys with information about the document (see compileStoreData()).
289
     * The docHandler variable is stored in the $docDat session data, key "0".
290
     *
291
     * @var array
292
     */
293
    protected $docHandler;
294
295
    /**
296
     * Array of the elements to create edit forms for.
297
     *
298
     * @var array
299
     */
300
    protected $elementsData;
301
302
    /**
303
     * Pointer to the first element in $elementsData
304
     *
305
     * @var array
306
     */
307
    protected $firstEl;
308
309
    /**
310
     * Counter, used to count the number of errors (when users do not have edit permissions)
311
     *
312
     * @var int
313
     */
314
    protected $errorC;
315
316
    /**
317
     * Counter, used to count the number of new record forms displayed
318
     *
319
     * @var int
320
     */
321
    protected $newC;
322
323
    /**
324
     * Is set to the pid value of the last shown record - thus indicating which page to
325
     * show when clicking the SAVE/VIEW button
326
     *
327
     * @var int
328
     */
329
    protected $viewId;
330
331
    /**
332
     * Is set to additional parameters (like "&L=xxx") if the record supports it.
333
     *
334
     * @var string
335
     */
336
    protected $viewId_addParams;
337
338
    /**
339
     * @var FormResultCompiler
340
     */
341
    protected $formResultCompiler;
342
343
    /**
344
     * Used internally to disable the storage of the document reference (eg. new records)
345
     *
346
     * @var int
347
     */
348
    protected $dontStoreDocumentRef = 0;
349
350
    /**
351
     * Stores information needed to preview the currently saved record
352
     *
353
     * @var array
354
     */
355
    protected $previewData = [];
356
357
    /**
358
     * ModuleTemplate object
359
     *
360
     * @var ModuleTemplate
361
     */
362
    protected $moduleTemplate;
363
364
    /**
365
     * Check if a record has been saved
366
     *
367
     * @var bool
368
     */
369
    protected $isSavedRecord;
370
371
    /**
372
     * Check if a page in free translation mode
373
     *
374
     * @var bool
375
     */
376
    protected $isPageInFreeTranslationMode = false;
377
378
    /**
379
     * @var EventDispatcherInterface
380
     */
381
    protected $eventDispatcher;
382
383
    /**
384
     * @var UriBuilder
385
     */
386
    protected $uriBuilder;
387
388
    public function __construct(EventDispatcherInterface $eventDispatcher)
389
    {
390
        $this->eventDispatcher = $eventDispatcher;
391
        $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
392
        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
393
        $this->moduleTemplate->setUiBlock(true);
394
        $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
395
    }
396
397
    /**
398
     * Main dispatcher entry method registered as "record_edit" end point
399
     *
400
     * @param ServerRequestInterface $request the current request
401
     * @return ResponseInterface the response with the content
402
     */
403
    public function mainAction(ServerRequestInterface $request): ResponseInterface
404
    {
405
        // Unlock all locked records
406
        BackendUtility::lockRecords();
407
        if ($response = $this->preInit($request)) {
408
            return $response;
409
        }
410
411
        // Process incoming data via DataHandler?
412
        $parsedBody = $request->getParsedBody();
413
        if ($this->doSave
414
            || isset($parsedBody['_savedok'])
415
            || isset($parsedBody['_saveandclosedok'])
416
            || isset($parsedBody['_savedokview'])
417
            || isset($parsedBody['_savedoknew'])
418
            || isset($parsedBody['_duplicatedoc'])
419
        ) {
420
            if ($response = $this->processData($request)) {
421
                return $response;
422
            }
423
        }
424
425
        $this->init($request);
426
        $this->main($request);
427
428
        return new HtmlResponse($this->moduleTemplate->renderContent());
429
    }
430
431
    /**
432
     * First initialization, always called, even before processData() executes DataHandler processing.
433
     *
434
     * @param ServerRequestInterface $request
435
     * @return ResponseInterface Possible redirect response
436
     */
437
    protected function preInit(ServerRequestInterface $request): ?ResponseInterface
438
    {
439
        if ($response = $this->localizationRedirect($request)) {
440
            return $response;
441
        }
442
443
        $parsedBody = $request->getParsedBody();
444
        $queryParams = $request->getQueryParams();
445
446
        $this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
447
        $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
448
        $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
449
        $this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
450
        $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null);
451
        $this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
452
        $this->doSave = (bool)($parsedBody['doSave'] ?? $queryParams['doSave'] ?? false);
453
        $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
454
        $this->uc = $parsedBody['uc'] ?? $queryParams['uc'] ?? null;
455
456
        // Set overrideVals as default values if defVals does not exist.
457
        // @todo: Why?
458
        if (!is_array($this->defVals) && is_array($this->overrideVals)) {
459
            $this->defVals = $this->overrideVals;
460
        }
461
        $this->addSlugFieldsToColumnsOnly($queryParams);
462
463
        // Set final return URL
464
        $this->retUrl = $this->returnUrl ?: (string)$this->uriBuilder->buildUriFromRoute('dummy');
465
466
        // Change $this->editconf if versioning applies to any of the records
467
        $this->fixWSversioningInEditConf();
468
469
        // Prepare R_URL (request url)
470
        $this->R_URL_parts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
471
        $this->R_URL_getvars = $queryParams;
472
        $this->R_URL_getvars['edit'] = $this->editconf;
473
474
        // Prepare 'open documents' url, this is later modified again various times
475
        $this->compileStoreData();
476
        // Backend user session data of this module
477
        $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
478
        $this->docHandler = $this->docDat[0];
479
480
        // Close document if a request for closing the document has been sent
481
        if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
482
            if ($response = $this->closeDocument($this->closeDoc, $request)) {
483
                return $response;
484
            }
485
        }
486
487
        $event = new BeforeFormEnginePageInitializedEvent($this, $request);
488
        $this->eventDispatcher->dispatch($event);
489
        return null;
490
    }
491
492
    /**
493
     * Always add required fields of slug field
494
     *
495
     * @param array $queryParams
496
     */
497
    protected function addSlugFieldsToColumnsOnly(array $queryParams): void
498
    {
499
        $data = $queryParams['edit'] ?? [];
500
        $data = array_keys($data);
501
        $table = reset($data);
502
        if ($this->columnsOnly && $table !== false && isset($GLOBALS['TCA'][$table])) {
503
            $fields = GeneralUtility::trimExplode(',', $this->columnsOnly, true);
504
            foreach ($fields as $field) {
505
                $postModifiers = $GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['postModifiers'] ?? [];
506
                if (isset($GLOBALS['TCA'][$table]['columns'][$field])
507
                    && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'slug'
508
                    && (!is_array($postModifiers) || $postModifiers === [])
509
                ) {
510
                    foreach ($GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['fields'] ?? [] as $fields) {
511
                        $this->columnsOnly .= ',' . (is_array($fields) ? implode(',', $fields) : $fields);
512
                    }
513
                }
514
            }
515
        }
516
    }
517
518
    /**
519
     * Do processing of data, submitting it to DataHandler. May return a RedirectResponse
520
     *
521
     * @param ServerRequestInterface $request
522
     * @return ResponseInterface|null
523
     */
524
    protected function processData(ServerRequestInterface $request): ?ResponseInterface
525
    {
526
        $parsedBody = $request->getParsedBody();
527
        $queryParams = $request->getQueryParams();
528
529
        $beUser = $this->getBackendUser();
530
531
        // Processing related GET / POST vars
532
        $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];
533
        $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];
534
        $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];
535
        $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);
536
537
        // Only options related to $this->data submission are included here
538
        $tce = GeneralUtility::makeInstance(DataHandler::class);
539
540
        $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);
541
542
        // Set internal vars
543
        if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {
544
            $tce->neverHideAtCopy = 1;
545
        }
546
547
        // Set default values fetched previously from GET / POST vars
548
        if (is_array($this->defVals) && $this->defVals !== [] && is_array($tce->defaultValues)) {
549
            $tce->defaultValues = array_merge_recursive($this->defVals, $tce->defaultValues);
550
        }
551
552
        // Load DataHandler with data
553
        $tce->start($this->data, $this->cmd);
554
        if (is_array($this->mirror)) {
555
            $tce->setMirror($this->mirror);
556
        }
557
558
        // Perform the saving operation with DataHandler:
559
        if ($this->doSave === true) {
560
            $tce->process_datamap();
561
            $tce->process_cmdmap();
562
        }
563
        // If pages are being edited, we set an instruction about updating the page tree after this operation.
564
        if ($tce->pagetreeNeedsRefresh
565
            && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
566
        ) {
567
            BackendUtility::setUpdateSignal('updatePageTree');
568
        }
569
        // If there was saved any new items, load them:
570
        if (!empty($tce->substNEWwithIDs_table)) {
571
            // Save the expanded/collapsed states for new inline records, if any
572
            FormEngineUtility::updateInlineView($this->uc, $tce);
573
            $newEditConf = [];
574
            foreach ($this->editconf as $tableName => $tableCmds) {
575
                $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
576
                if (!empty($keys)) {
577
                    foreach ($keys as $key) {
578
                        $editId = $tce->substNEWwithIDs[$key];
579
                        // Check if the $editId isn't a child record of an IRRE action
580
                        if (!(is_array($tce->newRelatedIDs[$tableName])
581
                            && in_array($editId, $tce->newRelatedIDs[$tableName]))
582
                        ) {
583
                            // Translate new id to the workspace version
584
                            if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
585
                                $beUser->workspace,
586
                                $tableName,
587
                                $editId,
588
                                'uid'
589
                            )) {
590
                                $editId = $versionRec['uid'];
591
                            }
592
                            $newEditConf[$tableName][$editId] = 'edit';
593
                        }
594
                        // Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
595
                        if ($tableName === 'pages'
596
                            && $this->retUrl !== (string)$this->uriBuilder->buildUriFromRoute('dummy')
597
                            && $this->retUrl !== $this->getCloseUrl()
598
                            && $this->returnNewPageId
599
                        ) {
600
                            $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
601
                        }
602
                    }
603
                } else {
604
                    $newEditConf[$tableName] = $tableCmds;
605
                }
606
            }
607
            // Reset editconf if newEditConf has values
608
            if (!empty($newEditConf)) {
609
                $this->editconf = $newEditConf;
610
            }
611
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
612
            $this->R_URL_getvars['edit'] = $this->editconf;
613
            // Unset default values since we don't need them anymore.
614
            unset($this->R_URL_getvars['defVals']);
615
            // Recompile the store* values since editconf changed
616
            $this->compileStoreData();
617
        }
618
        // See if any records was auto-created as new versions?
619
        if (!empty($tce->autoVersionIdMap)) {
620
            $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
621
        }
622
        // If a document is saved and a new one is created right after.
623
        if (isset($parsedBody['_savedoknew']) && is_array($this->editconf)) {
624
            if ($redirect = $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request)) {
625
                return $redirect;
626
            }
627
            // Find the current table
628
            reset($this->editconf);
629
            $nTable = (string)key($this->editconf);
630
            // Finding the first id, getting the records pid+uid
631
            reset($this->editconf[$nTable]);
632
            $nUid = (int)key($this->editconf[$nTable]);
633
            $recordFields = 'pid,uid';
634
            if (BackendUtility::isTableWorkspaceEnabled($nTable)) {
635
                $recordFields .= ',t3ver_oid';
636
            }
637
            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
638
            // Determine insertion mode: 'top' is self-explaining,
639
            // otherwise new elements are inserted after one using a negative uid
640
            $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
641
            // Setting a blank editconf array for a new record:
642
            $this->editconf = [];
643
            // Determine related page ID for regular live context
644
            if ((int)$nRec['t3ver_oid'] === 0) {
645
                if ($insertRecordOnTop) {
646
                    $relatedPageId = $nRec['pid'];
647
                } else {
648
                    $relatedPageId = -$nRec['uid'];
649
                }
650
            } else {
651
                // Determine related page ID for workspace context
652
                if ($insertRecordOnTop) {
653
                    // Fetch live version of workspace version since the pid value is always -1 in workspaces
654
                    $liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
655
                    $relatedPageId = $liveRecord['pid'];
656
                } else {
657
                    // Use uid of live version of workspace version
658
                    $relatedPageId = -$nRec['t3ver_oid'];
659
                }
660
            }
661
            $this->editconf[$nTable][$relatedPageId] = 'new';
662
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
663
            $this->R_URL_getvars['edit'] = $this->editconf;
664
            // Recompile the store* values since editconf changed...
665
            $this->compileStoreData();
666
        }
667
        // If a document should be duplicated.
668
        if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf)) {
669
            $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request);
670
            // Find current table
671
            reset($this->editconf);
672
            $nTable = (string)key($this->editconf);
673
            // Find the first id, getting the records pid+uid
674
            reset($this->editconf[$nTable]);
675
            $nUid = key($this->editconf[$nTable]);
676
            if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
677
                $nUid = $tce->substNEWwithIDs[$nUid];
678
            }
679
680
            $recordFields = 'pid,uid';
681
            if (!BackendUtility::isTableWorkspaceEnabled($nTable)) {
682
                $recordFields .= ',t3ver_oid';
683
            }
684
            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
685
686
            // Setting a blank editconf array for a new record:
687
            $this->editconf = [];
688
689
            if ((int)$nRec['t3ver_oid'] === 0) {
690
                $relatedPageId = -$nRec['uid'];
691
            } else {
692
                $relatedPageId = -$nRec['t3ver_oid'];
693
            }
694
695
            /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $duplicateTce */
696
            $duplicateTce = GeneralUtility::makeInstance(DataHandler::class);
697
698
            $duplicateCmd = [
699
                $nTable => [
700
                    $nUid => [
701
                        'copy' => $relatedPageId
702
                    ]
703
                ]
704
            ];
705
706
            $duplicateTce->start([], $duplicateCmd);
707
            $duplicateTce->process_cmdmap();
708
709
            $duplicateMappingArray = $duplicateTce->copyMappingArray;
710
            $duplicateUid = $duplicateMappingArray[$nTable][$nUid];
711
712
            if ($nTable === 'pages') {
713
                BackendUtility::setUpdateSignal('updatePageTree');
714
            }
715
716
            $this->editconf[$nTable][$duplicateUid] = 'edit';
717
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
718
            $this->R_URL_getvars['edit'] = $this->editconf;
719
            // Recompile the store* values since editconf changed...
720
            $this->compileStoreData();
721
722
            // Inform the user of the duplication
723
            $flashMessage = GeneralUtility::makeInstance(
724
                FlashMessage::class,
725
                $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
726
                '',
727
                FlashMessage::OK
728
            );
729
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
730
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
731
            $defaultFlashMessageQueue->enqueue($flashMessage);
732
        }
733
        // If a preview is requested
734
        if (isset($parsedBody['_savedokview'])) {
735
            $array_keys = array_keys($this->data);
736
            // Get the first table and id of the data array from DataHandler
737
            $table = reset($array_keys);
738
            $array_keys = array_keys($this->data[$table]);
739
            $id = reset($array_keys);
740
            if (!MathUtility::canBeInterpretedAsInteger($id)) {
741
                $id = $tce->substNEWwithIDs[$id];
742
            }
743
            // Store this information for later use
744
            $this->previewData['table'] = $table;
745
            $this->previewData['id'] = $id;
746
        }
747
        $tce->printLogErrorMessages();
748
749
        if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
750
            || isset($parsedBody['_saveandclosedok'])
751
        ) {
752
            // Redirect if element should be closed after save
753
            return $this->closeDocument((int)abs($this->closeDoc), $request);
754
        }
755
        return null;
756
    }
757
758
    /**
759
     * Initialize the view part of the controller logic.
760
     *
761
     * @param ServerRequestInterface $request
762
     */
763
    protected function init(ServerRequestInterface $request): void
764
    {
765
        $parsedBody = $request->getParsedBody();
766
        $queryParams = $request->getQueryParams();
767
768
        $beUser = $this->getBackendUser();
769
770
        $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);
771
        $this->viewUrl = (string)($parsedBody['viewUrl'] ?? $queryParams['viewUrl'] ?? '');
772
        $this->recTitle = (string)($parsedBody['recTitle'] ?? $queryParams['recTitle'] ?? '');
773
        $this->noView = (bool)($parsedBody['noView'] ?? $queryParams['noView'] ?? false);
774
        $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
775
        // Set other internal variables:
776
        $this->R_URL_getvars['returnUrl'] = $this->retUrl;
777
        $this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
778
779
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
780
        $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->moduleTemplate->getPageRenderer()->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) {
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
        $this->languageSwitch(
1019
            (string)($this->firstEl['table'] ?? ''),
1020
            (int)($this->firstEl['uid'] ?? 0),
1021
            isset($this->firstEl['pid']) ? (int)$this->firstEl['pid'] : null
1022
        );
1023
        $this->moduleTemplate->setContent($body);
1024
    }
1025
1026
    /**
1027
     * Creates the editing form with FormEngine, based on the input from GPvars.
1028
     *
1029
     * @return string HTML form elements wrapped in tables
1030
     */
1031
    protected function makeEditForm(): string
1032
    {
1033
        // Initialize variables
1034
        $this->elementsData = [];
1035
        $this->errorC = 0;
1036
        $this->newC = 0;
1037
        $editForm = '';
1038
        $beUser = $this->getBackendUser();
1039
        // Traverse the GPvar edit array tables
1040
        foreach ($this->editconf as $table => $conf) {
1041
            if (is_array($conf) && $GLOBALS['TCA'][$table] && $beUser->check('tables_modify', $table)) {
1042
                // Traverse the keys/comments of each table (keys can be a comma list of uids)
1043
                foreach ($conf as $cKey => $command) {
1044
                    if ($command === 'edit' || $command === 'new') {
1045
                        // Get the ids:
1046
                        $ids = GeneralUtility::trimExplode(',', $cKey, true);
1047
                        // Traverse the ids:
1048
                        foreach ($ids as $theUid) {
1049
                            // Don't save this document title in the document selector if the document is new.
1050
                            if ($command === 'new') {
1051
                                $this->dontStoreDocumentRef = 1;
1052
                            }
1053
1054
                            try {
1055
                                $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
1056
                                $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
1057
                                $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
1058
1059
                                // Reset viewId - it should hold data of last entry only
1060
                                $this->viewId = 0;
1061
                                $this->viewId_addParams = '';
1062
1063
                                $formDataCompilerInput = [
1064
                                    'tableName' => $table,
1065
                                    'vanillaUid' => (int)$theUid,
1066
                                    'command' => $command,
1067
                                    'returnUrl' => $this->R_URI,
1068
                                ];
1069
                                if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
1070
                                    $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
1071
                                }
1072
                                if (!empty($this->defVals) && is_array($this->defVals)) {
1073
                                    $formDataCompilerInput['defaultValues'] = $this->defVals;
1074
                                }
1075
1076
                                $formData = $formDataCompiler->compile($formDataCompilerInput);
1077
1078
                                // Set this->viewId if possible
1079
                                if ($command === 'new'
1080
                                    && $table !== 'pages'
1081
                                    && !empty($formData['parentPageRow']['uid'])
1082
                                ) {
1083
                                    $this->viewId = $formData['parentPageRow']['uid'];
1084
                                } else {
1085
                                    if ($table === 'pages') {
1086
                                        $this->viewId = $formData['databaseRow']['uid'];
1087
                                    } elseif (!empty($formData['parentPageRow']['uid'])) {
1088
                                        $this->viewId = $formData['parentPageRow']['uid'];
1089
                                        // Adding "&L=xx" if the record being edited has a languageField with a value larger than zero!
1090
                                        if (!empty($formData['processedTca']['ctrl']['languageField'])
1091
                                            && is_array($formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']])
1092
                                            && $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0] > 0
1093
                                        ) {
1094
                                            $this->viewId_addParams = '&L=' . $formData['databaseRow'][$formData['processedTca']['ctrl']['languageField']][0];
1095
                                        }
1096
                                    }
1097
                                }
1098
1099
                                // Determine if delete button can be shown
1100
                                $deleteAccess = false;
1101
                                if (
1102
                                    $command === 'edit'
1103
                                    || $command === 'new'
1104
                                ) {
1105
                                    $permission = new Permission($formData['userPermissionOnPage']);
1106
                                    if ($formData['tableName'] === 'pages') {
1107
                                        $deleteAccess = $permission->get(Permission::PAGE_DELETE);
1108
                                    } else {
1109
                                        $deleteAccess = $permission->get(Permission::CONTENT_EDIT);
1110
                                    }
1111
                                }
1112
1113
                                // Display "is-locked" message
1114
                                if ($command === 'edit') {
1115
                                    $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
1116
                                    if ($lockInfo) {
1117
                                        $flashMessage = GeneralUtility::makeInstance(
1118
                                            FlashMessage::class,
1119
                                            $lockInfo['msg'],
1120
                                            '',
1121
                                            FlashMessage::WARNING
1122
                                        );
1123
                                        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1124
                                        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1125
                                        $defaultFlashMessageQueue->enqueue($flashMessage);
1126
                                    }
1127
                                }
1128
1129
                                // Record title
1130
                                if (!$this->storeTitle) {
1131
                                    $this->storeTitle = $this->recTitle
1132
                                        ? htmlspecialchars($this->recTitle)
1133
                                        : BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($formData['databaseRow']), true);
1134
                                }
1135
1136
                                $this->elementsData[] = [
1137
                                    'table' => $table,
1138
                                    'uid' => $formData['databaseRow']['uid'],
1139
                                    'pid' => $formData['databaseRow']['pid'],
1140
                                    'cmd' => $command,
1141
                                    'deleteAccess' => $deleteAccess
1142
                                ];
1143
1144
                                if ($command !== 'new') {
1145
                                    BackendUtility::lockRecords($table, $formData['databaseRow']['uid'], $table === 'tt_content' ? $formData['databaseRow']['pid'] : 0);
1146
                                }
1147
1148
                                // Set list if only specific fields should be rendered. This will trigger
1149
                                // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
1150
                                if ($this->columnsOnly) {
1151
                                    if (is_array($this->columnsOnly)) {
1152
                                        $formData['fieldListToRender'] = $this->columnsOnly[$table];
1153
                                    } else {
1154
                                        $formData['fieldListToRender'] = $this->columnsOnly;
1155
                                    }
1156
                                }
1157
1158
                                $formData['renderType'] = 'outerWrapContainer';
1159
                                $formResult = $nodeFactory->create($formData)->render();
1160
1161
                                $html = $formResult['html'];
1162
1163
                                $formResult['html'] = '';
1164
                                $formResult['doSaveFieldName'] = 'doSave';
1165
1166
                                // @todo: Put all the stuff into FormEngine as final "compiler" class
1167
                                // @todo: This is done here for now to not rewrite addCssFiles()
1168
                                // @todo: and printNeededJSFunctions() now
1169
                                $this->formResultCompiler->mergeResult($formResult);
1170
1171
                                // Seems the pid is set as hidden field (again) at end?!
1172
                                if ($command === 'new') {
1173
                                    // @todo: looks ugly
1174
                                    $html .= LF
1175
                                        . '<input type="hidden"'
1176
                                        . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($formData['databaseRow']['uid']) . '][pid]"'
1177
                                        . ' value="' . (int)$formData['databaseRow']['pid'] . '" />';
1178
                                    $this->newC++;
1179
                                }
1180
1181
                                $editForm .= $html;
1182
                            } catch (AccessDeniedException $e) {
1183
                                $this->errorC++;
1184
                                // Try to fetch error message from "recordInternals" be user object
1185
                                // @todo: This construct should be logged and localized and de-uglified
1186
                                $message = (!empty($beUser->errorMsg)) ? $beUser->errorMsg : $e->getMessage() . ' ' . $e->getCode();
1187
                                $title = $this->getLanguageService()
1188
                                    ->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission');
1189
                                $editForm .= $this->getInfobox($message, $title);
1190
                            } catch (DatabaseRecordException $e) {
1191
                                $editForm .= $this->getInfobox($e->getMessage());
1192
                            }
1193
                        } // End of for each uid
1194
                    }
1195
                }
1196
            }
1197
        }
1198
        return $editForm;
1199
    }
1200
1201
    /**
1202
     * Helper function for rendering an Infobox
1203
     *
1204
     * @param string $message
1205
     * @param string|null $title
1206
     * @return string
1207
     */
1208
    protected function getInfobox(string $message, ?string $title = null): string
1209
    {
1210
        return '<div class="callout callout-danger">' .
1211
                '<div class="media">' .
1212
                    '<div class="media-left">' .
1213
                        '<span class="fa-stack fa-lg callout-icon">' .
1214
                            '<i class="fa fa-circle fa-stack-2x"></i>' .
1215
                            '<i class="fa fa-times fa-stack-1x"></i>' .
1216
                        '</span>' .
1217
                    '</div>' .
1218
                    '<div class="media-body">' .
1219
                        ($title ? '<h4 class="callout-title">' . htmlspecialchars($title) . '</h4>' : '') .
1220
                        '<div class="callout-body">' . htmlspecialchars($message) . '</div>' .
1221
                    '</div>' .
1222
                '</div>' .
1223
            '</div>';
1224
    }
1225
1226
    /**
1227
     * Create the panel of buttons for submitting the form or otherwise perform operations.
1228
     *
1229
     * @param ServerRequestInterface $request
1230
     */
1231
    protected function getButtons(ServerRequestInterface $request): void
1232
    {
1233
        $record = BackendUtility::getRecord($this->firstEl['table'], $this->firstEl['uid']);
1234
        $TCActrl = $GLOBALS['TCA'][$this->firstEl['table']]['ctrl'];
1235
1236
        $this->setIsSavedRecord();
1237
1238
        $sysLanguageUid = 0;
1239
        if (
1240
            $this->isSavedRecord
1241
            && isset($TCActrl['languageField'], $record[$TCActrl['languageField']])
1242
        ) {
1243
            $sysLanguageUid = (int)$record[$TCActrl['languageField']];
1244
        } elseif (isset($this->defVals['sys_language_uid'])) {
1245
            $sysLanguageUid = (int)$this->defVals['sys_language_uid'];
1246
        }
1247
1248
        $l18nParent = isset($TCActrl['transOrigPointerField'], $record[$TCActrl['transOrigPointerField']])
1249
            ? (int)$record[$TCActrl['transOrigPointerField']]
1250
            : 0;
1251
1252
        $this->setIsPageInFreeTranslationMode($record, $sysLanguageUid);
1253
1254
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1255
1256
        $this->registerCloseButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 1);
1257
1258
        // Show buttons when table is not read-only
1259
        if (
1260
            !$this->errorC
1261
            && !$GLOBALS['TCA'][$this->firstEl['table']]['ctrl']['readOnly']
1262
        ) {
1263
            $this->registerSaveButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 2);
1264
            $this->registerViewButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 3);
1265
            if ($this->firstEl['cmd'] !== 'new') {
1266
                $this->registerNewButtonToButtonBar(
1267
                    $buttonBar,
1268
                    ButtonBar::BUTTON_POSITION_LEFT,
1269
                    4,
1270
                    $sysLanguageUid,
1271
                    $l18nParent
1272
                );
1273
                $this->registerDuplicationButtonToButtonBar(
1274
                    $buttonBar,
1275
                    ButtonBar::BUTTON_POSITION_LEFT,
1276
                    5,
1277
                    $sysLanguageUid,
1278
                    $l18nParent
1279
                );
1280
            }
1281
            $this->registerDeleteButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 6);
1282
            $this->registerColumnsOnlyButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_LEFT, 7);
1283
            $this->registerHistoryButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1284
        }
1285
1286
        $this->registerOpenInNewWindowButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 2);
1287
        $this->registerShortcutButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 3, $request);
1288
        $this->registerCshButtonToButtonBar($buttonBar, ButtonBar::BUTTON_POSITION_RIGHT, 4);
1289
    }
1290
1291
    /**
1292
     * Set the boolean to check if the record is saved
1293
     */
1294
    protected function setIsSavedRecord()
1295
    {
1296
        if (!is_bool($this->isSavedRecord)) {
0 ignored issues
show
introduced by
The condition is_bool($this->isSavedRecord) is always true.
Loading history...
1297
            $this->isSavedRecord = (
1298
                $this->firstEl['cmd'] !== 'new'
1299
                && MathUtility::canBeInterpretedAsInteger($this->firstEl['uid'])
1300
            );
1301
        }
1302
    }
1303
1304
    /**
1305
     * Returns if inconsistent language handling is allowed
1306
     *
1307
     * @return bool
1308
     */
1309
    protected function isInconsistentLanguageHandlingAllowed(): bool
1310
    {
1311
        $allowInconsistentLanguageHandling = BackendUtility::getPagesTSconfig(
1312
            $this->pageinfo['uid']
1313
        )['mod']['web_layout']['allowInconsistentLanguageHandling'];
1314
1315
        return $allowInconsistentLanguageHandling['value'] === '1';
1316
    }
1317
1318
    /**
1319
     * Set the boolean to check if the page is in free translation mode
1320
     *
1321
     * @param array|null $record
1322
     * @param int $sysLanguageUid
1323
     */
1324
    protected function setIsPageInFreeTranslationMode($record, int $sysLanguageUid)
1325
    {
1326
        if ($this->firstEl['table'] === 'tt_content') {
1327
            if (!$this->isSavedRecord) {
1328
                $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1329
                    (int)$this->pageinfo['uid'],
1330
                    (int)$this->defVals['colPos'],
1331
                    $sysLanguageUid
1332
                );
1333
            } else {
1334
                $this->isPageInFreeTranslationMode = $this->getFreeTranslationMode(
1335
                    (int)$this->pageinfo['uid'],
1336
                    (int)$record['colPos'],
1337
                    $sysLanguageUid
1338
                );
1339
            }
1340
        }
1341
    }
1342
1343
    /**
1344
     * Check if the page is in free translation mode
1345
     *
1346
     * @param int $page
1347
     * @param int $column
1348
     * @param int $language
1349
     * @return bool
1350
     */
1351
    protected function getFreeTranslationMode(int $page, int $column, int $language): bool
1352
    {
1353
        $freeTranslationMode = false;
1354
1355
        if (
1356
            $this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0
1357
            && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0
1358
        ) {
1359
            $freeTranslationMode = true;
1360
        }
1361
1362
        return $freeTranslationMode;
1363
    }
1364
1365
    /**
1366
     * Register the close button to the button bar
1367
     *
1368
     * @param ButtonBar $buttonBar
1369
     * @param string $position
1370
     * @param int $group
1371
     */
1372
    protected function registerCloseButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1373
    {
1374
        $closeButton = $buttonBar->makeLinkButton()
1375
            ->setHref('#')
1376
            ->setClasses('t3js-editform-close')
1377
            ->setTitle($this->getLanguageService()->sL(
1378
                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'
1379
            ))
1380
            ->setShowLabelText(true)
1381
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1382
                'actions-close',
1383
                Icon::SIZE_SMALL
1384
            ));
1385
1386
        $buttonBar->addButton($closeButton, $position, $group);
1387
    }
1388
1389
    /**
1390
     * Register the save button to the button bar
1391
     *
1392
     * @param ButtonBar $buttonBar
1393
     * @param string $position
1394
     * @param int $group
1395
     */
1396
    protected function registerSaveButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1397
    {
1398
        $saveButton = $buttonBar->makeInputButton()
1399
            ->setForm('EditDocumentController')
1400
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1401
            ->setName('_savedok')
1402
            ->setShowLabelText(true)
1403
            ->setTitle($this->getLanguageService()->sL(
1404
                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'
1405
            ))
1406
            ->setValue('1');
1407
1408
        $buttonBar->addButton($saveButton, $position, $group);
1409
    }
1410
1411
    /**
1412
     * Register the view button to the button bar
1413
     *
1414
     * @param ButtonBar $buttonBar
1415
     * @param string $position
1416
     * @param int $group
1417
     */
1418
    protected function registerViewButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1419
    {
1420
        if (
1421
            $this->viewId // Pid to show the record
1422
            && !$this->noView // Passed parameter
1423
            && !empty($this->firstEl['table']) // No table
1424
1425
            // @TODO: TsConfig option should change to viewDoc
1426
            && $this->getTsConfigOption($this->firstEl['table'], 'saveDocView')
1427
        ) {
1428
            $classNames = 't3js-editform-view';
1429
1430
            $pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
1431
1432
            if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
1433
                $excludeDokTypes = GeneralUtility::intExplode(
1434
                    ',',
1435
                    $pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
1436
                    true
1437
                );
1438
            } else {
1439
                // exclude sysfolders, spacers and recycler by default
1440
                $excludeDokTypes = [
1441
                    PageRepository::DOKTYPE_RECYCLER,
1442
                    PageRepository::DOKTYPE_SYSFOLDER,
1443
                    PageRepository::DOKTYPE_SPACER
1444
                ];
1445
            }
1446
1447
            if (
1448
                !in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)
1449
                || isset($pagesTSconfig['TCEMAIN.']['preview.'][$this->firstEl['table'] . '.']['previewPageId'])
1450
            ) {
1451
                $previewPageId = $this->getPreviewPageId();
1452
                try {
1453
                    $previewUrl = BackendUtility::getPreviewUrl(
1454
                        $previewPageId,
1455
                        '',
1456
                        BackendUtility::BEgetRootLine($previewPageId),
1457
                        $this->getPreviewUrlAnchorSection(),
1458
                        $this->viewUrl,
1459
                        $this->getPreviewUrlParameters($previewPageId)
1460
                    );
1461
1462
                    $viewButton = $buttonBar->makeLinkButton()
1463
                        ->setHref($previewUrl)
1464
                        ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1465
                            'actions-view',
1466
                            Icon::SIZE_SMALL
1467
                        ))
1468
                        ->setShowLabelText(true)
1469
                        ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc'));
1470
1471
                    if (!$this->isSavedRecord) {
1472
                        if ($this->firstEl['table'] === 'pages') {
1473
                            $viewButton->setDataAttributes(['is-new' => '']);
1474
                        }
1475
                    }
1476
1477
                    if ($classNames !== '') {
0 ignored issues
show
introduced by
The condition $classNames !== '' is always true.
Loading history...
1478
                        $viewButton->setClasses($classNames);
1479
                    }
1480
1481
                    $buttonBar->addButton($viewButton, $position, $group);
1482
                } catch (UnableToLinkToPageException $e) {
1483
                    // Do not add any button
1484
                }
1485
            }
1486
        }
1487
    }
1488
1489
    /**
1490
     * Register the new button to the button bar
1491
     *
1492
     * @param ButtonBar $buttonBar
1493
     * @param string $position
1494
     * @param int $group
1495
     * @param int $sysLanguageUid
1496
     * @param int $l18nParent
1497
     */
1498
    protected function registerNewButtonToButtonBar(
1499
        ButtonBar $buttonBar,
1500
        string $position,
1501
        int $group,
1502
        int $sysLanguageUid,
1503
        int $l18nParent
1504
    ) {
1505
        if (
1506
            $this->firstEl['table'] !== 'sys_file_metadata'
1507
            && !empty($this->firstEl['table'])
1508
            && (
1509
                (
1510
                    (
1511
                        $this->isInconsistentLanguageHandlingAllowed()
1512
                        || $this->isPageInFreeTranslationMode
1513
                    )
1514
                    && $this->firstEl['table'] === 'tt_content'
1515
                )
1516
                || (
1517
                    $this->firstEl['table'] !== 'tt_content'
1518
                    && (
1519
                        $sysLanguageUid === 0
1520
                        || $l18nParent === 0
1521
                    )
1522
                )
1523
            )
1524
            && $this->getTsConfigOption($this->firstEl['table'], 'saveDocNew')
1525
        ) {
1526
            $classNames = 't3js-editform-new';
1527
1528
            $newButton = $buttonBar->makeLinkButton()
1529
                ->setHref('#')
1530
                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1531
                    'actions-add',
1532
                    Icon::SIZE_SMALL
1533
                ))
1534
                ->setShowLabelText(true)
1535
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc'));
1536
1537
            if (!$this->isSavedRecord) {
1538
                $newButton->setDataAttributes(['is-new' => '']);
1539
            }
1540
1541
            if ($classNames !== '') {
0 ignored issues
show
introduced by
The condition $classNames !== '' is always true.
Loading history...
1542
                $newButton->setClasses($classNames);
1543
            }
1544
1545
            $buttonBar->addButton($newButton, $position, $group);
1546
        }
1547
    }
1548
1549
    /**
1550
     * Register the duplication button to the button bar
1551
     *
1552
     * @param ButtonBar $buttonBar
1553
     * @param string $position
1554
     * @param int $group
1555
     * @param int $sysLanguageUid
1556
     * @param int $l18nParent
1557
     */
1558
    protected function registerDuplicationButtonToButtonBar(
1559
        ButtonBar $buttonBar,
1560
        string $position,
1561
        int $group,
1562
        int $sysLanguageUid,
1563
        int $l18nParent
1564
    ) {
1565
        if (
1566
            $this->firstEl['table'] !== 'sys_file_metadata'
1567
            && !empty($this->firstEl['table'])
1568
            && (
1569
                (
1570
                    (
1571
                        $this->isInconsistentLanguageHandlingAllowed()
1572
                        || $this->isPageInFreeTranslationMode
1573
                    )
1574
                    && $this->firstEl['table'] === 'tt_content'
1575
                )
1576
                || (
1577
                    $this->firstEl['table'] !== 'tt_content'
1578
                    && (
1579
                        $sysLanguageUid === 0
1580
                        || $l18nParent === 0
1581
                    )
1582
                )
1583
            )
1584
            && $this->getTsConfigOption($this->firstEl['table'], 'showDuplicate')
1585
        ) {
1586
            $classNames = 't3js-editform-duplicate';
1587
1588
            $duplicateButton = $buttonBar->makeLinkButton()
1589
                ->setHref('#')
1590
                ->setShowLabelText(true)
1591
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc'))
1592
                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1593
                    'actions-document-duplicates-select',
1594
                    Icon::SIZE_SMALL
1595
                ));
1596
1597
            if (!$this->isSavedRecord) {
1598
                $duplicateButton->setDataAttributes(['is-new' => '']);
1599
            }
1600
1601
            if ($classNames !== '') {
0 ignored issues
show
introduced by
The condition $classNames !== '' is always true.
Loading history...
1602
                $duplicateButton->setClasses($classNames);
1603
            }
1604
1605
            $buttonBar->addButton($duplicateButton, $position, $group);
1606
        }
1607
    }
1608
1609
    /**
1610
     * Register the delete button to the button bar
1611
     *
1612
     * @param ButtonBar $buttonBar
1613
     * @param string $position
1614
     * @param int $group
1615
     */
1616
    protected function registerDeleteButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1617
    {
1618
        if (
1619
            $this->firstEl['deleteAccess']
1620
            && !$this->getDisableDelete()
1621
            && $this->isSavedRecord
1622
            && count($this->elementsData) === 1
1623
        ) {
1624
            $classNames = 't3js-editform-delete-record';
1625
            $returnUrl = $this->retUrl;
1626
            if ($this->firstEl['table'] === 'pages') {
1627
                parse_str((string)parse_url($returnUrl, PHP_URL_QUERY), $queryParams);
1628
                if (
1629
                    isset($queryParams['route'], $queryParams['id'])
1630
                    && (string)$this->firstEl['uid'] === (string)$queryParams['id']
1631
                ) {
1632
                    // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
1633
                    // tree from the outside to be able to mark the pid as active
1634
                    $returnUrl = (string)$this->uriBuilder->buildUriFromRoutePath($queryParams['route'], ['id' => 0]);
1635
                }
1636
            }
1637
1638
            /** @var ReferenceIndex $referenceIndex */
1639
            $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1640
            $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords(
1641
                $this->firstEl['table'],
1642
                (int)$this->firstEl['uid']
1643
            );
1644
1645
            $referenceCountMessage = BackendUtility::referenceCount(
1646
                $this->firstEl['table'],
1647
                (string)(int)$this->firstEl['uid'],
1648
                $this->getLanguageService()->sL(
1649
                    'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'
1650
                ),
1651
                (string)$numberOfReferences
1652
            );
1653
            $translationCountMessage = BackendUtility::translationCount(
1654
                $this->firstEl['table'],
1655
                (string)(int)$this->firstEl['uid'],
1656
                $this->getLanguageService()->sL(
1657
                    'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'
1658
                )
1659
            );
1660
1661
            $deleteUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [
1662
                'cmd' => [
1663
                    $this->firstEl['table'] => [
1664
                        $this->firstEl['uid'] => [
1665
                            'delete' => '1'
1666
                        ]
1667
                    ]
1668
                ],
1669
                'redirect' => $this->retUrl
1670
            ]);
1671
1672
            $deleteButton = $buttonBar->makeLinkButton()
1673
                ->setClasses($classNames)
1674
                ->setDataAttributes([
1675
                    'return-url' => $returnUrl,
1676
                    'uid' => $this->firstEl['uid'],
1677
                    'table' => $this->firstEl['table'],
1678
                    'reference-count-message' => $referenceCountMessage,
1679
                    'translation-count-message' => $translationCountMessage
1680
                ])
1681
                ->setHref($deleteUrl)
1682
                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1683
                    'actions-edit-delete',
1684
                    Icon::SIZE_SMALL
1685
                ))
1686
                ->setShowLabelText(true)
1687
                ->setTitle($this->getLanguageService()->getLL('deleteItem'));
1688
1689
            $buttonBar->addButton($deleteButton, $position, $group);
1690
        }
1691
    }
1692
1693
    /**
1694
     * Register the history button to the button bar
1695
     *
1696
     * @param ButtonBar $buttonBar
1697
     * @param string $position
1698
     * @param int $group
1699
     */
1700
    protected function registerHistoryButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1701
    {
1702
        if (
1703
            count($this->elementsData) === 1
1704
            && !empty($this->firstEl['table'])
1705
            && $this->getTsConfigOption($this->firstEl['table'], 'showHistory')
1706
        ) {
1707
            $historyUrl = (string)$this->uriBuilder->buildUriFromRoute('record_history', [
1708
                'element' => $this->firstEl['table'] . ':' . $this->firstEl['uid'],
1709
                'returnUrl' => $this->R_URI,
1710
            ]);
1711
            $historyButton = $buttonBar->makeLinkButton()
1712
                ->setHref($historyUrl)
1713
                ->setTitle('Open history of this record')
1714
                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1715
                    'actions-document-history-open',
1716
                    Icon::SIZE_SMALL
1717
                ));
1718
1719
            $buttonBar->addButton($historyButton, $position, $group);
1720
        }
1721
    }
1722
1723
    /**
1724
     * Register the columns only button to the button bar
1725
     *
1726
     * @param ButtonBar $buttonBar
1727
     * @param string $position
1728
     * @param int $group
1729
     */
1730
    protected function registerColumnsOnlyButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1731
    {
1732
        if (
1733
            $this->columnsOnly
1734
            && count($this->elementsData) === 1
1735
        ) {
1736
            $columnsOnlyButton = $buttonBar->makeLinkButton()
1737
                ->setHref($this->R_URI . '&columnsOnly=')
1738
                ->setTitle($this->getLanguageService()->getLL('editWholeRecord'))
1739
                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
1740
                    'actions-open',
1741
                    Icon::SIZE_SMALL
1742
                ));
1743
1744
            $buttonBar->addButton($columnsOnlyButton, $position, $group);
1745
        }
1746
    }
1747
1748
    /**
1749
     * Register the open in new window button to the button bar
1750
     *
1751
     * @param ButtonBar $buttonBar
1752
     * @param string $position
1753
     * @param int $group
1754
     */
1755
    protected function registerOpenInNewWindowButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1756
    {
1757
        $closeUrl = $this->getCloseUrl();
1758
        if ($this->returnUrl !== $closeUrl) {
1759
            $requestUri = GeneralUtility::linkThisScript([
1760
                'returnUrl' => $closeUrl,
1761
            ]);
1762
            $aOnClick = 'vHWin=window.open('
1763
                . GeneralUtility::quoteJSvalue($requestUri) . ','
1764
                . GeneralUtility::quoteJSvalue(md5($this->R_URI))
1765
                . ',\'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1\');vHWin.focus();return false;';
1766
1767
            $openInNewWindowButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()
1768
                ->makeLinkButton()
1769
                ->setHref('#')
1770
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
1771
                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-window-open', Icon::SIZE_SMALL))
1772
                ->setOnClick($aOnClick);
1773
1774
            $buttonBar->addButton($openInNewWindowButton, $position, $group);
1775
        }
1776
    }
1777
1778
    /**
1779
     * Register the shortcut button to the button bar
1780
     *
1781
     * @param ButtonBar $buttonBar
1782
     * @param string $position
1783
     * @param int $group
1784
     * @param ServerRequestInterface $request
1785
     */
1786
    protected function registerShortcutButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group, ServerRequestInterface $request)
1787
    {
1788
        if ($this->returnUrl !== $this->getCloseUrl()) {
1789
            $queryParams = $request->getQueryParams();
1790
            $potentialArguments = [
1791
                'returnUrl',
1792
                'edit',
1793
                'defVals',
1794
                'overrideVals',
1795
                'columnsOnly',
1796
                'returnNewPageId',
1797
                'noView'
1798
            ];
1799
            $arguments = [
1800
                'route' => $queryParams['route'],
1801
            ];
1802
            foreach ($potentialArguments as $argument) {
1803
                if (!empty($queryParams[$argument])) {
1804
                    $arguments[$argument] = $queryParams[$argument];
1805
                }
1806
            }
1807
            $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
1808
            $shortCutButton
1809
                ->setModuleName('xMOD_alt_doc.php')
1810
                ->setDisplayName($this->getShortcutTitle($request))
1811
                ->setArguments($arguments);
1812
            $buttonBar->addButton($shortCutButton, $position, $group);
1813
        }
1814
    }
1815
1816
    /**
1817
     * Register the CSH button to the button bar
1818
     *
1819
     * @param ButtonBar $buttonBar
1820
     * @param string $position
1821
     * @param int $group
1822
     */
1823
    protected function registerCshButtonToButtonBar(ButtonBar $buttonBar, string $position, int $group)
1824
    {
1825
        $cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('TCEforms');
1826
1827
        $buttonBar->addButton($cshButton, $position, $group);
1828
    }
1829
1830
    /**
1831
     * Get the count of connected translated content elements
1832
     *
1833
     * @param int $page
1834
     * @param int $column
1835
     * @param int $language
1836
     * @return int
1837
     */
1838
    protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int
1839
    {
1840
        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1841
1842
        return (int)$queryBuilder
1843
            ->andWhere(
1844
                $queryBuilder->expr()->gt(
1845
                    $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1846
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1847
                )
1848
            )
1849
            ->execute()
1850
            ->fetchColumn(0);
1851
    }
1852
1853
    /**
1854
     * Get the count of standalone translated content elements
1855
     *
1856
     * @param int $page
1857
     * @param int $column
1858
     * @param int $language
1859
     * @return int
1860
     */
1861
    protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int
1862
    {
1863
        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
1864
1865
        return (int)$queryBuilder
1866
            ->andWhere(
1867
                $queryBuilder->expr()->eq(
1868
                    $GLOBALS['TCA']['tt_content']['ctrl']['transOrigPointerField'],
1869
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1870
                )
1871
            )
1872
            ->execute()
1873
            ->fetchColumn(0);
1874
    }
1875
1876
    /**
1877
     * Get the query builder for the translation mode
1878
     *
1879
     * @param int $page
1880
     * @param int $column
1881
     * @param int $language
1882
     * @return QueryBuilder
1883
     */
1884
    protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder
1885
    {
1886
        $languageField = $GLOBALS['TCA']['tt_content']['ctrl']['languageField'];
1887
1888
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1889
            ->getQueryBuilderForTable('tt_content');
1890
1891
        $queryBuilder->getRestrictions()
1892
            ->removeAll()
1893
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1894
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
1895
1896
        return $queryBuilder
1897
            ->count('uid')
1898
            ->from('tt_content')
1899
            ->where(
1900
                $queryBuilder->expr()->eq(
1901
                    'pid',
1902
                    $queryBuilder->createNamedParameter($page, \PDO::PARAM_INT)
1903
                ),
1904
                $queryBuilder->expr()->eq(
1905
                    $languageField,
1906
                    $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
1907
                ),
1908
                $queryBuilder->expr()->eq(
1909
                    'colPos',
1910
                    $queryBuilder->createNamedParameter($column, \PDO::PARAM_INT)
1911
                )
1912
            );
1913
    }
1914
1915
    /**
1916
     * Put together the various elements (buttons, selectors, form) into a table
1917
     *
1918
     * @param string $editForm HTML form.
1919
     * @return string Composite HTML
1920
     */
1921
    protected function compileForm(string $editForm): string
1922
    {
1923
        $formContent = '
1924
            <form
1925
                action="' . htmlspecialchars($this->R_URI) . '"
1926
                method="post"
1927
                enctype="multipart/form-data"
1928
                name="editform"
1929
                id="EditDocumentController"
1930
            >
1931
            ' . $editForm . '
1932
            <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
1933
            <input type="hidden" name="viewUrl" value="' . htmlspecialchars($this->viewUrl) . '" />
1934
            <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$this->viewId) . '" />
1935
            <input type="hidden" name="closeDoc" value="0" />
1936
            <input type="hidden" name="doSave" value="0" />
1937
            <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
1938
            <input type="hidden" name="_scrollPosition" value="" />';
1939
        if ($this->returnNewPageId) {
1940
            $formContent .= '<input type="hidden" name="returnNewPageId" value="1" />';
1941
        }
1942
        if ($this->viewId_addParams) {
1943
            $formContent .= '<input type="hidden" name="popViewId_addParams" value="' . htmlspecialchars($this->viewId_addParams) . '" />';
1944
        }
1945
        return $formContent;
1946
    }
1947
1948
    /**
1949
     * Returns if delete for the current table is disabled by configuration.
1950
     * For sys_file_metadata in default language delete is always disabled.
1951
     *
1952
     * @return bool
1953
     */
1954
    protected function getDisableDelete(): bool
1955
    {
1956
        $disableDelete = false;
1957
        if ($this->firstEl['table'] === 'sys_file_metadata') {
1958
            $row = BackendUtility::getRecord('sys_file_metadata', $this->firstEl['uid'], 'sys_language_uid');
1959
            $languageUid = $row['sys_language_uid'];
1960
            if ($languageUid === 0) {
1961
                $disableDelete = true;
1962
            }
1963
        } else {
1964
            $disableDelete = (bool)$this->getTsConfigOption($this->firstEl['table'] ?? '', 'disableDelete');
1965
        }
1966
        return $disableDelete;
1967
    }
1968
1969
    /**
1970
     * Returns the URL (usually for the "returnUrl") which closes the current window.
1971
     * Used when editing a record in a popup.
1972
     *
1973
     * @return string
1974
     */
1975
    protected function getCloseUrl(): string
1976
    {
1977
        $closeUrl = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Html/Close.html');
1978
        return PathUtility::getAbsoluteWebPath($closeUrl);
1979
    }
1980
1981
    /***************************
1982
     *
1983
     * Localization stuff
1984
     *
1985
     ***************************/
1986
    /**
1987
     * Make selector box for creating new translation for a record or switching to edit the record in an existing
1988
     * language.
1989
     * Displays only languages which are available for the current page.
1990
     *
1991
     * @param string $table Table name
1992
     * @param int $uid Uid for which to create a new language
1993
     * @param int|null $pid Pid of the record
1994
     */
1995
    protected function languageSwitch(string $table, int $uid, $pid = null)
1996
    {
1997
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1998
        $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1999
        // Table editable and activated for languages?
2000
        if ($this->getBackendUser()->check('tables_modify', $table)
2001
            && $languageField
2002
            && $transOrigPointerField
2003
        ) {
2004
            if ($pid === null) {
2005
                $row = BackendUtility::getRecord($table, $uid, 'pid');
2006
                $pid = $row['pid'];
2007
            }
2008
            // Get all available languages for the page
2009
            // If editing a page, the translations of the current UID need to be fetched
2010
            if ($table === 'pages') {
2011
                $row = BackendUtility::getRecord($table, $uid, $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']);
2012
                // Ensure the check is always done against the default language page
2013
                $availableLanguages = $this->getLanguages(
2014
                    (int)$row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $uid,
2015
                    $table
2016
                );
2017
            } else {
2018
                $availableLanguages = $this->getLanguages((int)$pid, $table);
2019
            }
2020
            // Page available in other languages than default language?
2021
            if (count($availableLanguages) > 1) {
2022
                $rowsByLang = [];
2023
                $fetchFields = 'uid,' . $languageField . ',' . $transOrigPointerField;
2024
                // Get record in current language
2025
                $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
2026
                if (!is_array($rowCurrent)) {
2027
                    $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
2028
                }
2029
                $currentLanguage = (int)$rowCurrent[$languageField];
2030
                // Disabled for records with [all] language!
2031
                if ($currentLanguage > -1) {
2032
                    // Get record in default language if needed
2033
                    if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
2034
                        $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
2035
                            $table,
2036
                            $rowCurrent[$transOrigPointerField],
2037
                            $fetchFields
2038
                        );
2039
                        if (!is_array($rowsByLang[0])) {
2040
                            $rowsByLang[0] = BackendUtility::getRecord(
2041
                                $table,
2042
                                $rowCurrent[$transOrigPointerField],
2043
                                $fetchFields
2044
                            );
2045
                        }
2046
                    } else {
2047
                        $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
2048
                    }
2049
                    if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
2050
                        // Get record in other languages to see what's already available
2051
2052
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2053
                            ->getQueryBuilderForTable($table);
2054
2055
                        $queryBuilder->getRestrictions()
2056
                            ->removeAll()
2057
                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2058
                            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
2059
2060
                        $result = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fetchFields, true))
2061
                            ->from($table)
2062
                            ->where(
2063
                                $queryBuilder->expr()->eq(
2064
                                    'pid',
2065
                                    $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
2066
                                ),
2067
                                $queryBuilder->expr()->gt(
2068
                                    $languageField,
2069
                                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2070
                                ),
2071
                                $queryBuilder->expr()->eq(
2072
                                    $transOrigPointerField,
2073
                                    $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], \PDO::PARAM_INT)
2074
                                )
2075
                            )
2076
                            ->execute();
2077
2078
                        while ($row = $result->fetch()) {
2079
                            $rowsByLang[$row[$languageField]] = $row;
2080
                        }
2081
                    }
2082
                    $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
2083
                    $languageMenu->setIdentifier('_langSelector');
2084
                    foreach ($availableLanguages as $language) {
2085
                        $languageId = $language->getLanguageId();
2086
                        $selectorOptionLabel = $language->getTitle();
2087
                        // Create url for creating a localized record
2088
                        $addOption = true;
2089
                        $href = '';
2090
                        if (!isset($rowsByLang[$languageId])) {
2091
                            // Translation in this language does not exist
2092
                            $selectorOptionLabel .= ' [' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . ']';
2093
                            $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
2094
                                'justLocalized' => $table . ':' . $rowsByLang[0]['uid'] . ':' . $languageId,
2095
                                'returnUrl' => $this->retUrl
2096
                            ]);
2097
2098
                            if (array_key_exists(0, $rowsByLang)) {
2099
                                $href = BackendUtility::getLinkToDataHandlerAction(
2100
                                    '&cmd[' . $table . '][' . $rowsByLang[0]['uid'] . '][localize]=' . $languageId,
2101
                                    $redirectUrl
2102
                                );
2103
                            } else {
2104
                                $addOption = false;
2105
                            }
2106
                        } else {
2107
                            $params = [
2108
                                'edit[' . $table . '][' . $rowsByLang[$languageId]['uid'] . ']' => 'edit',
2109
                                'returnUrl' => $this->retUrl
2110
                            ];
2111
                            if ($table === 'pages') {
2112
                                // Disallow manual adjustment of the language field for pages
2113
                                $params['overrideVals'] = [
2114
                                    'pages' => [
2115
                                        'sys_language_uid' => $languageId
2116
                                    ]
2117
                                ];
2118
                            }
2119
                            $href = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params);
2120
                        }
2121
                        if ($addOption) {
2122
                            $menuItem = $languageMenu->makeMenuItem()
2123
                                ->setTitle($selectorOptionLabel)
2124
                                ->setHref($href);
2125
                            if ($languageId === $currentLanguage) {
2126
                                $menuItem->setActive(true);
2127
                            }
2128
                            $languageMenu->addMenuItem($menuItem);
2129
                        }
2130
                    }
2131
                    $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu);
2132
                }
2133
            }
2134
        }
2135
    }
2136
2137
    /**
2138
     * Redirects to FormEngine with new parameters to edit a just created localized record
2139
     *
2140
     * @param ServerRequestInterface $request Incoming request object
2141
     * @return ResponseInterface|null Possible redirect response
2142
     */
2143
    protected function localizationRedirect(ServerRequestInterface $request): ?ResponseInterface
2144
    {
2145
        $justLocalized = $request->getQueryParams()['justLocalized'];
2146
2147
        if (empty($justLocalized)) {
2148
            return null;
2149
        }
2150
2151
        [$table, $origUid, $language] = explode(':', $justLocalized);
2152
2153
        if ($GLOBALS['TCA'][$table]
2154
            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
2155
            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
2156
        ) {
2157
            $parsedBody = $request->getParsedBody();
2158
            $queryParams = $request->getQueryParams();
2159
2160
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
2161
            $queryBuilder->getRestrictions()
2162
                ->removeAll()
2163
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2164
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
2165
            $localizedRecord = $queryBuilder->select('uid')
2166
                ->from($table)
2167
                ->where(
2168
                    $queryBuilder->expr()->eq(
2169
                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
2170
                        $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
2171
                    ),
2172
                    $queryBuilder->expr()->eq(
2173
                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
2174
                        $queryBuilder->createNamedParameter($origUid, \PDO::PARAM_INT)
2175
                    )
2176
                )
2177
                ->execute()
2178
                ->fetch();
2179
            $returnUrl = $parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '';
2180
            if (is_array($localizedRecord)) {
2181
                // Create redirect response to self to edit just created record
2182
                return new RedirectResponse(
2183
                    (string)$this->uriBuilder->buildUriFromRoute(
2184
                        'record_edit',
2185
                        [
2186
                            'edit[' . $table . '][' . $localizedRecord['uid'] . ']' => 'edit',
2187
                            'returnUrl' => GeneralUtility::sanitizeLocalUrl($returnUrl)
2188
                        ]
2189
                    ),
2190
                    303
2191
                );
2192
            }
2193
        }
2194
        return null;
2195
    }
2196
2197
    /**
2198
     * Returns languages  available for record translations on given page.
2199
     *
2200
     * @param int $id Page id: If zero, the query will select all sys_language records from root level which are NOT
2201
     *                hidden. If set to another value, the query will select all sys_language records that has a
2202
     *                translation record on that page (and is not hidden, unless you are admin user)
2203
     * @param string $table For pages we want all languages, for other records the languages of the page translations
2204
     * @return SiteLanguage[] Language
2205
     */
2206
    protected function getLanguages(int $id, string $table): array
2207
    {
2208
        // This usually happens when a non-pages record is added after another, so we are fetching the proper page ID
2209
        if ($id < 0 && $table !== 'pages') {
2210
            $pageId = $this->pageinfo['uid'] ?? null;
2211
            if ($pageId !== null) {
2212
                $pageId = (int)$pageId;
2213
            } else {
2214
                $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

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