1<?php
2namespace TYPO3\CMS\Workspaces\Controller\Remote;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Backend\Backend\Avatar\Avatar;
18use TYPO3\CMS\Backend\Utility\BackendUtility;
19use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
20use TYPO3\CMS\Core\Database\ConnectionPool;
21use TYPO3\CMS\Core\Html\RteHtmlParser;
22use TYPO3\CMS\Core\Imaging\Icon;
23use TYPO3\CMS\Core\Imaging\IconFactory;
24use TYPO3\CMS\Core\Localization\LanguageService;
25use TYPO3\CMS\Core\Resource\FileReference;
26use TYPO3\CMS\Core\Resource\ProcessedFile;
27use TYPO3\CMS\Core\Utility\DiffUtility;
28use TYPO3\CMS\Core\Utility\GeneralUtility;
29use TYPO3\CMS\Core\Utility\MathUtility;
30use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
31use TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord;
32use TYPO3\CMS\Workspaces\Service\GridDataService;
33use TYPO3\CMS\Workspaces\Service\HistoryService;
34use TYPO3\CMS\Workspaces\Service\IntegrityService;
35use TYPO3\CMS\Workspaces\Service\StagesService;
36use TYPO3\CMS\Workspaces\Service\WorkspaceService;
37
38/**
39 * Class RemoteServer
40 * @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
41 */
42class RemoteServer
43{
44    /**
45     * @var GridDataService
46     */
47    protected $gridDataService;
48
49    /**
50     * @var StagesService
51     */
52    protected $stagesService;
53
54    /**
55     * @var WorkspaceService
56     */
57    protected $workspaceService;
58
59    /**
60     * @var DiffUtility
61     */
62    protected $differenceHandler;
63
64    public function __construct()
65    {
66        $this->workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
67        $this->gridDataService = GeneralUtility::makeInstance(GridDataService::class);
68        $this->stagesService = GeneralUtility::makeInstance(StagesService::class);
69    }
70
71    /**
72     * Checks integrity of elements before peforming actions on them.
73     *
74     * @param \stdClass $parameters
75     * @return array
76     */
77    public function checkIntegrity(\stdClass $parameters)
78    {
79        $integrity = $this->createIntegrityService($this->getAffectedElements($parameters));
80        $integrity->check();
81        $response = [
82            'result' => $integrity->getStatusRepresentation()
83        ];
84        return $response;
85    }
86
87    /**
88     * Get List of workspace changes
89     *
90     * @param \stdClass $parameter
91     * @return array $data
92     */
93    public function getWorkspaceInfos($parameter)
94    {
95        // To avoid too much work we use -1 to indicate that every page is relevant
96        $pageId = $parameter->id > 0 ? $parameter->id : -1;
97        if (!isset($parameter->language) || !MathUtility::canBeInterpretedAsInteger($parameter->language)) {
98            $parameter->language = null;
99        }
100        $versions = $this->workspaceService->selectVersionsInWorkspace($this->getCurrentWorkspace(), 0, -99, $pageId, $parameter->depth, 'tables_select', $parameter->language);
101        $data = $this->gridDataService->generateGridListFromVersions($versions, $parameter, $this->getCurrentWorkspace());
102        return $data;
103    }
104
105    /**
106     * Get List of available workspace actions
107     *
108     * @return array $data
109     */
110    public function getStageActions()
111    {
112        $currentWorkspace = $this->getCurrentWorkspace();
113        $stages = [];
114        if ($currentWorkspace != WorkspaceService::SELECT_ALL_WORKSPACES) {
115            $stages = $this->stagesService->getStagesForWSUser();
116        }
117        $data = [
118            'total' => count($stages),
119            'data' => $stages
120        ];
121        return $data;
122    }
123
124    /**
125     * Fetch further information to current selected workspace record.
126     *
127     * @param \stdClass $parameter
128     * @return array $data
129     */
130    public function getRowDetails($parameter)
131    {
132        $diffReturnArray = [];
133        $liveReturnArray = [];
134        $diffUtility = $this->getDifferenceHandler();
135        $parseObj = GeneralUtility::makeInstance(RteHtmlParser::class);
136        $liveRecord = BackendUtility::getRecord($parameter->table, $parameter->t3ver_oid);
137        $versionRecord = BackendUtility::getRecord($parameter->table, $parameter->uid);
138        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
139        $icon_Live = $iconFactory->getIconForRecord($parameter->table, $liveRecord, Icon::SIZE_SMALL)->render();
140        $icon_Workspace = $iconFactory->getIconForRecord($parameter->table, $versionRecord, Icon::SIZE_SMALL)->render();
141        $stagePosition = $this->stagesService->getPositionOfCurrentStage($parameter->stage);
142        $fieldsOfRecords = array_keys($liveRecord);
143        if ($GLOBALS['TCA'][$parameter->table]) {
144            if ($GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList']) {
145                $fieldsOfRecords = $GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList'];
146                $fieldsOfRecords = GeneralUtility::trimExplode(',', $fieldsOfRecords, true);
147            }
148        }
149        foreach ($fieldsOfRecords as $fieldName) {
150            if (empty($GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'])) {
151                continue;
152            }
153            // Get the field's label. If not available, use the field name
154            $fieldTitle = $this->getLanguageService()->sL(BackendUtility::getItemLabel($parameter->table, $fieldName));
155            if (empty($fieldTitle)) {
156                $fieldTitle = $fieldName;
157            }
158            // Gets the TCA configuration for the current field
159            $configuration = $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'];
160            // check for exclude fields
161            if ($this->getBackendUser()->isAdmin() || $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['exclude'] == 0 || GeneralUtility::inList($this->getBackendUser()->groupData['non_exclude_fields'], $parameter->table . ':' . $fieldName)) {
162                // call diff class only if there is a difference
163                if ($configuration['type'] === 'inline' && $configuration['foreign_table'] === 'sys_file_reference') {
164                    $useThumbnails = false;
165                    if (!empty($configuration['overrideChildTca']['columns']['uid_local']['config']['appearance']['elementBrowserAllowed']) && !empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'])) {
166                        $fileExtensions = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], true);
167                        $allowedExtensions = GeneralUtility::trimExplode(',', $configuration['overrideChildTca']['columns']['uid_local']['config']['appearance']['elementBrowserAllowed'], true);
168                        $differentExtensions = array_diff($allowedExtensions, $fileExtensions);
169                        $useThumbnails = empty($differentExtensions);
170                    }
171
172                    $liveFileReferences = BackendUtility::resolveFileReferences(
173                        $parameter->table,
174                        $fieldName,
175                        $liveRecord,
176                        0
177                    );
178                    $versionFileReferences = BackendUtility::resolveFileReferences(
179                        $parameter->table,
180                        $fieldName,
181                        $versionRecord,
182                        $this->getCurrentWorkspace()
183                    );
184                    $fileReferenceDifferences = $this->prepareFileReferenceDifferences(
185                        $liveFileReferences,
186                        $versionFileReferences,
187                        $useThumbnails
188                    );
189
190                    if ($fileReferenceDifferences === null) {
191                        continue;
192                    }
193
194                    $diffReturnArray[] = [
195                        'field' => $fieldName,
196                        'label' => $fieldTitle,
197                        'content' => $fileReferenceDifferences['differences']
198                    ];
199                    $liveReturnArray[] = [
200                        'field' => $fieldName,
201                        'label' => $fieldTitle,
202                        'content' => $fileReferenceDifferences['live']
203                    ];
204                } elseif ((string)$liveRecord[$fieldName] !== (string)$versionRecord[$fieldName]) {
205                    // Select the human readable values before diff
206                    $liveRecord[$fieldName] = BackendUtility::getProcessedValue(
207                        $parameter->table,
208                        $fieldName,
209                        $liveRecord[$fieldName],
210                        0,
211                        1,
212                        false,
213                        $liveRecord['uid']
214                    );
215                    $versionRecord[$fieldName] = BackendUtility::getProcessedValue(
216                        $parameter->table,
217                        $fieldName,
218                        $versionRecord[$fieldName],
219                        0,
220                        1,
221                        false,
222                        $versionRecord['uid']
223                    );
224
225                    if ($configuration['type'] === 'group' && $configuration['internal_type'] === 'file') {
226                        // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
227                        $versionThumb = BackendUtility::thumbCode($versionRecord, $parameter->table, $fieldName, '');
228                        $liveThumb = BackendUtility::thumbCode($liveRecord, $parameter->table, $fieldName, '');
229                        $diffReturnArray[] = [
230                            'field' => $fieldName,
231                            'label' => $fieldTitle,
232                            'content' => $versionThumb
233                        ];
234                        $liveReturnArray[] = [
235                            'field' => $fieldName,
236                            'label' => $fieldTitle,
237                            'content' => $liveThumb
238                        ];
239                    } else {
240                        $diffReturnArray[] = [
241                            'field' => $fieldName,
242                            'label' => $fieldTitle,
243                            'content' => $diffUtility->makeDiffDisplay($liveRecord[$fieldName], $versionRecord[$fieldName])
244                        ];
245                        $liveReturnArray[] = [
246                            'field' => $fieldName,
247                            'label' => $fieldTitle,
248                            'content' => $parseObj->TS_images_rte($liveRecord[$fieldName])
249                        ];
250                    }
251                }
252            }
253        }
254        // Hook for modifying the difference and live arrays
255        // (this may be used by custom or dynamically-defined fields)
256        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] ?? [] as $className) {
257            $hookObject = GeneralUtility::makeInstance($className);
258            if (method_exists($hookObject, 'modifyDifferenceArray')) {
259                $hookObject->modifyDifferenceArray($parameter, $diffReturnArray, $liveReturnArray, $diffUtility);
260            }
261        }
262        $commentsForRecord = $this->getCommentsForRecord($parameter->uid, $parameter->table);
263
264        $historyService = GeneralUtility::makeInstance(HistoryService::class);
265        $history = $historyService->getHistory($parameter->table, $parameter->t3ver_oid);
266
267        $prevStage = $this->stagesService->getPrevStage($parameter->stage);
268        $nextStage = $this->stagesService->getNextStage($parameter->stage);
269
270        if (isset($prevStage[0])) {
271            $prevStage = current($prevStage);
272        }
273
274        if (isset($nextStage[0])) {
275            $nextStage = current($nextStage);
276        }
277
278        return [
279            'total' => 1,
280            'data' => [
281                [
282                    // these parts contain HTML (don't escape)
283                    'diff' => $diffReturnArray,
284                    'live_record' => $liveReturnArray,
285                    'icon_Live' => $icon_Live,
286                    'icon_Workspace' => $icon_Workspace,
287                    // this part is already escaped in getCommentsForRecord()
288                    'comments' => $commentsForRecord,
289                    // escape/sanitize the others
290                    'path_Live' => htmlspecialchars(BackendUtility::getRecordPath($liveRecord['pid'], '', 999)),
291                    'label_Stage' => htmlspecialchars($this->stagesService->getStageTitle($parameter->stage)),
292                    'label_PrevStage' => $prevStage,
293                    'label_NextStage' => $nextStage,
294                    'stage_position' => (int)$stagePosition['position'],
295                    'stage_count' => (int)$stagePosition['count'],
296                    'parent' => [
297                        'table' => htmlspecialchars($parameter->table),
298                        'uid' => (int)$parameter->uid
299                    ],
300                    'history' => [
301                        'data' => $history,
302                        'total' => count($history)
303                    ]
304                ]
305            ]
306        ];
307    }
308
309    /**
310     * Prepares difference view for file references.
311     *
312     * @param FileReference[] $liveFileReferences
313     * @param FileReference[] $versionFileReferences
314     * @param bool|false $useThumbnails
315     * @return array|null
316     */
317    protected function prepareFileReferenceDifferences(array $liveFileReferences, array $versionFileReferences, $useThumbnails = false)
318    {
319        $randomValue = uniqid('file');
320
321        $liveValues = [];
322        $versionValues = [];
323        $candidates = [];
324        $substitutes = [];
325
326        // Process live references
327        foreach ($liveFileReferences as $identifier => $liveFileReference) {
328            $identifierWithRandomValue = $randomValue . '__' . $liveFileReference->getUid() . '__' . $randomValue;
329            $candidates[$identifierWithRandomValue] = $liveFileReference;
330            $liveValues[] = $identifierWithRandomValue;
331        }
332
333        // Process version references
334        foreach ($versionFileReferences as $identifier => $versionFileReference) {
335            $identifierWithRandomValue = $randomValue . '__' . $versionFileReference->getUid() . '__' . $randomValue;
336            $candidates[$identifierWithRandomValue] = $versionFileReference;
337            $versionValues[] = $identifierWithRandomValue;
338        }
339
340        // Combine values and surround by spaces
341        // (to reduce the chunks Diff will find)
342        $liveInformation = ' ' . implode(' ', $liveValues) . ' ';
343        $versionInformation = ' ' . implode(' ', $versionValues) . ' ';
344
345        // Return if information has not changed
346        if ($liveInformation === $versionInformation) {
347            return null;
348        }
349
350        /**
351         * @var string $identifierWithRandomValue
352         * @var FileReference $fileReference
353         */
354        foreach ($candidates as $identifierWithRandomValue => $fileReference) {
355            if ($useThumbnails) {
356                $thumbnailFile = $fileReference->getOriginalFile()->process(
357                    ProcessedFile::CONTEXT_IMAGEPREVIEW,
358                    ['width' => 40, 'height' => 40]
359                );
360                $thumbnailMarkup = '<img src="' . $thumbnailFile->getPublicUrl(true) . '" />';
361                $substitutes[$identifierWithRandomValue] = $thumbnailMarkup;
362            } else {
363                $substitutes[$identifierWithRandomValue] = $fileReference->getPublicUrl();
364            }
365        }
366
367        $differences = $this->getDifferenceHandler()->makeDiffDisplay($liveInformation, $versionInformation);
368        $liveInformation = str_replace(array_keys($substitutes), array_values($substitutes), trim($liveInformation));
369        $differences = str_replace(array_keys($substitutes), array_values($substitutes), trim($differences));
370
371        return [
372            'live' => $liveInformation,
373            'differences' => $differences
374        ];
375    }
376
377    /**
378     * Gets an array with all sys_log entries and their comments for the given record uid and table
379     *
380     * @param int $uid uid of changed element to search for in log
381     * @param string $table Name of the record's table
382     * @return array
383     */
384    public function getCommentsForRecord($uid, $table)
385    {
386        $sysLogReturnArray = [];
387        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
388
389        $result = $queryBuilder
390            ->select('log_data', 'tstamp', 'userid')
391            ->from('sys_log')
392            ->where(
393                $queryBuilder->expr()->eq(
394                    'action',
395                    $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT)
396                ),
397                $queryBuilder->expr()->eq(
398                    'details_nr',
399                    $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT)
400                ),
401                $queryBuilder->expr()->eq(
402                    'tablename',
403                    $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
404                ),
405                $queryBuilder->expr()->eq(
406                    'recuid',
407                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
408                )
409            )
410            ->orderBy('tstamp', 'DESC')
411            ->execute();
412
413        /** @var Avatar $avatar */
414        $avatar = GeneralUtility::makeInstance(Avatar::class);
415
416        while ($sysLogRow = $result->fetch()) {
417            $sysLogEntry = [];
418            $data = unserialize($sysLogRow['log_data']);
419            $beUserRecord = BackendUtility::getRecord('be_users', $sysLogRow['userid']);
420            $sysLogEntry['stage_title'] = htmlspecialchars($this->stagesService->getStageTitle($data['stage']));
421            $sysLogEntry['user_uid'] = (int)$sysLogRow['userid'];
422            $sysLogEntry['user_username'] = is_array($beUserRecord) ? htmlspecialchars($beUserRecord['username']) : '';
423            $sysLogEntry['tstamp'] = htmlspecialchars(BackendUtility::datetime($sysLogRow['tstamp']));
424            $sysLogEntry['user_comment'] = nl2br(htmlspecialchars($data['comment']));
425            $sysLogEntry['user_avatar'] = $avatar->render($beUserRecord);
426            $sysLogReturnArray[] = $sysLogEntry;
427        }
428        return $sysLogReturnArray;
429    }
430
431    /**
432     * Gets all available system languages.
433     *
434     * @param \stdClass $parameters
435     * @return array
436     */
437    public function getSystemLanguages(\stdClass $parameters)
438    {
439        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
440        $systemLanguages = [
441            [
442                'uid' => 'all',
443                'title' => LocalizationUtility::translate('language.allLanguages', 'workspaces'),
444                'icon' => $iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render()
445            ]
446        ];
447        foreach ($this->gridDataService->getSystemLanguages($parameters->pageUid ?? 0) as $id => $systemLanguage) {
448            if ($id < 0) {
449                continue;
450            }
451            $systemLanguages[] = [
452                'uid' => $id,
453                'title' => htmlspecialchars($systemLanguage['title']),
454                'icon' => $iconFactory->getIcon($systemLanguage['flagIcon'], Icon::SIZE_SMALL)->render()
455            ];
456        }
457        $result = [
458            'total' => count($systemLanguages),
459            'data' => $systemLanguages
460        ];
461        return $result;
462    }
463
464    /**
465     * @return BackendUserAuthentication
466     */
467    protected function getBackendUser()
468    {
469        return $GLOBALS['BE_USER'];
470    }
471
472    /**
473     * @return LanguageService
474     */
475    protected function getLanguageService()
476    {
477        return $GLOBALS['LANG'];
478    }
479
480    /**
481     * Gets the difference handler, parsing differences based on sentences.
482     *
483     * @return DiffUtility
484     */
485    protected function getDifferenceHandler()
486    {
487        if (!isset($this->differenceHandler)) {
488            $this->differenceHandler = GeneralUtility::makeInstance(DiffUtility::class);
489            $this->differenceHandler->stripTags = false;
490        }
491        return $this->differenceHandler;
492    }
493
494    /**
495     * Creates a new instance of the integrity service for the
496     * given set of affected elements.
497     *
498     * @param CombinedRecord[] $affectedElements
499     * @return IntegrityService
500     * @see getAffectedElements
501     */
502    protected function createIntegrityService(array $affectedElements)
503    {
504        $integrityService = GeneralUtility::makeInstance(IntegrityService::class);
505        $integrityService->setAffectedElements($affectedElements);
506        return $integrityService;
507    }
508
509    /**
510     * Gets affected elements on publishing/swapping actions.
511     * Affected elements have a dependency, e.g. translation overlay
512     * and the default origin record - thus, the default record would be
513     * affected if the translation overlay shall be published.
514     *
515     * @param \stdClass $parameters
516     * @return array
517     */
518    protected function getAffectedElements(\stdClass $parameters)
519    {
520        $affectedElements = [];
521        if ($parameters->type === 'selection') {
522            foreach ((array)$parameters->selection as $element) {
523                $affectedElements[] = CombinedRecord::create($element->table, $element->liveId, $element->versionId);
524            }
525        } elseif ($parameters->type === 'all') {
526            $versions = $this->workspaceService->selectVersionsInWorkspace($this->getCurrentWorkspace(), 0, -99, -1, 0, 'tables_select', $this->validateLanguageParameter($parameters));
527            foreach ($versions as $table => $tableElements) {
528                foreach ($tableElements as $element) {
529                    $affectedElement = CombinedRecord::create($table, $element['t3ver_oid'], $element['uid']);
530                    $affectedElement->getVersionRecord()->setRow($element);
531                    $affectedElements[] = $affectedElement;
532                }
533            }
534        }
535        return $affectedElements;
536    }
537
538    /**
539     * Validates whether the submitted language parameter can be
540     * interpreted as integer value.
541     *
542     * @param \stdClass $parameters
543     * @return int|null
544     */
545    protected function validateLanguageParameter(\stdClass $parameters)
546    {
547        $language = null;
548        if (isset($parameters->language) && MathUtility::canBeInterpretedAsInteger($parameters->language)) {
549            $language = $parameters->language;
550        }
551        return $language;
552    }
553
554    /**
555     * Gets the current workspace ID.
556     *
557     * @return int The current workspace ID
558     */
559    protected function getCurrentWorkspace()
560    {
561        return $this->workspaceService->getCurrentWorkspace();
562    }
563}
564