EditDocumentController.php 103 KB
Newer Older
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
9
10
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
11
 *
12
13
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
16
 * The TYPO3 project - inspiring people to share!
 */
17

18
19
namespace TYPO3\CMS\Backend\Controller;

20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use Psr\Http\Message\ResponseInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
24
use TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent;
use TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent;
25
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
26
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
27
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordWorkspaceDeletePlaceholderException;
28
29
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
30
use TYPO3\CMS\Backend\Form\FormResultCompiler;
31
use TYPO3\CMS\Backend\Form\NodeFactory;
32
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
33
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
34
use TYPO3\CMS\Backend\Routing\UriBuilder;
35
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
36
use TYPO3\CMS\Backend\Template\ModuleTemplate;
Nicole Cordes's avatar
Nicole Cordes committed
37
use TYPO3\CMS\Backend\Utility\BackendUtility;
38
use TYPO3\CMS\Core\Database\ConnectionPool;
39
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
40
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
41
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
42
use TYPO3\CMS\Core\Database\ReferenceIndex;
43
use TYPO3\CMS\Core\DataHandling\DataHandler;
44
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
45
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
46
use TYPO3\CMS\Core\Http\HtmlResponse;
47
use TYPO3\CMS\Core\Http\RedirectResponse;
48
use TYPO3\CMS\Core\Imaging\Icon;
49
50
51
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Page\PageRenderer;
52
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
53
use TYPO3\CMS\Core\Site\Entity\NullSite;
54
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
55
use TYPO3\CMS\Core\Site\SiteFinder;
56
use TYPO3\CMS\Core\Type\Bitmask\Permission;
Nicole Cordes's avatar
Nicole Cordes committed
57
58
59
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
60
use TYPO3\CMS\Core\Utility\PathUtility;
61
use TYPO3\CMS\Core\Versioning\VersionState;
Nicole Cordes's avatar
Nicole Cordes committed
62

63
/**
64
65
66
67
68
 * Main backend controller almost always used if some database record is edited in the backend.
 *
 * Main job of this controller is to evaluate and sanitize $request parameters,
 * call the DataHandler if records should be created or updated and
 * execute FormEngine for record rendering.
69
 */
