[SECURITY] XSS through WS comments
[Packages/TYPO3.CMS.git] / typo3 / sysext / workspaces / Classes / ExtDirect / ExtDirectServer.php
1 <?php
2 namespace TYPO3\CMS\Workspaces\ExtDirect;
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
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Imaging\Icon;
19 use TYPO3\CMS\Core\Imaging\IconFactory;
20 use TYPO3\CMS\Core\Resource\FileReference;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Extbase\Object\ObjectManager;
23
24 /**
25 * ExtDirect server
26 */
27 class ExtDirectServer extends AbstractHandler
28 {
29 /**
30 * @var \TYPO3\CMS\Workspaces\Service\GridDataService
31 */
32 protected $gridDataService;
33
34 /**
35 * @var \TYPO3\CMS\Workspaces\Service\StagesService
36 */
37 protected $stagesService;
38
39 /**
40 * @var \cogpowered\FineDiff\Diff
41 */
42 protected $differenceHandler;
43
44 /**
45 * Checks integrity of elements before peforming actions on them.
46 *
47 * @param \stdClass $parameters
48 * @return array
49 */
50 public function checkIntegrity(\stdClass $parameters)
51 {
52 $integrity = $this->createIntegrityService($this->getAffectedElements($parameters));
53 $integrity->check();
54 $response = array(
55 'result' => $integrity->getStatusRepresentation()
56 );
57 return $response;
58 }
59
60 /**
61 * Get List of workspace changes
62 *
63 * @param \stdClass $parameter
64 * @return array $data
65 */
66 public function getWorkspaceInfos($parameter)
67 {
68 // To avoid too much work we use -1 to indicate that every page is relevant
69 $pageId = $parameter->id > 0 ? $parameter->id : -1;
70 if (!isset($parameter->language) || !\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($parameter->language)) {
71 $parameter->language = null;
72 }
73 $versions = $this->getWorkspaceService()->selectVersionsInWorkspace($this->getCurrentWorkspace(), 0, -99, $pageId, $parameter->depth, 'tables_select', $parameter->language);
74 $data = $this->getGridDataService()->generateGridListFromVersions($versions, $parameter, $this->getCurrentWorkspace());
75 return $data;
76 }
77
78 /**
79 * Gets the editing history of a record.
80 *
81 * @param stdClass $parameters
82 * @return array
83 */
84 public function getHistory($parameters)
85 {
86 /** @var $historyService \TYPO3\CMS\Workspaces\Service\HistoryService */
87 $historyService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\HistoryService::class);
88 $history = $historyService->getHistory($parameters->table, $parameters->liveId);
89 return array(
90 'data' => $history,
91 'total' => count($history)
92 );
93 }
94
95 /**
96 * Get List of available workspace actions
97 *
98 * @param \stdClass $parameter
99 * @return array $data
100 */
101 public function getStageActions(\stdClass $parameter)
102 {
103 $currentWorkspace = $this->getCurrentWorkspace();
104 $stages = array();
105 if ($currentWorkspace != \TYPO3\CMS\Workspaces\Service\WorkspaceService::SELECT_ALL_WORKSPACES) {
106 $stages = $this->getStagesService()->getStagesForWSUser();
107 }
108 $data = array(
109 'total' => count($stages),
110 'data' => $stages
111 );
112 return $data;
113 }
114
115 /**
116 * Fetch further information to current selected workspace record.
117 *
118 * @param \stdClass $parameter
119 * @return array $data
120 */
121 public function getRowDetails($parameter)
122 {
123 $diffReturnArray = array();
124 $liveReturnArray = array();
125 /** @var $diffUtility \TYPO3\CMS\Core\Utility\DiffUtility */
126 $diffUtility = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Utility\DiffUtility::class);
127 /** @var $parseObj \TYPO3\CMS\Core\Html\RteHtmlParser */
128 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Html\RteHtmlParser::class);
129 $liveRecord = BackendUtility::getRecord($parameter->table, $parameter->t3ver_oid);
130 $versionRecord = BackendUtility::getRecord($parameter->table, $parameter->uid);
131 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
132 $icon_Live = $iconFactory->getIconForRecord($parameter->table, $liveRecord, Icon::SIZE_SMALL)->render();
133 $icon_Workspace = $iconFactory->getIconForRecord($parameter->table, $versionRecord, Icon::SIZE_SMALL)->render();
134 $stagePosition = $this->getStagesService()->getPositionOfCurrentStage($parameter->stage);
135 $fieldsOfRecords = array_keys($liveRecord);
136 if ($GLOBALS['TCA'][$parameter->table]) {
137 if ($GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList']) {
138 $fieldsOfRecords = $GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList'];
139 $fieldsOfRecords = GeneralUtility::trimExplode(',', $fieldsOfRecords, true);
140 }
141 }
142 foreach ($fieldsOfRecords as $fieldName) {
143 if (empty($GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'])) {
144 continue;
145 }
146 // Get the field's label. If not available, use the field name
147 $fieldTitle = $GLOBALS['LANG']->sL(BackendUtility::getItemLabel($parameter->table, $fieldName));
148 if (empty($fieldTitle)) {
149 $fieldTitle = $fieldName;
150 }
151 // Gets the TCA configuration for the current field
152 $configuration = $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'];
153 // check for exclude fields
154 if ($GLOBALS['BE_USER']->isAdmin() || $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['exclude'] == 0 || GeneralUtility::inList($GLOBALS['BE_USER']->groupData['non_exclude_fields'], $parameter->table . ':' . $fieldName)) {
155 // call diff class only if there is a difference
156 if ($configuration['type'] === 'inline' && $configuration['foreign_table'] === 'sys_file_reference') {
157 $useThumbnails = false;
158 if (!empty($configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed']) && !empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'])) {
159 $fileExtensions = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], true);
160 $allowedExtensions = GeneralUtility::trimExplode(',', $configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed'], true);
161 $differentExtensions = array_diff($allowedExtensions, $fileExtensions);
162 $useThumbnails = empty($differentExtensions);
163 }
164
165 $liveFileReferences = BackendUtility::resolveFileReferences(
166 $parameter->table,
167 $fieldName,
168 $liveRecord,
169 0
170 );
171 $versionFileReferences = BackendUtility::resolveFileReferences(
172 $parameter->table,
173 $fieldName,
174 $versionRecord,
175 $this->getCurrentWorkspace()
176 );
177 $fileReferenceDifferences = $this->prepareFileReferenceDifferences(
178 $liveFileReferences,
179 $versionFileReferences,
180 $useThumbnails
181 );
182
183 if ($fileReferenceDifferences === null) {
184 continue;
185 }
186
187 $diffReturnArray[] = array(
188 'field' => $fieldName,
189 'label' => $fieldTitle,
190 'content' => $fileReferenceDifferences['differences']
191 );
192 $liveReturnArray[] = array(
193 'field' => $fieldName,
194 'label' => $fieldTitle,
195 'content' => $fileReferenceDifferences['live']
196 );
197 } elseif ((string)$liveRecord[$fieldName] !== (string)$versionRecord[$fieldName]) {
198 // Select the human readable values before diff
199 $liveRecord[$fieldName] = BackendUtility::getProcessedValue(
200 $parameter->table,
201 $fieldName,
202 $liveRecord[$fieldName],
203 0,
204 1,
205 false,
206 $liveRecord['uid']
207 );
208 $versionRecord[$fieldName] = BackendUtility::getProcessedValue(
209 $parameter->table,
210 $fieldName,
211 $versionRecord[$fieldName],
212 0,
213 1,
214 false,
215 $versionRecord['uid']
216 );
217
218 if ($configuration['type'] == 'group' && $configuration['internal_type'] == 'file') {
219 $versionThumb = BackendUtility::thumbCode($versionRecord, $parameter->table, $fieldName, '');
220 $liveThumb = BackendUtility::thumbCode($liveRecord, $parameter->table, $fieldName, '');
221 $diffReturnArray[] = array(
222 'field' => $fieldName,
223 'label' => $fieldTitle,
224 'content' => $versionThumb
225 );
226 $liveReturnArray[] = array(
227 'field' => $fieldName,
228 'label' => $fieldTitle,
229 'content' => $liveThumb
230 );
231 } else {
232 $diffReturnArray[] = array(
233 'field' => $fieldName,
234 'label' => $fieldTitle,
235 'content' => $diffUtility->makeDiffDisplay($liveRecord[$fieldName], $versionRecord[$fieldName])
236 );
237 $liveReturnArray[] = array(
238 'field' => $fieldName,
239 'label' => $fieldTitle,
240 'content' => $parseObj->TS_images_rte($liveRecord[$fieldName])
241 );
242 }
243 }
244 }
245 }
246 // Hook for modifying the difference and live arrays
247 // (this may be used by custom or dynamically-defined fields)
248 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'])) {
249 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] as $className) {
250 $hookObject = GeneralUtility::getUserObj($className);
251 if (method_exists($hookObject, 'modifyDifferenceArray')) {
252 $hookObject->modifyDifferenceArray($parameter, $diffReturnArray, $liveReturnArray, $diffUtility);
253 }
254 }
255 }
256 $commentsForRecord = $this->getCommentsForRecord($parameter->uid, $parameter->table);
257 return array(
258 'total' => 1,
259 'data' => array(
260 array(
261 // these parts contain HTML (don't escape)
262 'diff' => $diffReturnArray,
263 'live_record' => $liveReturnArray,
264 'icon_Live' => $icon_Live,
265 'icon_Workspace' => $icon_Workspace,
266 // this part is already escaped in getCommentsForRecord()
267 'comments' => $commentsForRecord,
268 // escape/santinize the others
269 'path_Live' => htmlspecialchars($parameter->path_Live),
270 'label_Stage' => htmlspecialchars($parameter->label_Stage),
271 'stage_position' => (int)$stagePosition['position'],
272 'stage_count' => (int)$stagePosition['count']
273 )
274 )
275 );
276 }
277
278 /**
279 * Prepares difference view for file references.
280 *
281 * @param FileReference[] $liveFileReferences
282 * @param FileReference[] $versionFileReferences
283 * @param bool|false $useThumbnails
284 * @return array|null
285 */
286 protected function prepareFileReferenceDifferences(array $liveFileReferences, array $versionFileReferences, $useThumbnails = false)
287 {
288 $randomValue = uniqid('file');
289
290 $liveValues = array();
291 $versionValues = array();
292 $candidates = array();
293 $substitutes = array();
294
295 // Process live references
296 foreach ($liveFileReferences as $identifier => $liveFileReference) {
297 $identifierWithRandomValue = $randomValue . '__' . $liveFileReference->getUid() . '__' . $randomValue;
298 $candidates[$identifierWithRandomValue] = $liveFileReference;
299 $liveValues[] = $identifierWithRandomValue;
300 }
301
302 // Process version references
303 foreach ($versionFileReferences as $identifier => $versionFileReference) {
304 $identifierWithRandomValue = $randomValue . '__' . $versionFileReference->getUid() . '__' . $randomValue;
305 $candidates[$identifierWithRandomValue] = $versionFileReference;
306 $versionValues[] = $identifierWithRandomValue;
307 }
308
309 // Combine values and surround by spaces
310 // (to reduce the chunks Diff will find)
311 $liveInformation = ' ' . implode(' ', $liveValues) . ' ';
312 $versionInformation = ' ' . implode(' ', $versionValues) . ' ';
313
314 // Return if information has not changed
315 if ($liveInformation === $versionInformation) {
316 return null;
317 }
318
319 /**
320 * @var string $identifierWithRandomValue
321 * @var FileReference $fileReference
322 */
323 foreach ($candidates as $identifierWithRandomValue => $fileReference) {
324 if ($useThumbnails) {
325 $thumbnailFile = $fileReference->getOriginalFile()->process(
326 \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW,
327 array('width' => 40, 'height' => 40)
328 );
329 $thumbnailMarkup = '<img src="' . $thumbnailFile->getPublicUrl(true) . '" />';
330 $substitutes[$identifierWithRandomValue] = $thumbnailMarkup;
331 } else {
332 $substitutes[$identifierWithRandomValue] = $fileReference->getPublicUrl();
333 }
334 }
335
336 $differences = $this->getDifferenceHandler()->render($liveInformation, $versionInformation);
337 $liveInformation = str_replace(array_keys($substitutes), array_values($substitutes), trim($liveInformation));
338 $differences = str_replace(array_keys($substitutes), array_values($substitutes), trim($differences));
339
340 return array(
341 'live' => $liveInformation,
342 'differences' => $differences
343 );
344 }
345
346 /**
347 * Gets an array with all sys_log entries and their comments for the given record uid and table
348 *
349 * @param int $uid uid of changed element to search for in log
350 * @param string $table Name of the record's table
351 * @return array
352 */
353 public function getCommentsForRecord($uid, $table)
354 {
355 $sysLogReturnArray = array();
356 $sysLogRows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
357 'log_data,tstamp,userid',
358 'sys_log',
359 'action=6 and details_nr=30 AND tablename=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($table, 'sys_log')
360 . ' AND recuid=' . (int)$uid,
361 '',
362 'tstamp DESC'
363 );
364 foreach ($sysLogRows as $sysLogRow) {
365 $sysLogEntry = array();
366 $data = unserialize($sysLogRow['log_data']);
367 $beUserRecord = BackendUtility::getRecord('be_users', $sysLogRow['userid']);
368 $sysLogEntry['stage_title'] = htmlspecialchars($this->getStagesService()->getStageTitle($data['stage']));
369 $sysLogEntry['user_uid'] = (int)$sysLogRow['userid'];
370 $sysLogEntry['user_username'] = is_array($beUserRecord) ? htmlspecialchars($beUserRecord['username']) : '';
371 $sysLogEntry['tstamp'] = htmlspecialchars(BackendUtility::datetime($sysLogRow['tstamp']));
372 $sysLogEntry['user_comment'] = nl2br(htmlspecialchars($data['comment']));
373 $sysLogReturnArray[] = $sysLogEntry;
374 }
375 return $sysLogReturnArray;
376 }
377
378 /**
379 * Gets all available system languages.
380 *
381 * @return array
382 */
383 public function getSystemLanguages()
384 {
385 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
386 $systemLanguages = array(
387 array(
388 'uid' => 'all',
389 'title' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('language.allLanguages', 'workspaces'),
390 'icon' => $iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render()
391 )
392 );
393 foreach ($this->getGridDataService()->getSystemLanguages() as $id => $systemLanguage) {
394 if ($id < 0) {
395 continue;
396 }
397 $systemLanguages[] = array(
398 'uid' => $id,
399 'title' => htmlspecialchars($systemLanguage['title']),
400 'icon' => $iconFactory->getIcon($systemLanguage['flagIcon'], Icon::SIZE_SMALL)->render()
401 );
402 }
403 $result = array(
404 'total' => count($systemLanguages),
405 'data' => $systemLanguages
406 );
407 return $result;
408 }
409
410 /**
411 * Gets the Grid Data Service.
412 *
413 * @return \TYPO3\CMS\Workspaces\Service\GridDataService
414 */
415 protected function getGridDataService()
416 {
417 if (!isset($this->gridDataService)) {
418 $this->gridDataService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\GridDataService::class);
419 }
420 return $this->gridDataService;
421 }
422
423 /**
424 * Gets the Stages Service.
425 *
426 * @return \TYPO3\CMS\Workspaces\Service\StagesService
427 */
428 protected function getStagesService()
429 {
430 if (!isset($this->stagesService)) {
431 $this->stagesService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class);
432 }
433 return $this->stagesService;
434 }
435
436 /**
437 * Gets the difference handler, parsing differences based on sentences.
438 *
439 * @return \cogpowered\FineDiff\Diff
440 */
441 protected function getDifferenceHandler()
442 {
443 if (!isset($this->differenceHandler)) {
444 $granularity = new \cogpowered\FineDiff\Granularity\Word();
445 $this->differenceHandler = new \cogpowered\FineDiff\Diff($granularity);
446 }
447 return $this->differenceHandler;
448 }
449
450 /**
451 * @return \TYPO3\CMS\Extbase\Object\ObjectManager
452 */
453 protected function getObjectManager()
454 {
455 return GeneralUtility::makeInstance(ObjectManager::class);
456 }
457 }