[FEATURE] Ext:form - extend the extension location functionality 54/51254/3
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Tue, 10 Jan 2017 22:06:25 +0000 (23:06 +0100)
committerSusanne Moog <susanne.moog@typo3.org>
Wed, 11 Jan 2017 09:18:01 +0000 (10:18 +0100)
With this patch is it possible to:

* save existing forms within extension locations
  ("allowedExtensionPaths") if "allowSaveToExtensionPaths"
  is set to true (like before)
* save new created forms within extension locations
  ("allowedExtensionPaths") if "allowSaveToExtensionPaths"
  is set to true
* delete forms within extension locations ("allowedExtensionPaths")
  if "allowDeleteFromExtensionPaths" is set to true

Resolves: #79250
Releases: master
Change-Id: I2d06448f7ee9a0ab0a249ddfee750eda8aeee54e
Reviewed-on: https://review.typo3.org/51254
Tested-by: TYPO3com <no-reply@typo3.com>
Tested-by: Tobi Kretschmann <tobi@tobishome.de>
Reviewed-by: Tobi Kretschmann <tobi@tobishome.de>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Feature-79250-ExtFormExtendExtensionLocationFunctionality.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Controller/FormManagerController.php
typo3/sysext/form/Classes/Mvc/Configuration/YamlSource.php
typo3/sysext/form/Classes/Mvc/Persistence/FormPersistenceManager.php
typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml
typo3/sysext/form/Resources/Private/Backend/Templates/FormManager/Index.html
typo3/sysext/form/Resources/Private/Language/locallang_formManager_javascript.xlf
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormManager/ViewModel.js
typo3/sysext/form/Tests/Unit/Controller/FormManagerControllerTest.php
typo3/sysext/form/Tests/Unit/Mvc/Persistence/FormPersistenceManagerTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-79250-ExtFormExtendExtensionLocationFunctionality.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-79250-ExtFormExtendExtensionLocationFunctionality.rst
new file mode 100644 (file)
index 0000000..6080aff
--- /dev/null
@@ -0,0 +1,55 @@
+.. include:: ../../Includes.txt
+
+======================================================================
+Feature: #79250 - Ext:form extend the extension location functionality
+======================================================================
+
+See :issue:`79250`
+
+Description
+===========
+
+Ext:form has a feature to load custom form definitions from within extension locations.
+This locations can be configured through the :code:`allowedExtensionPaths` setting.
+To define whether forms can be changed from within extension locations through the form editor, a setting named :code:`allowSaveToExtensionPaths` exists.
+But this setting affects only already existing form definitions within extension locations.
+This feature makes it possible to store new forms within extension locations through the form manager as well.
+You can also define whether forms can be deleted within extension locations through the form manager with a new setting called :code:`allowDeleteFromExtensionPaths`.
+By default both settings :code:`allowSaveToExtensionPaths` and :code:`allowDeleteFromExtensionPaths` are disabled.
+
+Summary
+
+With this patch is it possible to:
+
+* save existing forms within extension locations ("allowedExtensionPaths") if "allowSaveToExtensionPaths" is set to true (like before)
+* save new created forms within extension locations ("allowedExtensionPaths") if "allowSaveToExtensionPaths" is set to true
+* delete forms within extension locations ("allowedExtensionPaths") if "allowDeleteFromExtensionPaths" is set to true
+
+
+Impact
+======
+
+Example to allow edit form definitions within 'EXT:my_ext/Resources/Private/Forms/':
+
+.. code-block:: yaml
+    TYPO3:
+      CMS:
+        Form:
+          persistenceManager:
+            allowSaveToExtensionPaths: true
+            allowedExtensionPaths:
+              100: EXT:my_ext/Resources/Private/Forms/
+
+
+Example to allow remove form definitions within 'EXT:my_ext/Resources/Private/Forms/':
+
+.. code-block:: yaml
+    TYPO3:
+      CMS:
+        Form:
+          persistenceManager:
+            allowDeleteFromExtensionPaths: true
+            allowedExtensionPaths:
+              100: EXT:my_ext/Resources/Private/Forms/
+
+.. index:: Backend, ext:form
\ No newline at end of file
index e3f77e6..eeaa426 100644 (file)
@@ -154,7 +154,6 @@ class FormManagerController extends AbstractBackendController
 
     /**
      * Delete a formDefinition identified by the $formPersistenceIdentifier.
-     * Only formDefinitions within storage folders are deletable.
      *
      * @param string $formPersistenceIdentifier persistence identifier to delete
      * @return void
@@ -162,10 +161,7 @@ class FormManagerController extends AbstractBackendController
      */
     public function deleteAction(string $formPersistenceIdentifier)
     {
-        if (
-            empty($this->getReferences($formPersistenceIdentifier))
-            && strpos($formPersistenceIdentifier, 'EXT:') === false
-        ) {
+        if (empty($this->getReferences($formPersistenceIdentifier))) {
             $this->formPersistenceManager->delete($formPersistenceIdentifier);
         } else {
             $this->addFlashMessage(
@@ -209,6 +205,16 @@ class FormManagerController extends AbstractBackendController
                 'value' => $identifier
             ];
         }
+
+        if ($this->formSettings['persistenceManager']['allowSaveToExtensionPaths']) {
+            foreach ($this->formPersistenceManager->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
+                $preparedAccessibleFormStorageFolders[] = [
+                    'label' => $relativePath,
+                    'value' => $relativePath
+                ];
+            }
+        }
+
         return $preparedAccessibleFormStorageFolders;
     }
 
index dcf11d5..e8defe7 100644 (file)
@@ -142,9 +142,12 @@ class YamlSource
         $header = '';
         if ($file instanceof File) {
             $fileLines = explode(LF, $file->getContents());
-        } else {
+        } elseif (is_file($file)) {
             $fileLines = file($file);
+        } else {
+            return '';
         }
+
         foreach ($fileLines as $line) {
             if (preg_match('/^#/', $line)) {
                 $header .= $line;
index 3d41cdd..6516b85 100644 (file)
@@ -85,7 +85,6 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
     /**
      * Load the array formDefinition identified by $persistenceIdentifier, and return it.
      * Only files with the extension .yaml are loaded.
-     * At this place there is no check if the file location is allowed.
      *
      * @param string $persistenceIdentifier
      * @return array
@@ -99,6 +98,9 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
         }
 
         if (strpos($persistenceIdentifier, 'EXT:') === 0) {
+            if (!array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
+                throw new PersistenceManagerException(sprintf('The file "%s" could not be loaded.', $persistenceIdentifier), 1484071985);
+            }
             $file = $persistenceIdentifier;
         } else {
             $file = $this->getFileByIdentifier($persistenceIdentifier);
@@ -130,6 +132,9 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
             if (!$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']) {
                 throw new PersistenceManagerException('Save to extension paths is not allowed.', 1477680881);
             }
+            if (!array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
+                throw new PersistenceManagerException(sprintf('The file "%s" could not be saved.', $persistenceIdentifier), 1484073571);
+            }
             $fileToSave = GeneralUtility::getFileAbsFileName($persistenceIdentifier);
         } else {
             $fileToSave = $this->getOrCreateFile($persistenceIdentifier);
@@ -141,7 +146,6 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
     /**
      * Delete the form representation identified by $persistenceIdentifier.
      * Only files with the extension .yaml are removed.
-     * formDefinitions within an EXT: resource are not removable.
      *
      * @param string $persistenceIdentifier
      * @return void
@@ -157,16 +161,23 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
             throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239535);
         }
         if (strpos($persistenceIdentifier, 'EXT:') === 0) {
-            throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239536);
-        }
-
-        list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
-        $storage = $this->getStorageByUid((int)$storageUid);
-        $file = $storage->getFile($fileIdentifier);
-        if (!$storage->checkFileActionPermission('delete', $file)) {
-            throw new PersistenceManagerException(sprintf('No delete access to file "%s".', $persistenceIdentifier), 1472239516);
+            if (!$this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths']) {
+                throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239536);
+            }
+            if (!array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
+                throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1484073878);
+            }
+            $fileToDelete = GeneralUtility::getFileAbsFileName($persistenceIdentifier);
+            unlink($fileToDelete);
+        } else {
+            list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
+            $storage = $this->getStorageByUid((int)$storageUid);
+            $file = $storage->getFile($fileIdentifier);
+            if (!$storage->checkFileActionPermission('delete', $file)) {
+                throw new PersistenceManagerException(sprintf('No delete access to file "%s".', $persistenceIdentifier), 1472239516);
+            }
+            $storage->deleteFile($file);
         }
-        $storage->deleteFile($file);
     }
 
     /**
@@ -181,7 +192,9 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
         $exists = false;
         if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) === 'yaml') {
             if (strpos($persistenceIdentifier, 'EXT:') === 0) {
-                $exists = file_exists(GeneralUtility::getFileAbsFileName($persistenceIdentifier));
+                if (array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
+                    $exists = file_exists(GeneralUtility::getFileAbsFileName($persistenceIdentifier));
+                }
             } else {
                 list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
                 $storage = $this->getStorageByUid((int)$storageUid);
@@ -223,6 +236,7 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
                     'name' => isset($form['label']) ? $form['label'] : $form['identifier'],
                     'persistenceIdentifier' => $persistenceIdentifier,
                     'readOnly' => false,
+                    'removable' => true,
                     'location' => 'storage',
                     'duplicateIdentifier' => false,
                 ];
@@ -243,6 +257,7 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
                     'name' => isset($form['label']) ? $form['label'] : $form['identifier'],
                     'persistenceIdentifier' => $relativePath . $fileInfo->getFilename(),
                     'readOnly' => $this->formSettings['persistenceManager']['allowSaveToExtensionPaths'] ? false: true,
+                    'removable' => $this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths'] ? true: false,
                     'location' => 'extension',
                     'duplicateIdentifier' => false,
                 ];
@@ -335,6 +350,7 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
             if (!file_exists($allowedExtensionFullPath)) {
                 continue;
             }
+            $allowedExtensionPath = rtrim($allowedExtensionPath, '/') . '/';
             $extensionFolders[$allowedExtensionPath] = $allowedExtensionFullPath;
         }
         return $extensionFolders;
index dacc84b..31733d1 100644 (file)
@@ -5,6 +5,7 @@ TYPO3:
         allowedFileMounts:
           10: 1:/user_upload/
         allowSaveToExtensionPaths: false
+        allowDeleteFromExtensionPaths: false
         #allowedExtensionPaths:
           #10: EXT:example/Resources/Private/Forms/
 
index cacc99f..9b2ee78 100644 (file)
@@ -71,7 +71,7 @@
                                                     </f:else>
                                                 </f:if>
                                                 <a href="#" data-identifier="duplicateForm" data-form-persistence-identifier="{form.persistenceIdentifier}" data-form-name="{form.name}" title="{f:translate(key: 'LLL:EXT:form/Resources/Private/Language/Database.xlf:formManager.duplicate_this_form')}" class="btn btn-default form-record-duplicate"><core:icon identifier="t3-form-icon-duplicate" /></a>
-                                                <f:if condition="{form.location} === 'storage'">
+                                                <f:if condition="{form.removable}">
                                                     <f:then>
                                                         <a href="#" data-identifier="removeForm" data-form-persistence-identifier="{form.persistenceIdentifier}" title="{f:translate(key: 'LLL:EXT:form/Resources/Private/Language/Database.xlf:formManager.delete_form')}" class="btn btn-default form-record-delete"><core:icon identifier="actions-edit-delete" /></a>
                                                     </f:then>
index 1468bc8..c2dfda2 100644 (file)
@@ -33,6 +33,9 @@
             <trans-unit id="formManager.newFormWizard.step1.title" xml:space="preserve">
                 <source>Create new form</source>
             </trans-unit>
+            <trans-unit id="formManager.newFormWizard.step1.noStorages" xml:space="preserve">
+                <source>No accessible form storage folders</source>
+            </trans-unit>
             <trans-unit id="formManager.newFormWizard.step1.advanced" xml:space="preserve">
                 <source>Advanced settings</source>
             </trans-unit>
index d379fe6..1d2bc79 100644 (file)
@@ -88,7 +88,18 @@ define(['jquery',
                     nextButton = modal.find('.modal-footer').find('button[name="next"]');
 
                     folders = _formManagerApp.getAccessibleFormStorageFolders();
-                    _formManagerApp.assert(folders.length > 0, 'No accessible form storage folders', 1477506500);
+                    if (folders.length === 0) {
+                        html = '<div class="new-form-modal">'
+                                 + '<div class="form-horizontal">'
+                                     + '<div>'
+                                         + '<label class="control-label">' + TYPO3.lang['formManager.newFormWizard.step1.noStorages'] + '</label>'
+                                     + '</div>'
+                                 + '</div>'
+                             + '</div>';
+
+                        slide.html(html);
+                        _formManagerApp.assert(false, 'No accessible form storage folders', 1477506500);
+                    }
 
                     Wizard.set('savePath', folders[0]['value']);
                     if (folders.length > 1) {
index b37e3e1..2a31c01 100644 (file)
@@ -69,6 +69,12 @@ class FormManagerControllerTest extends \TYPO3\CMS\Components\TestingFramework\C
             ->getMock();
         $mockController->_set('formPersistenceManager', $formPersistenceManagerProphecy->reveal());
 
+        $mockController->_set('formSettings', [
+            'persistenceManager' => [
+                'allowSaveToExtensionPaths' => true,
+            ],
+        ]);
+
         $folder1 = new Folder($mockStorage, '/user_upload/', 'user_upload');
         $folder2 = new Folder($mockStorage, '/forms/', 'forms');
 
@@ -77,6 +83,11 @@ class FormManagerControllerTest extends \TYPO3\CMS\Components\TestingFramework\C
             '2:/forms/' => $folder2,
         ]);
 
+        $formPersistenceManagerProphecy->getAccessibleExtensionFolders(Argument::cetera())->willReturn([
+            'EXT:form/Resources/Forms/' => '/some/path/form/Resources/Forms/',
+            'EXT:form_additions/Resources/Forms/' => '/some/path/form_additions/Resources/Forms/',
+        ]);
+
         $expected = [
             0 => [
                 'label' => 'user_upload',
@@ -86,6 +97,14 @@ class FormManagerControllerTest extends \TYPO3\CMS\Components\TestingFramework\C
                 'label' => 'forms',
                 'value' => '2:/forms/',
             ],
+            2 => [
+                'label' => 'EXT:form/Resources/Forms/',
+                'value' => 'EXT:form/Resources/Forms/',
+            ],
+            3 => [
+                'label' => 'EXT:form_additions/Resources/Forms/',
+                'value' => 'EXT:form_additions/Resources/Forms/',
+            ],
         ];
 
         $this->assertSame($expected, $mockController->_call('getAccessibleFormStorageFolders'));
@@ -182,6 +201,7 @@ class FormManagerControllerTest extends \TYPO3\CMS\Components\TestingFramework\C
                 'name' => 'some name',
                 'persistenceIdentifier' => '1:/user_uploads/someFormName.yaml',
                 'readOnly' => false,
+                'removable' => true,
                 'location' => 'storage',
                 'duplicateIdentifier' => false,
             ],
@@ -201,6 +221,7 @@ class FormManagerControllerTest extends \TYPO3\CMS\Components\TestingFramework\C
                 'name' => 'some name',
                 'persistenceIdentifier' => '1:/user_uploads/someFormName.yaml',
                 'readOnly' => false,
+                'removable' => true,
                 'location' => 'storage',
                 'duplicateIdentifier' => false,
                 'referenceCount' => 2,
index 8cd6e99..18a419a 100644 (file)
@@ -46,6 +46,28 @@ class FormPersistenceManagerTest extends \TYPO3\CMS\Components\TestingFramework\
     /**
      * @test
      */
+    public function loadThrowsExceptionIfPersistenceIdentifierIsAExtensionLocationWhichIsNotAllowed()
+    {
+        $this->expectException(PersistenceManagerException::class);
+        $this->expectExceptionCode(1484071985);
+
+        $mockFormPersistenceManager = $this->getAccessibleMock(FormPersistenceManager::class, [
+            'dummy'
+        ], [], '', false);
+
+        $mockFormPersistenceManager->_set('formSettings', [
+            'persistenceManager' => [
+                'allowedExtensionPaths' => [],
+            ],
+        ]);
+
+        $input = 'EXT:form/Resources/Forms/_example.yaml';
+        $mockFormPersistenceManager->_call('load', $input);
+    }
+
+    /**
+     * @test
+     */
     public function saveThrowsExceptionIfPersistenceIdentifierHasNoYamlExtension()
     {
         $this->expectException(PersistenceManagerException::class);
@@ -84,6 +106,29 @@ class FormPersistenceManagerTest extends \TYPO3\CMS\Components\TestingFramework\
     /**
      * @test
      */
+    public function saveThrowsExceptionIfPersistenceIdentifierIsAExtensionLocationWhichIsNotAllowed()
+    {
+        $this->expectException(PersistenceManagerException::class);
+        $this->expectExceptionCode(1484073571);
+
+        $mockFormPersistenceManager = $this->getAccessibleMock(FormPersistenceManager::class, [
+            'dummy'
+        ], [], '', false);
+
+        $mockFormPersistenceManager->_set('formSettings', [
+            'persistenceManager' => [
+                'allowSaveToExtensionPaths' => true,
+                'allowedExtensionPaths' => [],
+            ],
+        ]);
+
+        $input = 'EXT:form/Resources/Forms/_example.yaml';
+        $mockFormPersistenceManager->_call('save', $input, []);
+    }
+
+    /**
+     * @test
+     */
     public function deleteThrowsExceptionIfPersistenceIdentifierHasNoYamlExtension()
     {
         $this->expectException(PersistenceManagerException::class);
@@ -121,7 +166,7 @@ class FormPersistenceManagerTest extends \TYPO3\CMS\Components\TestingFramework\
     /**
      * @test
      */
-    public function deleteThrowsExceptionIfPersistenceIdentifierIsExtensionLocation()
+    public function deleteThrowsExceptionIfPersistenceIdentifierIsExtensionLocationAndDeleteFromExtensionLocationsIsNotAllowed()
     {
         $this->expectException(PersistenceManagerException::class);
         $this->expectExceptionCode(1472239536);
@@ -135,6 +180,40 @@ class FormPersistenceManagerTest extends \TYPO3\CMS\Components\TestingFramework\
             ->method('exists')
             ->willReturn(true);
 
+        $mockFormPersistenceManager->_set('formSettings', [
+            'persistenceManager' => [
+                'allowDeleteFromExtensionPaths' => false,
+            ],
+        ]);
+
+        $input = 'EXT:form/Resources/Forms/_example.yaml';
+        $mockFormPersistenceManager->_call('delete', $input);
+    }
+
+    /**
+     * @test
+     */
+    public function deleteThrowsExceptionIfPersistenceIdentifierIsExtensionLocationWhichIsNotAllowed()
+    {
+        $this->expectException(PersistenceManagerException::class);
+        $this->expectExceptionCode(1484073878);
+
+        $mockFormPersistenceManager = $this->getAccessibleMock(FormPersistenceManager::class, [
+            'exists'
+        ], [], '', false);
+
+        $mockFormPersistenceManager
+            ->expects($this->any())
+            ->method('exists')
+            ->willReturn(true);
+
+        $mockFormPersistenceManager->_set('formSettings', [
+            'persistenceManager' => [
+                'allowDeleteFromExtensionPaths' => true,
+                'allowedExtensionPaths' => [],
+            ],
+        ]);
+
         $input = 'EXT:form/Resources/Forms/_example.yaml';
         $mockFormPersistenceManager->_call('delete', $input);
     }
@@ -190,6 +269,14 @@ class FormPersistenceManagerTest extends \TYPO3\CMS\Components\TestingFramework\
             'dummy'
         ], [], '', false);
 
+        $mockFormPersistenceManager->_set('formSettings', [
+            'persistenceManager' => [
+                'allowedExtensionPaths' => [
+                    'EXT:form/Tests/Unit/Mvc/Persistence/Fixtures/'
+                ],
+            ],
+        ]);
+
         $input = 'EXT:form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.yaml';
         $this->assertTrue($mockFormPersistenceManager->_call('exists', $input));
     }
@@ -210,6 +297,25 @@ class FormPersistenceManagerTest extends \TYPO3\CMS\Components\TestingFramework\
     /**
      * @test
      */
+    public function existsReturnsFalseIfPersistenceIdentifierIsExtensionLocationAndFileExistsAndExtensionLocationIsNotAllowed()
+    {
+        $mockFormPersistenceManager = $this->getAccessibleMock(FormPersistenceManager::class, [
+            'dummy'
+        ], [], '', false);
+
+        $mockFormPersistenceManager->_set('formSettings', [
+            'persistenceManager' => [
+                'allowedExtensionPaths' => [],
+            ],
+        ]);
+
+        $input = 'EXT:form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.yaml';
+        $this->assertFalse($mockFormPersistenceManager->_call('exists', $input));
+    }
+
+    /**
+     * @test
+     */
     public function existsReturnsFalseIfPersistenceIdentifierIsExtensionLocationAndFileNotExistsAndFileHasYamlExtension()
     {
         $mockFormPersistenceManager = $this->getAccessibleMock(FormPersistenceManager::class, [