70
class EditDocumentController
71
{
72
73
74
75
76
    protected const DOCUMENT_CLOSE_MODE_DEFAULT = 0;
    // works like DOCUMENT_CLOSE_MODE_DEFAULT
    protected const DOCUMENT_CLOSE_MODE_REDIRECT = 1;
    protected const DOCUMENT_CLOSE_MODE_CLEAR_ALL = 3;
    protected const DOCUMENT_CLOSE_MODE_NO_REDIRECT = 4;
77
78
79
80

    /**
     * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
     *
81
     * @var array<string,array>
82
83
     */
    protected $editconf = [];
84
85

    /**
86
87
     * Comma list of field names to edit. If specified, only those fields will be rendered.
     * Otherwise all (available) fields in the record are shown according to the TCA type.
88
     *
89
     * @var string|null
90
     */
91
    protected $columnsOnly;
92
93

    /**
94
     * Default values for fields
95
     *
96
     * @var array|null [table][field]
97
     */
98
    protected $defVals;
99
100

    /**
101
     * Array of values to force being set as hidden fields in FormEngine
102
     *
103
     * @var array|null [table][field]
104
     */
105
    protected $overrideVals;
106
107

    /**
108
109
110
111
112
113
114
115
116
117
118
     * If set, this value will be set in $this->retUrl as "returnUrl", if not,
     * $this->retUrl will link to dummy controller
     *
     * @var string|null
     */
    protected $returnUrl;

    /**
     * Prepared return URL. Contains the URL that we should return to from FormEngine if
     * close button is clicked. Usually passed along as 'returnUrl', but falls back to
     * "dummy" controller.
119
120
121
     *
     * @var string
     */
122
    protected $retUrl;
123
124

    /**
125
     * Close document command. One of the DOCUMENT_CLOSE_MODE_* constants above
126
127
128
     *
     * @var int
     */
129
    protected $closeDoc;
130
131

    /**
132
133
134
     * If true, the processing of incoming data will be performed as if a save-button is pressed.
     * Used in the forms as a hidden field which can be set through
     * JavaScript if the form is somehow submitted by JavaScript.
135
136
137
     *
     * @var bool
     */
138
    protected $doSave;
139
140

    /**
141
     * Main DataHandler datamap array
142
143
144
     *
     * @var array
     */
145
    protected $data;
146
147

    /**
148
149
150
     * Main DataHandler cmdmap array
     *
     * @var array
151
     */
152
    protected $cmd;
153
154

    /**
155
156
     * DataHandler 'mirror' input
     *
157
158
     * @var array
     */
159
    protected $mirror;
160
161
162
163
164
165
166

    /**
     * Boolean: If set, then the GET var "&id=" will be added to the
     * retUrl string so that the NEW id of something is returned to the script calling the form.
     *
     * @var bool
     */
167
    protected $returnNewPageId = false;
168
169

    /**
170
171
     * Updated values for backendUser->uc. Used for new inline records to mark them
     * as expanded: uc[inlineView][...]
172
     *
173
     * @var array|null
174
     */
175
    protected $uc;
176
177

    /**
178
     * ID for displaying the page in the frontend, "save and view"
179
180
181
     *
     * @var int
     */
182
    protected $popViewId;
183
184
185
186
187
188

    /**
     * Alternative URL for viewing the frontend pages.
     *
     * @var string
     */
189
    protected $viewUrl;
190

191
192
193
194
195
    /**
     * @var string|null
     */
    protected $previewCode;

196
197
198
199
200
    /**
     * Alternative title for the document handler.
     *
     * @var string
     */
201
    protected $recTitle;
202
203

    /**
204
     * If set, then no save & view button is printed
205
206
207
     *
     * @var bool
     */
208
    protected $noView;
209
210
211
212

    /**
     * @var string
     */
213
    protected $perms_clause;
214
215

    /**
216
     * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
217
218
219
     *
     * @var bool
     */
220
    protected $returnEditConf;
221
222

    /**
223
     * parse_url() of current requested URI, contains ['path'] and ['query'] parts.
224
225
226
     *
     * @var array
     */
227
    protected $R_URL_parts;
228
229

    /**
230
231
     * Contains $request query parameters. This array is the foundation for creating
     * the R_URI internal var which becomes the url to which forms are submitted
232
233
234
     *
     * @var array
     */
235
    protected $R_URL_getvars;
236
237

    /**
238
     * Set to the URL of this script including variables which is needed to re-display the form.
239
240
241
     *
     * @var string
     */
242
    protected $R_URI;
243
244
245
246

    /**
     * @var array
     */
247
    protected $pageinfo;
248
249

    /**
250
251
     * Is loaded with the "title" of the currently "open document"
     * used for the open document toolbar
252
253
254
     *
     * @var string
     */
255
    protected $storeTitle = '';
256
257
258

    /**
     * Contains an array with key/value pairs of GET parameters needed to reach the
259
     * current document displayed - used in the 'open documents' toolbar.
260
261
262
     *
     * @var array
     */
263
    protected $storeArray;
264
265

    /**
266
     * $this->storeArray imploded to url
267
268
269
     *
     * @var string
     */
270
    protected $storeUrl;
271
272

    /**
273
     * md5 hash of storeURL, used to identify a single open document in backend user uc
274
275
276
     *
     * @var string
     */
277
    protected $storeUrlMd5;
278
279

    /**
280
     * Backend user session data of this module
281
282
283
     *
     * @var array
     */
284
    protected $docDat;
285
286
287
288

    /**
     * An array of the "open documents" - keys are md5 hashes (see $storeUrlMd5) identifying
     * the various documents on the GET parameter list needed to open it. The values are
289
     * arrays with 0,1,2 keys with information about the document (see compileStoreData()).
290
291
292
293
     * The docHandler variable is stored in the $docDat session data, key "0".
     *
     * @var array
     */
294
    protected $docHandler;
295
296
297
298
299
300

    /**
     * Array of the elements to create edit forms for.
     *
     * @var array
     */
301
    protected $elementsData;
302
303
304
305
306
307

    /**
     * Pointer to the first element in $elementsData
     *
     * @var array
     */
308
    protected $firstEl;
309
310
311
312
313
314

    /**
     * Counter, used to count the number of errors (when users do not have edit permissions)
     *
     * @var int
     */
315
    protected $errorC;
316
317
318
319
320
321

    /**
     * Counter, used to count the number of new record forms displayed
     *
     * @var int
     */
322
    protected $newC;
323
324
325
326
327
328
329

    /**
     * Is set to the pid value of the last shown record - thus indicating which page to
     * show when clicking the SAVE/VIEW button
     *
     * @var int
     */
330
    protected $viewId;
331
332
333
334
335
336

    /**
     * Is set to additional parameters (like "&L=xxx") if the record supports it.
     *
     * @var string
     */
337
    protected $viewId_addParams;
338
339
340
341
342
343
344
345
346

    /**
     * @var FormResultCompiler
     */
    protected $formResultCompiler;

    /**
     * Used internally to disable the storage of the document reference (eg. new records)
     *
347
     * @var int
348
     */
349
    protected $dontStoreDocumentRef = 0;
350
351
352
353
354
355
356
357

    /**
     * Stores information needed to preview the currently saved record
     *
     * @var array
     */
    protected $previewData = [];

358
359
360
361
362
363
364
    /**
     * ModuleTemplate object
     *
     * @var ModuleTemplate
     */
    protected $moduleTemplate;

365
366
367
368
369
370
371
372
373
374
375
376
377
378
    /**
     * Check if a record has been saved
     *
     * @var bool
     */
    protected $isSavedRecord;

    /**
     * Check if a page in free translation mode
     *
     * @var bool
     */
    protected $isPageInFreeTranslationMode = false;

379
    /**
380
     * @var EventDispatcherInterface
381
     */
382
383
    protected $eventDispatcher;

384
385
386
387
388
    /**
     * @var UriBuilder
     */
    protected $uriBuilder;

389
    public function __construct(EventDispatcherInterface $eventDispatcher)
390
    {
391
        $this->eventDispatcher = $eventDispatcher;
392
        $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
393
        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
394
        $this->moduleTemplate->setUiBlock(true);
395
        $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
396
397
398
    }

    /**
399
     * Main dispatcher entry method registered as "record_edit" end point
400
     *
401
402
     * @param ServerRequestInterface $request the current request
     * @return ResponseInterface the response with the content
403
     */
404
    public function mainAction(ServerRequestInterface $request): ResponseInterface
405
    {
406
407
408
409
        // Unlock all locked records
        BackendUtility::lockRecords();
        if ($response = $this->preInit($request)) {
            return $response;
410
        }
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429

        // Process incoming data via DataHandler?
        $parsedBody = $request->getParsedBody();
        if ($this->doSave
            || isset($parsedBody['_savedok'])
            || isset($parsedBody['_saveandclosedok'])
            || isset($parsedBody['_savedokview'])
            || isset($parsedBody['_savedoknew'])
            || isset($parsedBody['_duplicatedoc'])
        ) {
            if ($response = $this->processData($request)) {
                return $response;
            }
        }

        $this->init($request);
        $this->main($request);

        return new HtmlResponse($this->moduleTemplate->renderContent());
430
431
432
    }

    /**
433
     * First initialization, always called, even before processData() executes DataHandler processing.
434
     *
435
     * @param ServerRequestInterface $request
436
     * @return ResponseInterface Possible redirect response
437
     */
438
    protected function preInit(ServerRequestInterface $request): ?ResponseInterface
439
    {
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
        if ($response = $this->localizationRedirect($request)) {
            return $response;
        }

        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

        $this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
        $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
        $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
        $this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
        $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null);
        $this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
        $this->doSave = (bool)($parsedBody['doSave'] ?? $queryParams['doSave'] ?? false);
        $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
        $this->uc = $parsedBody['uc'] ?? $queryParams['uc'] ?? null;

        // Set overrideVals as default values if defVals does not exist.
458
        // @todo: Why?
459
460
461
        if (!is_array($this->defVals) && is_array($this->overrideVals)) {
            $this->defVals = $this->overrideVals;
        }
462
        $this->addSlugFieldsToColumnsOnly($queryParams);
463
464

        // Set final return URL
465
        $this->retUrl = $this->returnUrl ?: (string)$this->uriBuilder->buildUriFromRoute('dummy');
466
467

        // Change $this->editconf if versioning applies to any of the records
468
        $this->fixWSversioningInEditConf();
469
470
471
472

        // Prepare R_URL (request url)
        $this->R_URL_parts = parse_url($request->getAttribute('normalizedParams')->getRequestUri());
        $this->R_URL_getvars = $queryParams;
473
        $this->R_URL_getvars['edit'] = $this->editconf;
474
475
476
477

        // Prepare 'open documents' url, this is later modified again various times
        $this->compileStoreData();
        // Backend user session data of this module
478
479
        $this->docDat = $this->getBackendUser()->getModuleData('FormEngine', 'ses');
        $this->docHandler = $this->docDat[0];
480
481

        // Close document if a request for closing the document has been sent
482
        if ((int)$this->closeDoc > self::DOCUMENT_CLOSE_MODE_DEFAULT) {
483
484
485
            if ($response = $this->closeDocument($this->closeDoc, $request)) {
                return $response;
            }
486
487
        }

488
489
        $event = new BeforeFormEnginePageInitializedEvent($this, $request);
        $this->eventDispatcher->dispatch($event);
490
        return null;
491
492
    }

