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