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