493
494
495
496
497
498
499
500
501
502
503
504
505
    /**
     * Always add required fields of slug field
     *
     * @param array $queryParams
     */
    protected function addSlugFieldsToColumnsOnly(array $queryParams): void
    {
        $data = $queryParams['edit'] ?? [];
        $data = array_keys($data);
        $table = reset($data);
        if ($this->columnsOnly && $table !== false && isset($GLOBALS['TCA'][$table])) {
            $fields = GeneralUtility::trimExplode(',', $this->columnsOnly, true);
            foreach ($fields as $field) {
506
507
508
509
510
511
                $postModifiers = $GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['postModifiers'] ?? [];
                if (isset($GLOBALS['TCA'][$table]['columns'][$field])
                    && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'slug'
                    && (!is_array($postModifiers) || $postModifiers === [])
                ) {
                    foreach ($GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['fields'] ?? [] as $fields) {
512
513
514
515
516
517
518
                        $this->columnsOnly .= ',' . (is_array($fields) ? implode(',', $fields) : $fields);
                    }
                }
            }
        }
    }

519
    /**
520
521
     * Do processing of data, submitting it to DataHandler. May return a RedirectResponse
     *
522
     * @param ServerRequestInterface $request
523
     * @return ResponseInterface|null
524
     */
