[TASK] Missing visual representation of sys_file_reference 70/31270/10
authorOliver Hader <oliver@typo3.org>
Thu, 29 Oct 2015 10:55:02 +0000 (11:55 +0100)
committerOliver Hader <oliver.hader@typo3.org>
Thu, 29 Oct 2015 11:47:12 +0000 (12:47 +0100)
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>
typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php
typo3/sysext/workspaces/Resources/Public/Css/module.css
typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php [new file with mode: 0644]

index dd5df95..2b4c842 100644 (file)
@@ -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
@@ -36,9 +37,14 @@ 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);
@@ -218,6 +273,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
      *
      * @param int $uid uid of changed element to search for in log
@@ -308,6 +431,20 @@ class ExtDirectServer extends AbstractHandler
     }
 
     /**
+     * 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
      */
     protected function getObjectManager()
index f1a0401..1b58944 100644 (file)
@@ -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 (file)
index 0000000..1a3fb9f
--- /dev/null
@@ -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];
+    }
+
+}