Passed
Push — master ( 2ed7e8...9bc0b7 )
by
unknown
18:09
created

RemoteServer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Workspaces\Controller\Remote;
17
18
use TYPO3\CMS\Backend\Backend\Avatar\Avatar;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Imaging\Icon;
23
use TYPO3\CMS\Core\Imaging\IconFactory;
24
use TYPO3\CMS\Core\Localization\LanguageService;
25
use TYPO3\CMS\Core\Resource\FileReference;
26
use TYPO3\CMS\Core\Resource\ProcessedFile;
27
use TYPO3\CMS\Core\Utility\DiffUtility;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Core\Utility\MathUtility;
30
use TYPO3\CMS\Core\Utility\PathUtility;
31
use TYPO3\CMS\Core\Utility\StringUtility;
32
use TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord;
33
use TYPO3\CMS\Workspaces\Service\GridDataService;
34
use TYPO3\CMS\Workspaces\Service\HistoryService;
35
use TYPO3\CMS\Workspaces\Service\IntegrityService;
36
use TYPO3\CMS\Workspaces\Service\StagesService;
37
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
38
39
/**
40
 * Class RemoteServer
41
 * @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
42
 */
43
class RemoteServer
44
{
45
    /**
46
     * @var GridDataService
47
     */
48
    protected $gridDataService;
49
50
    /**
51
     * @var StagesService
52
     */
53
    protected $stagesService;
54
55
    /**
56
     * @var WorkspaceService
57
     */
58
    protected $workspaceService;
59
60
    /**
61
     * @var DiffUtility
62
     */
63
    protected $differenceHandler;
64
65
    public function __construct()
66
    {
67
        $this->workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
68
        $this->gridDataService = GeneralUtility::makeInstance(GridDataService::class);
69
        $this->stagesService = GeneralUtility::makeInstance(StagesService::class);
70
    }
71
72
    /**
73
     * Checks integrity of elements before performing actions on them.
74
     *
75
     * @param \stdClass $parameters
76
     * @return array
77
     */
78
    public function checkIntegrity(\stdClass $parameters)
79
    {
80
        $integrity = $this->createIntegrityService($this->getAffectedElements($parameters));
81
        $integrity->check();
82
        $response = [
83
            'result' => $integrity->getStatusRepresentation()
84
        ];
85
        return $response;
86
    }
87
88
    /**
89
     * Get List of workspace changes
90
     *
91
     * @param \stdClass $parameter
92
     * @return array $data
93
     */
94
    public function getWorkspaceInfos($parameter)
95
    {
96
        // To avoid too much work we use -1 to indicate that every page is relevant
97
        $pageId = $parameter->id > 0 ? $parameter->id : -1;
98
        if (!isset($parameter->language) || !MathUtility::canBeInterpretedAsInteger($parameter->language)) {
99
            $parameter->language = null;
100
        }
101
        $versions = $this->workspaceService->selectVersionsInWorkspace(
102
            $this->getCurrentWorkspace(),
103
            -99,
104
            $pageId,
105
            $parameter->depth,
106
            'tables_select',
107
            $parameter->language
108
        );
109
        $data = $this->gridDataService->generateGridListFromVersions($versions, $parameter, $this->getCurrentWorkspace());
110
        return $data;
111
    }
112
113
    /**
114
     * Get List of available workspace actions
115
     *
116
     * @return array $data
117
     */
118
    public function getStageActions()
119
    {
120
        $stages = $this->stagesService->getStagesForWSUser();
121
        $data = [
122
            'total' => count($stages),
123
            'data' => $stages
124
        ];
125
        return $data;
126
    }
127
128
    /**
129
     * Fetch further information to current selected workspace record.
130
     *
131
     * @param \stdClass $parameter
132
     * @return array $data
133
     */
134
    public function getRowDetails($parameter)
135
    {
136
        $diffReturnArray = [];
137
        $liveReturnArray = [];
138
        $diffUtility = $this->getDifferenceHandler();
139
        $liveRecord = (array)BackendUtility::getRecord($parameter->table, $parameter->t3ver_oid);
140
        $versionRecord = (array)BackendUtility::getRecord($parameter->table, $parameter->uid);
141
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
142
        $icon_Live = $iconFactory->getIconForRecord($parameter->table, $liveRecord, Icon::SIZE_SMALL)->render();
143
        $icon_Workspace = $iconFactory->getIconForRecord($parameter->table, $versionRecord, Icon::SIZE_SMALL)->render();
144
        $stagePosition = $this->stagesService->getPositionOfCurrentStage($parameter->stage);
145
        $fieldsOfRecords = array_keys($liveRecord);
146
        foreach ($fieldsOfRecords as $fieldName) {
147
            if (
148
                empty($GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'])
149
            ) {
150
                continue;
151
            }
152
            // Disable internal fields
153
            if (($GLOBALS['TCA'][$parameter->table]['ctrl']['transOrigDiffSourceField'] ?? '') === $fieldName) {
154
                continue;
155
            }
156
            if (($GLOBALS['TCA'][$parameter->table]['ctrl']['origUid'] ?? '') === $fieldName) {
157
                continue;
158
            }
159
            // Get the field's label. If not available, use the field name
160
            $fieldTitle = $this->getLanguageService()->sL(BackendUtility::getItemLabel($parameter->table, $fieldName));
161
            if (empty($fieldTitle)) {
162
                $fieldTitle = $fieldName;
163
            }
164
            // Gets the TCA configuration for the current field
165
            $configuration = $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'];
166
            // check for exclude fields
167
            if ($this->getBackendUser()->isAdmin() || $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['exclude'] == 0 || GeneralUtility::inList($this->getBackendUser()->groupData['non_exclude_fields'], $parameter->table . ':' . $fieldName)) {
168
                // call diff class only if there is a difference
169
                if ($configuration['type'] === 'inline' && $configuration['foreign_table'] === 'sys_file_reference') {
170
                    $useThumbnails = false;
171
                    if (!empty($configuration['overrideChildTca']['columns']['uid_local']['config']['appearance']['elementBrowserAllowed']) && !empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'])) {
172
                        $fileExtensions = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], true);
173
                        $allowedExtensions = GeneralUtility::trimExplode(',', $configuration['overrideChildTca']['columns']['uid_local']['config']['appearance']['elementBrowserAllowed'], true);
174
                        $differentExtensions = array_diff($allowedExtensions, $fileExtensions);
175
                        $useThumbnails = empty($differentExtensions);
176
                    }
177
178
                    $liveFileReferences = (array)BackendUtility::resolveFileReferences(
179
                        $parameter->table,
180
                        $fieldName,
181
                        $liveRecord,
182
                        0
183
                    );
184
                    $versionFileReferences = (array)BackendUtility::resolveFileReferences(
185
                        $parameter->table,
186
                        $fieldName,
187
                        $versionRecord,
188
                        $this->getCurrentWorkspace()
189
                    );
190
                    $fileReferenceDifferences = $this->prepareFileReferenceDifferences(
191
                        $liveFileReferences,
192
                        $versionFileReferences,
193
                        $useThumbnails
194
                    );
195
196
                    if ($fileReferenceDifferences === null) {
197
                        continue;
198
                    }
199
200
                    $diffReturnArray[] = [
201
                        'field' => $fieldName,
202
                        'label' => $fieldTitle,
203
                        'content' => $fileReferenceDifferences['differences']
204
                    ];
205
                    $liveReturnArray[] = [
206
                        'field' => $fieldName,
207
                        'label' => $fieldTitle,
208
                        'content' => $fileReferenceDifferences['live']
209
                    ];
210
                } elseif ((string)$liveRecord[$fieldName] !== (string)$versionRecord[$fieldName]) {
211
                    // Select the human readable values before diff
212
                    $liveRecord[$fieldName] = BackendUtility::getProcessedValue(
213
                        $parameter->table,
214
                        $fieldName,
215
                        $liveRecord[$fieldName],
216
                        0,
217
                        true,
218
                        false,
219
                        $liveRecord['uid']
220
                    );
221
                    $versionRecord[$fieldName] = BackendUtility::getProcessedValue(
222
                        $parameter->table,
223
                        $fieldName,
224
                        $versionRecord[$fieldName],
225
                        0,
226
                        true,
227
                        false,
228
                        $versionRecord['uid']
229
                    );
230
231
                    $diffReturnArray[] = [
232
                        'field' => $fieldName,
233
                        'label' => $fieldTitle,
234
                        'content' => $diffUtility->makeDiffDisplay($liveRecord[$fieldName], $versionRecord[$fieldName])
235
                    ];
236
                    $liveReturnArray[] = [
237
                        'field' => $fieldName,
238
                        'label' => $fieldTitle,
239
                        'content' => $liveRecord[$fieldName]
240
                    ];
241
                }
242
            }
243
        }
244
        // Hook for modifying the difference and live arrays
245
        // (this may be used by custom or dynamically-defined fields)
246
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] ?? [] as $className) {
247
            $hookObject = GeneralUtility::makeInstance($className);
248
            if (method_exists($hookObject, 'modifyDifferenceArray')) {
249
                $hookObject->modifyDifferenceArray($parameter, $diffReturnArray, $liveReturnArray, $diffUtility);
250
            }
251
        }