525
    protected function processData(ServerRequestInterface $request): ?ResponseInterface
526
    {
527
528
529
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

530
        $beUser = $this->getBackendUser();
531
532
533
534
535
536
537
538

        // Processing related GET / POST vars
        $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];
        $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];
        $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];
        $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);

        // Only options related to $this->data submission are included here
539
540
        $tce = GeneralUtility::makeInstance(DataHandler::class);

541
542
543
544
        $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);

        // Set internal vars
        if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {
545
546
            $tce->neverHideAtCopy = 1;
        }
547
548
549
550
551
552

        // Set default values fetched previously from GET / POST vars
        if (is_array($this->defVals) && $this->defVals !== [] && is_array($tce->defaultValues)) {
            $tce->defaultValues = array_merge_recursive($this->defVals, $tce->defaultValues);
        }

553
        // Load DataHandler with data
554
555
556
557
        $tce->start($this->data, $this->cmd);
        if (is_array($this->mirror)) {
            $tce->setMirror($this->mirror);
        }
558
559

        // Perform the saving operation with DataHandler:
560
561
562
563
        if ($this->doSave === true) {
            $tce->process_datamap();
            $tce->process_cmdmap();
        }
564
565
566
        // If pages are being edited, we set an instruction about updating the page tree after this operation.
        if ($tce->pagetreeNeedsRefresh
            && (isset($this->data['pages']) || $beUser->workspace != 0 && !empty($this->data))
567
        ) {
568
569
570
571
            BackendUtility::setUpdateSignal('updatePageTree');
        }
        // If there was saved any new items, load them:
        if (!empty($tce->substNEWwithIDs_table)) {
572
            // Save the expanded/collapsed states for new inline records, if any
573
574
575
576
577
578
579
580
581
582
583
            FormEngineUtility::updateInlineView($this->uc, $tce);
            $newEditConf = [];
            foreach ($this->editconf as $tableName => $tableCmds) {
                $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
                if (!empty($keys)) {
                    foreach ($keys as $key) {
                        $editId = $tce->substNEWwithIDs[$key];
                        // Check if the $editId isn't a child record of an IRRE action
                        if (!(is_array($tce->newRelatedIDs[$tableName])
                            && in_array($editId, $tce->newRelatedIDs[$tableName]))
                        ) {
584
                            // Translate new id to the workspace version
585
586
587
588
589
590
591
                            if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
                                $beUser->workspace,
                                $tableName,
                                $editId,
                                'uid'
                            )) {
                                $editId = $versionRec['uid'];
592
                            }
593
594
                            $newEditConf[$tableName][$editId] = 'edit';
                        }
595
                        // Traverse all new records and forge the content of ->editconf so we can continue to edit these records!
596
                        if ($tableName === 'pages'
597
598
                            && $this->retUrl !== (string)$this->uriBuilder->buildUriFromRoute('dummy')
                            && $this->retUrl !== $this->getCloseUrl()
599
600
601
                            && $this->returnNewPageId
                        ) {
                            $this->retUrl .= '&id=' . $tce->substNEWwithIDs[$key];
602
603
                        }
                    }
604
605
                } else {
                    $newEditConf[$tableName] = $tableCmds;
606
607
                }
            }
