From 75e9d0e0c88c8f9832b3227daf5cc6341727dcd8 Mon Sep 17 00:00:00 2001 From: Oliver Hader <oliver@typo3.org> Date: Thu, 29 Oct 2015 11:55:02 +0100 Subject: [PATCH] [TASK] Missing visual representation of sys_file_reference File references are currently only represented by the accordant record uid which should at least be a filename. In a workspace environment the changed file references shall be visualized as thumbnails - either being removed or inserted. Resolves: #60011 Releases: master Change-Id: I6d22619c264ff0e5411a47b2d566ec2c9b7c2607 Reviewed-on: https://review.typo3.org/31270 Reviewed-by: Alexander Opitz <opitz.alexander@googlemail.com> Tested-by: Alexander Opitz <opitz.alexander@googlemail.com> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> Tested-by: Oliver Hader <oliver.hader@typo3.org> --- .../Classes/ExtDirect/ExtDirectServer.php | 157 ++++++++++++++- .../Resources/Public/Css/module.css | 10 + .../Unit/ExtDirect/ExtDirectServerTest.php | 179 ++++++++++++++++++ 3 files changed, 336 insertions(+), 10 deletions(-) create mode 100644 typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php diff --git a/typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php b/typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php index dd5df95e2f73..2b4c842035a0 100644 --- a/typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php +++ b/typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php @@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; +use TYPO3\CMS\Core\Resource\FileReference; /** * ExtDirect server @@ -35,10 +36,15 @@ class ExtDirectServer extends AbstractHandler */ protected $stagesService; + /** + * @var \cogpowered\FineDiff\Diff + */ + protected $differenceHandler; + /** * Checks integrity of elements before peforming actions on them. * - * @param stdClass $parameters + * @param \stdClass $parameters * @return array */ public function checkIntegrity(\stdClass $parameters) @@ -134,10 +140,61 @@ class ExtDirectServer extends AbstractHandler } } foreach ($fieldsOfRecords as $fieldName) { + if (empty($GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'])) { + continue; + } + // Get the field's label. If not available, use the field name + $fieldTitle = $GLOBALS['LANG']->sL(BackendUtility::getItemLabel($parameter->table, $fieldName)); + if (empty($fieldTitle)) { + $fieldTitle = $fieldName; + } + // Gets the TCA configuration for the current field + $configuration = $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config']; // check for exclude fields 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)) { // call diff class only if there is a difference - if ((string)$liveRecord[$fieldName] !== (string)$versionRecord[$fieldName]) { + if ($configuration['type'] === 'inline' && $configuration['foreign_table'] === 'sys_file_reference') { + $useThumbnails = false; + if (!empty($configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed']) && !empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'])) { + $fileExtensions = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], true); + $allowedExtensions = GeneralUtility::trimExplode(',', $configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed'], true); + $differentExtensions = array_diff($allowedExtensions, $fileExtensions); + $useThumbnails = empty($differentExtensions); + } + + $liveFileReferences = BackendUtility::resolveFileReferences( + $parameter->table, + $fieldName, + $liveRecord, + 0 + ); + $versionFileReferences = BackendUtility::resolveFileReferences( + $parameter->table, + $fieldName, + $versionRecord, + $this->getCurrentWorkspace() + ); + $fileReferenceDifferences = $this->prepareFileReferenceDifferences( + $liveFileReferences, + $versionFileReferences, + $useThumbnails + ); + + if ($fileReferenceDifferences === null) { + continue; + } + + $diffReturnArray[] = array( + 'field' => $fieldName, + 'label' => $fieldTitle, + 'content' => $fileReferenceDifferences['differences'] + ); + $liveReturnArray[] = array( + 'field' => $fieldName, + 'label' => $fieldTitle, + 'content' => $fileReferenceDifferences['live'] + ); + } elseif ((string)$liveRecord[$fieldName] !== (string)$versionRecord[$fieldName]) { // Select the human readable values before diff $liveRecord[$fieldName] = BackendUtility::getProcessedValue( $parameter->table, @@ -157,12 +214,8 @@ class ExtDirectServer extends AbstractHandler false, $versionRecord['uid'] ); - // Get the field's label. If not available, use the field name - $fieldTitle = $GLOBALS['LANG']->sL(BackendUtility::getItemLabel($parameter->table, $fieldName)); - if (empty($fieldTitle)) { - $fieldTitle = $fieldName; - } - if ($GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config']['type'] == 'group' && $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config']['internal_type'] == 'file') { + + if ($configuration['type'] == 'group' && $configuration['internal_type'] == 'file') { $versionThumb = BackendUtility::thumbCode($versionRecord, $parameter->table, $fieldName, ''); $liveThumb = BackendUtility::thumbCode($liveRecord, $parameter->table, $fieldName, ''); $diffReturnArray[] = array( @@ -194,8 +247,10 @@ class ExtDirectServer extends AbstractHandler // (this may be used by custom or dynamically-defined fields) if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'])) { foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] as $className) { - $hookObject =& GeneralUtility::getUserObj($className); - $hookObject->modifyDifferenceArray($parameter, $diffReturnArray, $liveReturnArray, $diffUtility); + $hookObject = GeneralUtility::getUserObj($className); + if (method_exists($hookObject, 'modifyDifferenceArray')) { + $hookObject->modifyDifferenceArray($parameter, $diffReturnArray, $liveReturnArray, $diffUtility); + } } } $commentsForRecord = $this->getCommentsForRecord($parameter->uid, $parameter->table); @@ -217,6 +272,74 @@ class ExtDirectServer extends AbstractHandler ); } + /** + * Prepares difference view for file references. + * + * @param FileReference[] $liveFileReferences + * @param FileReference[] $versionFileReferences + * @param bool|false $useThumbnails + * @return array|null + */ + protected function prepareFileReferenceDifferences(array $liveFileReferences, array $versionFileReferences, $useThumbnails = false) + { + $randomValue = uniqid('file'); + + $liveValues = array(); + $versionValues = array(); + $candidates = array(); + $substitutes = array(); + + // Process live references + foreach ($liveFileReferences as $identifier => $liveFileReference) { + $identifierWithRandomValue = $randomValue . '__' . $liveFileReference->getUid() . '__' . $randomValue; + $candidates[$identifierWithRandomValue] = $liveFileReference; + $liveValues[] = $identifierWithRandomValue; + } + + // Process version references + foreach ($versionFileReferences as $identifier => $versionFileReference) { + $identifierWithRandomValue = $randomValue . '__' . $versionFileReference->getUid() . '__' . $randomValue; + $candidates[$identifierWithRandomValue] = $versionFileReference; + $versionValues[] = $identifierWithRandomValue; + } + + // Combine values and surround by spaces + // (to reduce the chunks Diff will find) + $liveInformation = ' ' . implode(' ', $liveValues) . ' '; + $versionInformation = ' ' . implode(' ', $versionValues) . ' '; + + // Return if information has not changed + if ($liveInformation === $versionInformation) { + return null; + } + + /** + * @var string $identifierWithRandomValue + * @var FileReference $fileReference + */ + foreach ($candidates as $identifierWithRandomValue => $fileReference) { + if ($useThumbnails) { + $thumbnailFile = $fileReference->getOriginalFile()->process( + \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW, + array('width' => 40, 'height' => 40) + ); + $thumbnailMarkup = '<img src="' . $thumbnailFile->getPublicUrl(true) . '" />'; + $substitutes[$identifierWithRandomValue] = $thumbnailMarkup; + } else { + $substitutes[$identifierWithRandomValue] = $fileReference->getPublicUrl(); + } + } + + $differences = $this->getDifferenceHandler()->render($liveInformation, $versionInformation); + $liveInformation = str_replace(array_keys($substitutes), array_values($substitutes), trim($liveInformation)); + $differences = str_replace(array_keys($substitutes), array_values($substitutes), trim($differences)); + + return array( + 'live' => $liveInformation, + 'differences' => $differences + ); + } + /** * Gets an array with all sys_log entries and their comments for the given record uid and table * @@ -307,6 +430,20 @@ class ExtDirectServer extends AbstractHandler return $this->stagesService; } + /** + * Gets the difference handler, parsing differences based on sentences. + * + * @return \cogpowered\FineDiff\Diff + */ + protected function getDifferenceHandler() + { + if (!isset($this->differenceHandler)) { + $granularity = new \cogpowered\FineDiff\Granularity\Word(); + $this->differenceHandler = new \cogpowered\FineDiff\Diff($granularity); + } + return $this->differenceHandler; + } + /** * @return \TYPO3\CMS\Extbase\Object\ObjectManager */ diff --git a/typo3/sysext/workspaces/Resources/Public/Css/module.css b/typo3/sysext/workspaces/Resources/Public/Css/module.css index f1a04016fdce..1b58944bf79b 100644 --- a/typo3/sysext/workspaces/Resources/Public/Css/module.css +++ b/typo3/sysext/workspaces/Resources/Public/Css/module.css @@ -132,6 +132,16 @@ table.t3-workspaces-foldout-contentDiff td { table.t3-workspaces-foldout-contentDiff .diff-r { text-decoration: line-through; } +.t3-workspaces-foldout-contentDiff .content ins > img { + padding: 1px; + margin-right: 2px; + border: 2px solid green; +} +.t3-workspaces-foldout-contentDiff .content del > img { + padding: 1px; + margin-right: 2px; + border: 2px solid red; +} div.t3-workspaces-foldoutWrapper td.char_select_profile_stats { padding-right: 10px; } diff --git a/typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php b/typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php new file mode 100644 index 000000000000..1a3fb9f75c1b --- /dev/null +++ b/typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php @@ -0,0 +1,179 @@ +<?php +namespace TYPO3\CMS\Workspaces\Tests\Unit\ExtDirect; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Prophecy\Argument; +use Prophecy\Prophecy\ObjectProphecy; +use TYPO3\CMS\Core\Resource\File; +use TYPO3\CMS\Core\Resource\FileReference; +use TYPO3\CMS\Core\Resource\ProcessedFile; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * ExtDirectServer test + */ +class ExtDirectServerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase +{ + + /** + * @var \TYPO3\CMS\Workspaces\ExtDirect\ExtDirectServer + */ + protected $subject; + + /** + * @var FileReference[]|ObjectProphecy[] + */ + protected $fileReferenceProphecies; + + /** + * Set up + */ + protected function setUp() + { + parent::setUp(); + $this->subject = $this->getAccessibleMock(\TYPO3\CMS\Workspaces\ExtDirect\ExtDirectServer::class, array('__none')); + } + + /** + * Tear down. + */ + protected function tearDown() { + parent::tearDown(); + unset($this->subject); + unset($this->fileReferenceProphecies); + } + + /** + * @return array + */ + public function prepareFileReferenceDifferencesAreCorrectDataProvider() { + return array( + // without thumbnails + 'unchanged wo/thumbnails' => array('1,2,3,4', '1,2,3,4', false, null), + 'front addition wo/thumbnails' => array('1,2,3,4', '99,1,2,3,4', false, array( + 'live' => '/img/1.png /img/2.png /img/3.png /img/4.png', + 'differences' => '<ins>/img/99.png </ins>/img/1.png /img/2.png /img/3.png /img/4.png', + )), + 'end addition wo/thumbnails' => array('1,2,3,4', '1,2,3,4,99', false, array( + 'live' => '/img/1.png /img/2.png /img/3.png /img/4.png', + 'differences' => '/img/1.png /img/2.png /img/3.png /img/4.png <ins>/img/99.png </ins>', + )), + 'reorder wo/thumbnails' => array('1,2,3,4', '1,3,2,4', false, array( + 'live' => '/img/1.png /img/2.png /img/3.png /img/4.png', + 'differences' => '/img/1.png <ins>/img/3.png </ins>/img/2.png <del>/img/3.png </del>/img/4.png', + )), + 'move to end wo/thumbnails' => array('1,2,3,4', '2,3,4,1', false, array( + 'live' => '/img/1.png /img/2.png /img/3.png /img/4.png', + 'differences' => '<del>/img/1.png </del>/img/2.png /img/3.png /img/4.png <ins>/img/1.png </ins>', + )), + 'move to front wo/thumbnails' => array('1,2,3,4', '4,1,2,3', false, array( + 'live' => '/img/1.png /img/2.png /img/3.png /img/4.png', + 'differences' => '<ins>/img/4.png </ins>/img/1.png /img/2.png /img/3.png <del>/img/4.png </del>', + )), + 'keep last wo/thumbnails' => array('1,2,3,4', '4', false, array( + 'live' => '/img/1.png /img/2.png /img/3.png /img/4.png', + 'differences' => '<del>/img/1.png /img/2.png /img/3.png </del>/img/4.png', + )), + // with thumbnails + 'unchanged w/thumbnails' => array('1,2,3,4', '1,2,3,4', true, null), + 'front addition w/thumbnails' => array('1,2,3,4', '99,1,2,3,4', true, array( + 'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />', + 'differences' => '<ins><img src="/tmb/99.png" /> </ins><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />', + )), + 'end addition w/thumbnails' => array('1,2,3,4', '1,2,3,4,99', true, array( + 'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />', + 'differences' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" /> <ins><img src="/tmb/99.png" /> </ins>', + )), + 'reorder w/thumbnails' => array('1,2,3,4', '1,3,2,4', true, array( + 'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />', + 'differences' => '<img src="/tmb/1.png" /> <ins><img src="/tmb/3.png" /> </ins><img src="/tmb/2.png" /> <del><img src="/tmb/3.png" /> </del><img src="/tmb/4.png" />', + )), + 'move to end w/thumbnails' => array('1,2,3,4', '2,3,4,1', true, array( + 'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />', + 'differences' => '<del><img src="/tmb/1.png" /> </del><img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" /> <ins><img src="/tmb/1.png" /> </ins>', + )), + 'move to front w/thumbnails' => array('1,2,3,4', '4,1,2,3', true, array( + 'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />', + 'differences' => '<ins><img src="/tmb/4.png" /> </ins><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <del><img src="/tmb/4.png" /> </del>', + )), + 'keep last w/thumbnails' => array('1,2,3,4', '4', true, array( + 'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />', + 'differences' => '<del><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> </del><img src="/tmb/4.png" />', + )), + ); + } + + /** + * @param string $fileFileReferenceList + * @param string $versionFileReferenceList + * @param $useThumbnails + * @param array|null $expected + * @dataProvider prepareFileReferenceDifferencesAreCorrectDataProvider + * @test + */ + public function prepareFileReferenceDifferencesAreCorrect($fileFileReferenceList, $versionFileReferenceList, $useThumbnails, array $expected = null) { + $liveFileReferences = $this->getFileReferenceProphecies($fileFileReferenceList); + $versionFileReferences = $this->getFileReferenceProphecies($versionFileReferenceList); + + $result = $this->subject->_call( + 'prepareFileReferenceDifferences', + $liveFileReferences, + $versionFileReferences, + $useThumbnails + ); + + $this->assertSame($expected, $result); + } + + /** + * @param string $idList List of ids + * @return FileReference[]|ObjectProphecy[] + */ + protected function getFileReferenceProphecies($idList) { + $fileReferenceProphecies = array(); + $ids = GeneralUtility::trimExplode(',', $idList, true); + + foreach ($ids as $id) { + $fileReferenceProphecies[$id] = $this->getFileReferenceProphecy($id); + } + + return $fileReferenceProphecies; + } + + /** + * @param int $id + * @return ObjectProphecy|FileReference + */ + protected function getFileReferenceProphecy($id) { + if (isset($this->fileReferenceProphecies[$id])) { + return $this->fileReferenceProphecies[$id]; + } + + $processedFileProphecy = $this->prophesize(ProcessedFile::class); + $processedFileProphecy->getPublicUrl(Argument::cetera())->willReturn('/tmb/' . $id . '.png'); + + $fileProphecy = $this->prophesize(File::class); + $fileProphecy->process(Argument::cetera())->willReturn($processedFileProphecy->reveal()); + + $fileReferenceProphecy = $this->prophesize(FileReference::class); + $fileReferenceProphecy->getUid()->willReturn($id); + $fileReferenceProphecy->getOriginalFile()->willReturn($fileProphecy->reveal()); + $fileReferenceProphecy->getPublicUrl(Argument::cetera())->willReturn('/img/' . $id . '.png'); + + $this->fileReferenceProphecies[$id] = $fileReferenceProphecy->reveal(); + return $this->fileReferenceProphecies[$id]; + } + +} -- GitLab