252
        $commentsForRecord = $this->getCommentsForRecord($parameter->uid, $parameter->table);
253
254
        $historyService = GeneralUtility::makeInstance(HistoryService::class);
255
        $history = $historyService->getHistory($parameter->table, $parameter->t3ver_oid);
256
257
        $prevStage = $this->stagesService->getPrevStage($parameter->stage);
258
        $nextStage = $this->stagesService->getNextStage($parameter->stage);
259
260
        if (isset($prevStage[0])) {
261
            $prevStage = current($prevStage);
0 ignored issues
show
Bug introduced by
It seems like $prevStage can also be of type boolean; however, parameter $array of current() does only seem to accept array|object, 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

261
            $prevStage = current(/** @scrutinizer ignore-type */ $prevStage);
Loading history...
262
        }
263
264
        if (isset($nextStage[0])) {
265
            $nextStage = current($nextStage);
266
        }
267
268
        return [
269
            'total' => 1,
270
            'data' => [
271
                [
272
                    // these parts contain HTML (don't escape)
273
                    'diff' => $diffReturnArray,
274
                    'live_record' => $liveReturnArray,
275
                    'icon_Live' => $icon_Live,
276
                    'icon_Workspace' => $icon_Workspace,
277
                    // this part is already escaped in getCommentsForRecord()
278
                    'comments' => $commentsForRecord,
279
                    // escape/sanitize the others
280
                    'path_Live' => htmlspecialchars(BackendUtility::getRecordPath($liveRecord['pid'], '', 999)),
0 ignored issues
show
Bug introduced by
It seems like TYPO3\CMS\Backend\Utilit...Record['pid'], '', 999) can also be of type array<integer,string>; however, parameter $string of htmlspecialchars() does only seem to accept string, 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

280
                    'path_Live' => htmlspecialchars(/** @scrutinizer ignore-type */ BackendUtility::getRecordPath($liveRecord['pid'], '', 999)),
Loading history...
281
                    'label_Stage' => htmlspecialchars($this->stagesService->getStageTitle($parameter->stage)),
282
                    'label_PrevStage' => $prevStage,
283
                    'label_NextStage' => $nextStage,
284
                    'stage_position' => (int)$stagePosition['position'],
285
                    'stage_count' => (int)$stagePosition['count'],
286
                    'parent' => [
287
                        'table' => htmlspecialchars($parameter->table),
288
                        'uid' => (int)$parameter->uid
289
                    ],
290
                    'history' => [
291
                        'data' => $history,
292
                        'total' => count($history)
293
                    ]
294
                ]
295
            ]