608
            // Reset editconf if newEditConf has values
609
610
611
612
613
            if (!empty($newEditConf)) {
                $this->editconf = $newEditConf;
            }
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
            $this->R_URL_getvars['edit'] = $this->editconf;
614
            // Unset default values since we don't need them anymore.
615
            unset($this->R_URL_getvars['defVals']);
616
617
            // Recompile the store* values since editconf changed
            $this->compileStoreData();
618
619
620
621
622
623
        }
        // See if any records was auto-created as new versions?
        if (!empty($tce->autoVersionIdMap)) {
            $this->fixWSversioningInEditConf($tce->autoVersionIdMap);
        }
        // If a document is saved and a new one is created right after.
624
625
626
627
628
        if (isset($parsedBody['_savedoknew']) && is_array($this->editconf)) {
            if ($redirect = $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request)) {
                return $redirect;
            }
            // Find the current table
629
            reset($this->editconf);
630
            $nTable = (string)key($this->editconf);
631
632
            // Finding the first id, getting the records pid+uid
            reset($this->editconf[$nTable]);
633
            $nUid = (int)key($this->editconf[$nTable]);
634
            $recordFields = 'pid,uid';
635
            if (BackendUtility::isTableWorkspaceEnabled($nTable)) {
636
                $recordFields .= ',t3ver_oid';
637
            }
638
            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);
639
640
641
            // Determine insertion mode: 'top' is self-explaining,
            // otherwise new elements are inserted after one using a negative uid
            $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
642
643
644
            // Setting a blank editconf array for a new record:
            $this->editconf = [];
            // Determine related page ID for regular live context
645
            if ((int)$nRec['t3ver_oid'] === 0) {
646
647
648
649
                if ($insertRecordOnTop) {
                    $relatedPageId = $nRec['pid'];
                } else {
                    $relatedPageId = -$nRec['uid'];
650
                }
651
652
653
654
655
656
            } else {
                // Determine related page ID for workspace context
                if ($insertRecordOnTop) {
                    // Fetch live version of workspace version since the pid value is always -1 in workspaces
                    $liveRecord = BackendUtility::getRecord($nTable, $nRec['t3ver_oid'], $recordFields);
                    $relatedPageId = $liveRecord['pid'];
657
                } else {
658
659
                    // Use uid of live version of workspace version
                    $relatedPageId = -$nRec['t3ver_oid'];
660
661
                }
            }
662
663
664
            $this->editconf[$nTable][$relatedPageId] = 'new';
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
            $this->R_URL_getvars['edit'] = $this->editconf;
665
666
            // Recompile the store* values since editconf changed...
            $this->compileStoreData();
667
        }
668
        // If a document should be duplicated.
669
670
671
        if (isset($parsedBody['_duplicatedoc']) && is_array($this->editconf)) {
            $this->closeDocument(self::DOCUMENT_CLOSE_MODE_NO_REDIRECT, $request);
            // Find current table
672
            reset($this->editconf);
673
            $nTable = (string)key($this->editconf);
674
            // Find the first id, getting the records pid+uid
675
676
677
678
679
680
681
            reset($this->editconf[$nTable]);
            $nUid = key($this->editconf[$nTable]);
            if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
                $nUid = $tce->substNEWwithIDs[$nUid];
            }

            $recordFields = 'pid,uid';
682
            if (!BackendUtility::isTableWorkspaceEnabled($nTable)) {
683
684
685
686
687
688
689
                $recordFields .= ',t3ver_oid';
            }
            $nRec = BackendUtility::getRecord($nTable, $nUid, $recordFields);

            // Setting a blank editconf array for a new record:
            $this->editconf = [];

690
            if ((int)$nRec['t3ver_oid'] === 0) {
691
692
693
694
695
                $relatedPageId = -$nRec['uid'];
            } else {
                $relatedPageId = -$nRec['t3ver_oid'];
            }

696
            /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $duplicateTce */
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
            $duplicateTce = GeneralUtility::makeInstance(DataHandler::class);

            $duplicateCmd = [
                $nTable => [
                    $nUid => [
                        'copy' => $relatedPageId
                    ]
                ]
            ];

            $duplicateTce->start([], $duplicateCmd);
            $duplicateTce->process_cmdmap();

            $duplicateMappingArray = $duplicateTce->copyMappingArray;
            $duplicateUid = $duplicateMappingArray[$nTable][$nUid];

            if ($nTable === 'pages') {
                BackendUtility::setUpdateSignal('updatePageTree');
            }

            $this->editconf[$nTable][$duplicateUid] = 'edit';
            // Finally, set the editconf array in the "getvars" so they will be passed along in URLs as needed.
            $this->R_URL_getvars['edit'] = $this->editconf;
720
721
            // Recompile the store* values since editconf changed...
            $this->compileStoreData();
722
723
724
725

            // Inform the user of the duplication
            $flashMessage = GeneralUtility::makeInstance(
                FlashMessage::class,
726
                $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'),
727
728
729
730
731
732
733
                '',
                FlashMessage::OK
            );
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
            $defaultFlashMessageQueue->enqueue($flashMessage);
        }
734
        // If a preview is requested
735
        if (isset($parsedBody['_savedokview'])) {
736
            $array_keys = array_keys($this->data);
737
            // Get the first table and id of the data array from DataHandler
738
739
740
            $table = reset($array_keys);
            $array_keys = array_keys($this->data[$table]);
            $id = reset($array_keys);
741
742
            if (!MathUtility::canBeInterpretedAsInteger($id)) {
                $id = $tce->substNEWwithIDs[$id];
743
            }
744
745
746
            // Store this information for later use
            $this->previewData['table'] = $table;
            $this->previewData['id'] = $id;
747
        }
748
749
        $tce->printLogErrorMessages();

750
        if ((int)$this->closeDoc < self::DOCUMENT_CLOSE_MODE_DEFAULT
751
            || isset($parsedBody['_saveandclosedok'])
752
        ) {
753
            // Redirect if element should be closed after save
Christian Kuhn's avatar
Christian Kuhn committed
754
            return $this->closeDocument((int)abs($this->closeDoc), $request);
755
        }
756
        return null;