296
        ];
297
    }
298
299
    /**
300
     * Prepares difference view for file references.
301
     *
302
     * @param FileReference[] $liveFileReferences
303
     * @param FileReference[] $versionFileReferences
304
     * @param bool|false $useThumbnails
305
     * @return array|null
306
     */
307
    protected function prepareFileReferenceDifferences(array $liveFileReferences, array $versionFileReferences, $useThumbnails = false)
308
    {
309
        $randomValue = StringUtility::getUniqueId('file');
310
311
        $liveValues = [];
312
        $versionValues = [];
313
        $candidates = [];
314
        $substitutes = [];
315
316
        // Process live references
317
        foreach ($liveFileReferences as $identifier => $liveFileReference) {
318
            $identifierWithRandomValue = $randomValue . '__' . $liveFileReference->getUid() . '__' . $randomValue;
319
            $candidates[$identifierWithRandomValue] = $liveFileReference;
320
            $liveValues[] = $identifierWithRandomValue;
321
        }
322
323
        // Process version references
324
        foreach ($versionFileReferences as $identifier => $versionFileReference) {
325
            $identifierWithRandomValue = $randomValue . '__' . $versionFileReference->getUid() . '__' . $randomValue;
326
            $candidates[$identifierWithRandomValue] = $versionFileReference;
327
            $versionValues[] = $identifierWithRandomValue;
328
        }
329
330
        // Combine values and surround by spaces
331
        // (to reduce the chunks Diff will find)
332
        $liveInformation = ' ' . implode(' ', $liveValues) . ' ';
333
        $versionInformation = ' ' . implode(' ', $versionValues) . ' ';
334
335
        // Return if information has not changed
336
        if ($liveInformation === $versionInformation) {
337
            return null;
338
        }
339
340
        /**
341
         * @var string $identifierWithRandomValue
342
         * @var FileReference $fileReference
343
         */
344
        foreach ($candidates as $identifierWithRandomValue => $fileReference) {
345
            if ($useThumbnails) {
346
                $thumbnailFile = $fileReference->getOriginalFile()->process(
347
                    ProcessedFile::CONTEXT_IMAGEPREVIEW,
348
                    ['width' => 40, 'height' => 40]
349
                );
350
                $thumbnailMarkup = '<img src="' . PathUtility::getAbsoluteWebPath($thumbnailFile->getPublicUrl()) . '" />';
351
                $substitutes[$identifierWithRandomValue] = $thumbnailMarkup;
352
            } else {
353
                $substitutes[$identifierWithRandomValue] = $fileReference->getPublicUrl();
354
            }
355
        }
356
357
        $differences = $this->getDifferenceHandler()->makeDiffDisplay($liveInformation, $versionInformation);
358
        $liveInformation = str_replace(array_keys($substitutes), array_values($substitutes), trim($liveInformation));
359
        $differences = str_replace(array_keys($substitutes), array_values($substitutes), trim($differences));
360
361
        return [
362
            'live' => $liveInformation,
363
            'differences' => $differences
364
        ];
365
    }