757
758
759
    }

    /**
760
761
     * Initialize the view part of the controller logic.
     *
762
     * @param ServerRequestInterface $request
763
     */
764
    protected function init(ServerRequestInterface $request): void
765
    {
766
767
768
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

769
        $beUser = $this->getBackendUser();
770
771
772
773
774

        $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);
        $this->viewUrl = (string)($parsedBody['viewUrl'] ?? $queryParams['viewUrl'] ?? '');
        $this->recTitle = (string)($parsedBody['recTitle'] ?? $queryParams['recTitle'] ?? '');
        $this->noView = (bool)($parsedBody['noView'] ?? $queryParams['noView'] ?? false);
775
        $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
776
777
        // Set other internal variables:
        $this->R_URL_getvars['returnUrl'] = $this->retUrl;
778
        $this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
779
780

        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
781
        $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
782

783
784
785
        if (isset($parsedBody['_savedokview']) && $this->popViewId) {
            $this->previewCode = $this->generatePreviewCode();
        }
786
        // Set context sensitive menu
787
        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
788

789
790
        $event = new AfterFormEnginePageInitializedEvent($this, $request);
        $this->eventDispatcher->dispatch($event);
791
792
793
    }

    /**
794
     * Generates markup for immediate action dispatching.
795
     *
796
797
     * @return string
     */
798
    protected function generatePreviewCode(): string
799
    {
800
801
        $previewPageId = $this->getPreviewPageId();
        $anchorSection = $this->getPreviewUrlAnchorSection();
802
803
        $previewPageRootLine = BackendUtility::BEgetRootLine($previewPageId);
        $previewUrlParameters = $this->getPreviewUrlParameters($previewPageId);
804

805
806
807
808
809
        return PreviewUriBuilder::create($previewPageId, $this->viewUrl)
            ->withRootLine($previewPageRootLine)
            ->withSection($anchorSection)
            ->withAdditionalQueryParameters($previewUrlParameters)
            ->buildImmediateActionElement([PreviewUriBuilder::OPTION_SWITCH_FOCUS => null]);
810
    }
811

812
813
814
815
816
817
818
819
    /**
     * Returns the parameters for the preview URL
     *
     * @param int $previewPageId
     * @return string
     */
    protected function getPreviewUrlParameters(int $previewPageId): string
    {
820
        $linkParameters = [];
821
822
        $table = $this->previewData['table'] ?: $this->firstEl['table'];
        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
823
        $previewConfiguration = BackendUtility::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
824
        $recordArray = BackendUtility::getRecord($table, $recordId);
825
826

        // language handling
827
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? '';
828
        if ($languageField && !empty($recordArray[$languageField])) {
829
            $recordId = $this->resolvePreviewRecordId($table, $recordArray, $previewConfiguration);
830
831
832
833
            $language = $recordArray[$languageField];
            if ($language > 0) {
                $linkParameters['L'] = $language;
            }
834
835
        }

836
        // Always use live workspace record uid for the preview
Daniel Dorndorf's avatar
Daniel Dorndorf committed
837
        if (BackendUtility::isTableWorkspaceEnabled($table) && $recordArray['t3ver_oid'] > 0) {
838
839
840
            $recordId = $recordArray['t3ver_oid'];
        }

841
842
843
844
845
846
847
848
849
850
851
852
853
854
        // map record data to GET parameters
        if (isset($previewConfiguration['fieldToParameterMap.'])) {
            foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
                $value = $recordArray[$field];
                if ($field === 'uid') {
                    $value = $recordId;
                }
                $linkParameters[$parameterName] = $value;
            }
        }

        // add/override parameters by configuration
        if (isset($previewConfiguration['additionalGetParameters.'])) {
            $additionalGetParameters = [];
855
856
857
858
            $this->parseAdditionalGetParameters(
                $additionalGetParameters,
                $previewConfiguration['additionalGetParameters.']
            );
859
860
861
            $linkParameters = array_replace($linkParameters, $additionalGetParameters);
        }

862
        return HttpUtility::buildQueryString($linkParameters, '&');
863
    }
864