366
367
    /**
368
     * Gets an array with all sys_log entries and their comments for the given record uid and table
369
     *
370
     * @param int $uid uid of changed element to search for in log
371
     * @param string $table Name of the record's table
372
     * @return array
373
     */
374
    public function getCommentsForRecord($uid, $table)
375
    {
376
        $sysLogReturnArray = [];
377
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
378
379
        $result = $queryBuilder
380
            ->select('log_data', 'tstamp', 'userid')
381
            ->from('sys_log')
382
            ->where(
383
                $queryBuilder->expr()->eq(
384
                    'action',
385
                    $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT)
386
                ),
387
                $queryBuilder->expr()->eq(
388
                    'details_nr',
389
                    $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT)
390
                ),
391
                $queryBuilder->expr()->eq(
392
                    'tablename',
393
                    $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
394
                ),
395
                $queryBuilder->expr()->eq(
396
                    'recuid',
397
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
398
                )
399
            )
400
            ->orderBy('tstamp', 'DESC')
401
            ->execute();
402
403
        /** @var Avatar $avatar */
404
        $avatar = GeneralUtility::makeInstance(Avatar::class);
405
406
        while ($sysLogRow = $result->fetch()) {
407
            $sysLogEntry = [];
408
            $data = unserialize($sysLogRow['log_data']);
409
            $beUserRecord = BackendUtility::getRecord('be_users', $sysLogRow['userid']);
410
            $sysLogEntry['stage_title'] = htmlspecialchars($this->stagesService->getStageTitle($data['stage']));
411
            $sysLogEntry['user_uid'] = (int)$sysLogRow['userid'];
412
            $sysLogEntry['user_username'] = is_array($beUserRecord) ? htmlspecialchars($beUserRecord['username']) : '';
413
            $sysLogEntry['tstamp'] = htmlspecialchars(BackendUtility::datetime($sysLogRow['tstamp']));
414
            $sysLogEntry['user_comment'] = nl2br(htmlspecialchars($data['comment']));
415
            $sysLogEntry['user_avatar'] = $avatar->render($beUserRecord);
416
            $sysLogReturnArray[] = $sysLogEntry;
417
        }
418
        return $sysLogReturnArray;
419
    }
420
421
    /**
422
     * Gets all available system languages.
423
     *
424
     * @param \stdClass $parameters
425
     * @return array
426
     */