865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
    /**
     * @param string $table
     * @param array $recordArray
     * @param array $previewConfiguration
     *
     * @return int
     */
    protected function resolvePreviewRecordId(string $table, array $recordArray, array $previewConfiguration): int
    {
        $l10nPointer = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
        if ($l10nPointer
            && !empty($recordArray[$l10nPointer])
            && (
                // not set -> default to true
                !isset($previewConfiguration['useDefaultLanguageRecord'])
                // or set -> use value
                || $previewConfiguration['useDefaultLanguageRecord']
            )
        ) {
884
            return (int)$recordArray[$l10nPointer];
885
        }
886
        return (int)$recordArray['uid'];
887
888
    }

889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
    /**
     * Returns the anchor section for the preview url
     *
     * @return string
     */
    protected function getPreviewUrlAnchorSection(): string
    {
        $table = $this->previewData['table'] ?: $this->firstEl['table'];
        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];

        return $table === 'tt_content' ? '#c' . (int)$recordId : '';
    }

    /**
     * Returns the preview page id
     *
     * @return int
     */
    protected function getPreviewPageId(): int
    {
        $previewPageId = 0;
        $table = $this->previewData['table'] ?: $this->firstEl['table'];
        $recordId = $this->previewData['id'] ?: $this->firstEl['uid'];
        $pageId = $this->popViewId ?: $this->viewId;

        if ($table === 'pages') {
            $currentPageId = (int)$recordId;
        } else {
            $currentPageId = MathUtility::convertToPositiveInteger($pageId);
        }

920
        $previewConfiguration = BackendUtility::getPagesTSconfig($currentPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
921
922

        if (isset($previewConfiguration['previewPageId'])) {
923
            $previewPageId = (int)$previewConfiguration['previewPageId'];
924
925
926
927
928
929
930
931
932
933
934
        }
        // if no preview page was configured
        if (!$previewPageId) {
            $rootPageData = null;
            $rootLine = BackendUtility::BEgetRootLine($currentPageId);
            $currentPage = reset($rootLine);
            // Allow all doktypes below 200
            // This makes custom doktype work as well with opening a frontend page.
            if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) {
                // try the current page
                $previewPageId = $currentPageId;
935
            } else {
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
                // or search for the root page
                foreach ($rootLine as $page) {
                    if ($page['is_siteroot']) {
                        $rootPageData = $page;
                        break;
                    }
                }
                $previewPageId = isset($rootPageData)
                    ? (int)$rootPageData['uid']
                    : $currentPageId;
            }
        }

        $this->popViewId = $previewPageId;

        return $previewPageId;
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
    }

    /**
     * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a plain array
     *
     * This basically removes the trailing dots of sub-array keys in TypoScript.
     * The result can be used to create a query string with GeneralUtility::implodeArrayForUrl().
     *
     * @param array $parameters Should be an empty array by default
     * @param array $typoScript The TypoScript configuration
     */
    protected function parseAdditionalGetParameters(array &$parameters, array $typoScript)
    {
        foreach ($typoScript as $key => $value) {
            if (is_array($value)) {
                $key = rtrim($key, '.');
                $parameters[$key] = [];
                $this->parseAdditionalGetParameters($parameters[$key], $value);
            } else {
                $parameters[$key] = $value;
            }
        }
    }

    /**
     * Main module operation
978
     *
979
     * @param ServerRequestInterface $request
980
     */
981
    protected function main(ServerRequestInterface $request): void
982
    {
983
        $body = $this->previewCode ?? '';
984
        // Begin edit
985
986
987
988
989
990
991
        if (is_array($this->editconf)) {
            $this->formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);

            // Creating the editing form, wrap it with buttons, document selector etc.
            $editForm = $this->makeEditForm();
            if ($editForm) {
                $this->firstEl = reset($this->elementsData);
992
                // Checking if the currently open document is stored in the list of "open documents" - if not, add it:
993
                if (($this->docDat[1] !== $this->storeUrlMd5 || !isset($this->docHandler[$this->storeUrlMd5]))
994
995
                    && !$this->dontStoreDocumentRef
                ) {