427
    public function getSystemLanguages(\stdClass $parameters)
428
    {
429
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
430
        $systemLanguages = [
431
            [
432
                'uid' => 'all',
433
                'title' => $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:language.allLanguages'),
434
                'icon' => $iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render()
435
            ]
436
        ];
437
        foreach ($this->gridDataService->getSystemLanguages($parameters->pageUid ?? 0) as $id => $systemLanguage) {
438
            if ($id < 0) {
439
                continue;
440
            }
441
            $systemLanguages[] = [
442
                'uid' => $id,
443
                'title' => htmlspecialchars($systemLanguage['title']),
444
                'icon' => $iconFactory->getIcon($systemLanguage['flagIcon'], Icon::SIZE_SMALL)->render()
445
            ];
446
        }
447
        $result = [
448
            'total' => count($systemLanguages),
449
            'data' => $systemLanguages
450
        ];
451
        return $result;
452
    }
453
454
    protected function getBackendUser(): BackendUserAuthentication
455
    {
456
        return $GLOBALS['BE_USER'];
457
    }
458
459
    protected function getLanguageService(): LanguageService
460
    {
461
        return $GLOBALS['LANG'];
462
    }
463
464
    /**
465
     * Gets the difference handler, parsing differences based on sentences.
466
     *
467
     * @return DiffUtility
468
     */
469
    protected function getDifferenceHandler()
470
    {
471
        if (!isset($this->differenceHandler)) {
472
            $this->differenceHandler = GeneralUtility::makeInstance(DiffUtility::class);
473
            $this->differenceHandler->stripTags = false;
474
        }
475
        return $this->differenceHandler;
476
    }
477
478
    /**
479
     * Creates a new instance of the integrity service for the
480
     * given set of affected elements.
481
     *
482
     * @param CombinedRecord[] $affectedElements
483
     * @return IntegrityService
484
     * @see getAffectedElements
485
     */
486
    protected function createIntegrityService(array $affectedElements)
487
    {
488
        $integrityService = GeneralUtility::makeInstance(IntegrityService::class);
489
        $integrityService->setAffectedElements($affectedElements);
490
        return $integrityService;
491
    }
492
493
    /**
494
     * Gets affected elements on publishing/swapping actions.
495
     * Affected elements have a dependency, e.g. translation overlay
496
     * and the default origin record - thus, the default record would be
497
     * affected if the translation overlay shall be published.
498
     *
499
     * @param \stdClass $parameters
500
     * @return array
501
     */
502
    protected function getAffectedElements(\stdClass $parameters)
503
    {
504
        $affectedElements = [];
505
        if ($parameters->type === 'selection') {
506
            foreach ((array)$parameters->selection as $element) {
507
                $affectedElements[] = CombinedRecord::create($element->table, $element->liveId, $element->versionId);
508
            }
509
        } elseif ($parameters->type === 'all') {
510
            $versions = $this->workspaceService->selectVersionsInWorkspace(
511
                $this->getCurrentWorkspace(),
512
                -99,
513
                -1,
514
                0,
515
                'tables_select',
516
                $this->validateLanguageParameter($parameters)
517
            );
518
            foreach ($versions as $table => $tableElements) {
519
                foreach ($tableElements as $element) {
520
                    $affectedElement = CombinedRecord::create($table, $element['t3ver_oid'], $element['uid']);
521
                    $affectedElement->getVersionRecord()->setRow($element);
522
                    $affectedElements[] = $affectedElement;
523
                }
524
            }
525
        }
526
        return $affectedElements;
527
    }
528
529
    /**
530
     * Validates whether the submitted language parameter can be
531
     * interpreted as integer value.
532
     *
533
     * @param \stdClass $parameters
534
     * @return int|null
535
     */
536
    protected function validateLanguageParameter(\stdClass $parameters)
537
    {
538
        $language = null;
539
        if (isset($parameters->language) && MathUtility::canBeInterpretedAsInteger($parameters->language)) {
540
            $language = $parameters->language;
541
        }
542
        return $language;
543
    }
544
545
    /**
546
     * Gets the current workspace ID.
547
     *
548
     * @return int The current workspace ID
549
     */
550
    protected function getCurrentWorkspace()
551
    {
552
        return $this->workspaceService->getCurrentWorkspace();
553
    }
554
}
555