[FEATURE] Enhance form protection and add class for frontend 26/43426/12
authorHelmut Hummel <helmut.hummel@typo3.org>
Fri, 18 Sep 2015 15:24:03 +0000 (17:24 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Sat, 24 Oct 2015 14:48:54 +0000 (16:48 +0200)
This change adds API for CSRF protection in the frontend.
The usage is exactly the same as for backend modules, except
that the factory now returns a proper implementation for frontend.

The refactoring enabled a massive cleanup of the tests as the classes
now properly use dependency inversion.

Resolves: #56633
Releases: master
Change-Id: I7a9710215c38fda705fea62827419f63abdd2dc1
Reviewed-on: https://review.typo3.org/43426
Reviewed-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Tested-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Stephan GroƟberndt <stephan@grossberndt.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
12 files changed:
typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
typo3/sysext/core/Classes/FormProtection/AbstractFormProtection.php
typo3/sysext/core/Classes/FormProtection/BackendFormProtection.php
typo3/sysext/core/Classes/FormProtection/DisabledFormProtection.php
typo3/sysext/core/Classes/FormProtection/FormProtectionFactory.php
typo3/sysext/core/Classes/FormProtection/FrontendFormProtection.php [new file with mode: 0644]
typo3/sysext/core/Classes/FormProtection/InstallToolFormProtection.php
typo3/sysext/core/Documentation/Changelog/master/Feature-56633-FormProtectionAPIForFrontEndUsage.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php
typo3/sysext/core/Tests/Unit/FormProtection/BackendFormProtectionTest.php
typo3/sysext/core/Tests/Unit/FormProtection/Fixtures/FormProtectionTesting.php
typo3/sysext/core/Tests/Unit/FormProtection/FormProtectionFactoryTest.php

index 328b7e6..e819ade 100644 (file)
@@ -2547,7 +2547,7 @@ This is a dump of the failures:
      */
     public function logoff()
     {
-        if (isset($GLOBALS['BE_USER'])) {
+        if (isset($GLOBALS['BE_USER']) && $GLOBALS['BE_USER'] instanceof BackendUserAuthentication && isset($GLOBALS['BE_USER']->user['uid'])) {
             \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->clean();
         }
         parent::logoff();
index a4b715e..a879371 100644 (file)
@@ -26,6 +26,11 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 abstract class AbstractFormProtection
 {
     /**
+     * @var \Closure
+     */
+    protected $validationFailedCallback;
+
+    /**
      * The session token which is used to be hashed during token generation.
      *
      * @var string
@@ -124,12 +129,14 @@ abstract class AbstractFormProtection
      * Creates or displays an error message telling the user that the submitted
      * form token is invalid.
      *
-     * This function may also be empty if the validation error should be handled
-     * silently.
-     *
      * @return void
      */
-    abstract protected function createValidationErrorMessage();
+    protected function createValidationErrorMessage()
+    {
+        if ($this->validationFailedCallback !== null) {
+            $this->validationFailedCallback->__invoke();
+        }
+    }
 
     /**
      * Retrieves the session token.
index 0646f08..ce2d1c2 100644 (file)
@@ -25,7 +25,7 @@ namespace TYPO3\CMS\Core\FormProtection;
  * matter; you only need it to get the form token for verifying it.
  *
  * <pre>
- * $formToken = TYPO3\CMS\Core\FormProtection\BackendFormProtectionFactory::get()
+ * $formToken = TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
  * ->generateToken(
  * 'BE user setup', 'edit'
  * );
@@ -42,7 +42,7 @@ namespace TYPO3\CMS\Core\FormProtection;
  * For editing a tt_content record, the call could look like this:
  *
  * <pre>
- * $formToken = \TYPO3\CMS\Core\FormProtection\BackendFormProtectionFactory::get()
+ * $formToken = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
  * ->getFormProtection()->generateToken(
  * 'tt_content', 'edit', $uid
  * );
@@ -53,7 +53,7 @@ namespace TYPO3\CMS\Core\FormProtection;
  * that the form token is valid like this:
  *
  * <pre>
- * if ($dataHasBeenSubmitted && TYPO3\CMS\Core\FormProtection\BackendFormProtectionFactory::get()
+ * if ($dataHasBeenSubmitted && TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
  * ->validateToken(
  * \TYPO3\CMS\Core\Utility\GeneralUtility::_POST('formToken'),
  * 'BE user setup', 'edit
@@ -66,7 +66,9 @@ namespace TYPO3\CMS\Core\FormProtection;
  * }
  * </pre>
  */
-use TYPO3\CMS\Core\Messaging\FlashMessageService;
+
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Registry;
 
 /**
  * Backend form protection
@@ -77,7 +79,7 @@ class BackendFormProtection extends AbstractFormProtection
      * Keeps the instance of the user which existed during creation
      * of the object.
      *
-     * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
+     * @var BackendUserAuthentication
      */
     protected $backendUser;
 
@@ -85,55 +87,26 @@ class BackendFormProtection extends AbstractFormProtection
      * Instance of the registry, which is used to permanently persist
      * the session token so that it can be restored during re-login.
      *
-     * @var \TYPO3\CMS\Core\Registry
+     * @var Registry
      */
     protected $registry;
 
     /**
-     * Only allow construction if we have a backend session
+     * Only allow construction if we have an authorized backend session
      *
+     * @param BackendUserAuthentication $backendUser
+     * @param Registry $registry
+     * @param \Closure $validationFailedCallback
      * @throws \TYPO3\CMS\Core\Error\Exception
      */
-    public function __construct()
+    public function __construct(BackendUserAuthentication $backendUser, Registry $registry, \Closure $validationFailedCallback = null)
     {
+        $this->backendUser = $backendUser;
+        $this->registry = $registry;
+        $this->validationFailedCallback = $validationFailedCallback;
         if (!$this->isAuthorizedBackendSession()) {
-            throw new \TYPO3\CMS\Core\Error\Exception('A back-end form protection may only be instantiated if there' . ' is an active back-end session.', 1285067843);
+            throw new \TYPO3\CMS\Core\Error\Exception('A back-end form protection may only be instantiated if there is an active back-end session.', 1285067843);
         }
-        $this->backendUser = $GLOBALS['BE_USER'];
-    }
-
-    /**
-     * Creates or displays an error message telling the user that the submitted
-     * form token is invalid.
-     *
-     * @return void
-     */
-    protected function createValidationErrorMessage()
-    {
-        /** @var \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage */
-        $flashMessage = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
-            \TYPO3\CMS\Core\Messaging\FlashMessage::class,
-            $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:error.formProtection.tokenInvalid'),
-            '',
-            \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR,
-            !$this->isAjaxRequest()
-        );
-        /** @var $flashMessageService FlashMessageService */
-        $flashMessageService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(FlashMessageService::class);
-
-        /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
-        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
-        $defaultFlashMessageQueue->enqueue($flashMessage);
-    }
-
-    /**
-     * Checks if the current request is an Ajax request
-     *
-     * @return bool
-     */
-    protected function isAjaxRequest()
-    {
-        return (bool)(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX);
     }
 
     /**
@@ -143,7 +116,7 @@ class BackendFormProtection extends AbstractFormProtection
      */
     protected function retrieveSessionToken()
     {
-        $this->sessionToken = $this->backendUser->getSessionData('formSessionToken');
+        $this->sessionToken = $this->backendUser->getSessionData('formProtectionSessionToken');
         if (empty($this->sessionToken)) {
             $this->sessionToken = $this->generateSessionToken();
             $this->persistSessionToken();
@@ -160,7 +133,7 @@ class BackendFormProtection extends AbstractFormProtection
      */
     public function persistSessionToken()
     {
-        $this->backendUser->setAndSaveSessionData('formSessionToken', $this->sessionToken);
+        $this->backendUser->setAndSaveSessionData('formProtectionSessionToken', $this->sessionToken);
     }
 
     /**
@@ -173,7 +146,7 @@ class BackendFormProtection extends AbstractFormProtection
      */
     public function setSessionTokenFromRegistry()
     {
-        $this->sessionToken = $this->getRegistry()->get('core', 'formSessionToken:' . $this->backendUser->user['uid']);
+        $this->sessionToken = $this->registry->get('core', 'formProtectionSessionToken:' . $this->backendUser->user['uid']);
         if (empty($this->sessionToken)) {
             throw new \UnexpectedValueException('Failed to restore the session token from the registry.', 1301827270);
         }
@@ -189,7 +162,7 @@ class BackendFormProtection extends AbstractFormProtection
      */
     public function storeSessionTokenInRegistry()
     {
-        $this->getRegistry()->set('core', 'formSessionToken:' . $this->backendUser->user['uid'], $this->getSessionToken());
+        $this->registry->set('core', 'formProtectionSessionToken:' . $this->backendUser->user['uid'], $this->getSessionToken());
     }
 
     /**
@@ -199,32 +172,7 @@ class BackendFormProtection extends AbstractFormProtection
      */
     public function removeSessionTokenFromRegistry()
     {
-        $this->getRegistry()->remove('core', 'formSessionToken:' . $this->backendUser->user['uid']);
-    }
-
-    /**
-     * Returns the instance of the registry.
-     *
-     * @return \TYPO3\CMS\Core\Registry
-     */
-    protected function getRegistry()
-    {
-        if (!$this->registry instanceof \TYPO3\CMS\Core\Registry) {
-            $this->registry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
-        }
-        return $this->registry;
-    }
-
-    /**
-     * Inject the registry. Currently only used in unit tests.
-     *
-     * @access private
-     * @param \TYPO3\CMS\Core\Registry $registry
-     * @return void
-     */
-    public function injectRegistry(\TYPO3\CMS\Core\Registry $registry)
-    {
-        $this->registry = $registry;
+        $this->registry->remove('core', 'formProtectionSessionToken:' . $this->backendUser->user['uid']);
     }
 
     /**
@@ -234,16 +182,6 @@ class BackendFormProtection extends AbstractFormProtection
      */
     protected function isAuthorizedBackendSession()
     {
-        return isset($GLOBALS['BE_USER']) && $GLOBALS['BE_USER'] instanceof \TYPO3\CMS\Core\Authentication\BackendUserAuthentication && isset($GLOBALS['BE_USER']->user['uid']);
-    }
-
-    /**
-     * Return language service instance
-     *
-     * @return \TYPO3\CMS\Lang\LanguageService
-     */
-    protected function getLanguageService()
-    {
-        return $GLOBALS['LANG'];
+        return !empty($this->backendUser->user['uid']);
     }
 }
index bcc862f..5815277 100644 (file)
@@ -21,14 +21,12 @@ namespace TYPO3\CMS\Core\FormProtection;
 class DisabledFormProtection extends AbstractFormProtection
 {
     /**
-     * Disable parent constructor
-     */
-    public function __construct()
-    {
-    }
-
-    /**
      * Disable parent method
+     *
+     * @param string $formName
+     * @param string $action
+     * @param string $formInstanceName
+     * @return string
      */
     public function generateToken($formName, $action = '', $formInstanceName = '')
     {
@@ -53,13 +51,6 @@ class DisabledFormProtection extends AbstractFormProtection
     /**
      * Dummy implementation
      */
-    protected function createValidationErrorMessage()
-    {
-    }
-
-    /**
-     * Dummy implementation
-     */
     protected function retrieveSessionToken()
     {
     }
index 80034c8..56b2e60 100644 (file)
@@ -14,6 +14,12 @@ namespace TYPO3\CMS\Core\FormProtection;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
+use TYPO3\CMS\Core\Messaging\FlashMessageService;
+use TYPO3\CMS\Core\Registry;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
 /**
  * This class creates and manages instances of the various form protection
  * classes.
@@ -58,14 +64,17 @@ class FormProtectionFactory
      * @param string $className
      * @return \TYPO3\CMS\Core\FormProtection\AbstractFormProtection the requested instance
      */
-    public static function get($className = null)
+    public static function get($className = 'default')
     {
-        if ($className === null) {
-            $className = self::getClassNameByState();
+        if (isset(self::$instances[$className])) {
+            return self::$instances[$className];
         }
-        if (!isset(self::$instances[$className])) {
-            self::createAndStoreInstance($className);
+        if ($className === 'default') {
+            $classNameAndConstructorArguments = self::getClassNameAndConstructorArgumentsByState();
+        } else {
+            $classNameAndConstructorArguments = func_get_args();
         }
+        self::$instances[$className] = self::createInstance($classNameAndConstructorArguments);
         return self::$instances[$className];
     }
 
@@ -73,22 +82,40 @@ class FormProtectionFactory
      * Returns the class name depending on TYPO3_MODE and
      * active backend session.
      *
-     * @return string
+     * @return array
      */
-    protected static function getClassNameByState()
+    protected static function getClassNameAndConstructorArgumentsByState()
     {
         switch (true) {
             case self::isInstallToolSession():
-                $className = \TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class;
+                $classNameAndConstructorArguments = [
+                    InstallToolFormProtection::class
+                ];
+                break;
+            case self::isFrontendSession():
+                $classNameAndConstructorArguments = [
+                    FrontendFormProtection::class,
+                    $GLOBALS['TSFE']->fe_user
+                ];
                 break;
             case self::isBackendSession():
-                $className = \TYPO3\CMS\Core\FormProtection\BackendFormProtection::class;
+                $classNameAndConstructorArguments = [
+                    BackendFormProtection::class,
+                    $GLOBALS['BE_USER'],
+                    GeneralUtility::makeInstance(Registry::class),
+                    self::getMessageClosure(
+                        $GLOBALS['LANG'],
+                        GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier(),
+                        (bool)(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX)
+                    )
+                ];
                 break;
-            case self::isFrontendSession():
             default:
-                $className = \TYPO3\CMS\Core\FormProtection\DisabledFormProtection::class;
+                $classNameAndConstructorArguments = [
+                    DisabledFormProtection::class
+                ];
         }
-        return $className;
+        return $classNameAndConstructorArguments;
     }
 
     /**
@@ -118,26 +145,50 @@ class FormProtectionFactory
      */
     protected static function isFrontendSession()
     {
-        return is_object($GLOBALS['TSFE']) && $GLOBALS['TSFE']->fe_user instanceof \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication && isset($GLOBALS['TSFE']->fe_user->user['uid']) && TYPO3_MODE === 'FE';
+        return TYPO3_MODE === 'FE' && is_object($GLOBALS['TSFE']) && $GLOBALS['TSFE']->fe_user instanceof \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication && isset($GLOBALS['TSFE']->fe_user->user['uid']);
+    }
+
+    /**
+     * @param LanguageService $languageService
+     * @param FlashMessageQueue $messageQueue
+     * @param bool $isAjaxCall
+     * @internal Only public to be used in tests
+     * @return \Closure
+     */
+    public static function getMessageClosure(LanguageService $languageService, FlashMessageQueue $messageQueue, $isAjaxCall)
+    {
+        return function () use ($languageService, $messageQueue, $isAjaxCall) {
+            /** @var \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage */
+            $flashMessage = GeneralUtility::makeInstance(
+                \TYPO3\CMS\Core\Messaging\FlashMessage::class,
+                $languageService->sL('LLL:EXT:lang/locallang_core.xlf:error.formProtection.tokenInvalid'),
+                '',
+                \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR,
+                !$isAjaxCall
+            );
+            $messageQueue->enqueue($flashMessage);
+        };
     }
 
     /**
      * Creates an instance for the requested class $className
      * and stores it internally.
      *
-     * @param string $className
+     * @param array $classNameAndConstructorArguments
      * @throws \InvalidArgumentException
+     * @return AbstractFormProtection
      */
-    protected static function createAndStoreInstance($className)
+    protected static function createInstance(array $classNameAndConstructorArguments)
     {
-        if (!class_exists($className, true)) {
+        $className = $classNameAndConstructorArguments[0];
+        if (!class_exists($className)) {
             throw new \InvalidArgumentException('$className must be the name of an existing class, but ' . 'actually was "' . $className . '".', 1285352962);
         }
-        $instance = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($className);
+        $instance = call_user_func_array([\TYPO3\CMS\Core\Utility\GeneralUtility::class, 'makeInstance'], $classNameAndConstructorArguments);
         if (!$instance instanceof AbstractFormProtection) {
-            throw new \InvalidArgumentException('$className must be a subclass of ' . \TYPO3\CMS\Core\FormProtection\AbstractFormProtection::class . ', but actually was "' . $className . '".', 1285353026);
+            throw new \InvalidArgumentException('$className must be a subclass of ' . AbstractFormProtection::class . ', but actually was "' . $className . '".', 1285353026);
         }
-        self::$instances[$className] = $instance;
+        return $instance;
     }
 
     /**
@@ -166,7 +217,6 @@ class FormProtectionFactory
     public static function purgeInstances()
     {
         foreach (self::$instances as $key => $instance) {
-            $instance->__destruct();
             unset(self::$instances[$key]);
         }
     }
diff --git a/typo3/sysext/core/Classes/FormProtection/FrontendFormProtection.php b/typo3/sysext/core/Classes/FormProtection/FrontendFormProtection.php
new file mode 100644 (file)
index 0000000..0d1e526
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+namespace TYPO3\CMS\Core\FormProtection;
+
+/*
+ * 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!
+ */
+
+/**
+ * This class provides protection against cross-site request forgery (XSRF/CSRF)
+ * for actions in the frontend that change data.
+ *
+ * How to use:
+ *
+ * For each form (or link that changes some data), create a token and
+ * insert is as a hidden form element or use it as GET argument. The name of the form element does not
+ * matter; you only need it to get the form token for verifying it.
+ *
+ * <pre>
+ * $formToken = TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
+ * ->generateToken(
+ * 'User setup', 'edit'
+ * );
+ * $this->content .= '<input type="hidden" name="formToken" value="' .
+ * $formToken . '" />';
+ * </pre>
+ *
+ * The three parameters $formName, $action and $formInstanceName can be
+ * arbitrary strings, but they should make the form token as specific as
+ * possible. For different forms (e.g. User setup and editing a news
+ * record) or different records (with different UIDs) from the same table,
+ * those values should be different.
+ *
+ * For editing a news record, the call could look like this:
+ *
+ * <pre>
+ * $formToken = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
+ * ->getFormProtection()->generateToken(
+ * 'news', 'edit', $uid
+ * );
+ * </pre>
+ *
+ *
+ * When processing the data that has been submitted by the form, you can check
+ * that the form token is valid like this:
+ *
+ * <pre>
+ * if ($dataHasBeenSubmitted && \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
+ * ->validateToken(
+ * \TYPO3\CMS\Core\Utility\GeneralUtility::_POST('formToken'),
+ * 'User setup', 'edit
+ * )
+ * ) {
+ * Processes the data.
+ * } else {
+ * Create a flash message for the invalid token or just discard this request.
+ * }
+ * </pre>
+ */
+
+use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+
+/**
+ * Frontend form protection
+ */
+class FrontendFormProtection extends AbstractFormProtection
+{
+    /**
+     * Keeps the instance of the user which existed during creation
+     * of the object.
+     *
+     * @var FrontendUserAuthentication
+     */
+    protected $frontendUser;
+
+    /**
+     * Only allow construction if we have an authorized frontend session
+     *
+     * @param FrontendUserAuthentication $frontendUser
+     * @param \Closure $validationFailedCallback
+     * @throws \TYPO3\CMS\Core\Error\Exception
+     */
+    public function __construct(FrontendUserAuthentication $frontendUser, \Closure $validationFailedCallback = null)
+    {
+        $this->frontendUser = $frontendUser;
+        $this->validationFailedCallback = $validationFailedCallback;
+        if (!$this->isAuthorizedFrontendSession()) {
+            throw new \TYPO3\CMS\Core\Error\Exception('A front-end form protection may only be instantiated if there is an active front-end session.', 1285067843);
+        }
+    }
+
+    /**
+     * Retrieves the saved session token or generates a new one.
+     *
+     * @return string
+     */
+    protected function retrieveSessionToken()
+    {
+        $this->sessionToken = $this->frontendUser->getSessionData('formProtectionSessionToken');
+        if (empty($this->sessionToken)) {
+            $this->sessionToken = $this->generateSessionToken();
+            $this->persistSessionToken();
+        }
+        return $this->sessionToken;
+    }
+
+    /**
+     * Saves the tokens so that they can be used by a later incarnation of this
+     * class.
+     *
+     * @access private
+     * @return void
+     */
+    public function persistSessionToken()
+    {
+        $this->frontendUser->setAndSaveSessionData('formProtectionSessionToken', $this->sessionToken);
+    }
+
+    /**
+     * Checks if a user is logged in and the session is active.
+     *
+     * @return bool
+     */
+    protected function isAuthorizedFrontendSession()
+    {
+        return !empty($this->frontendUser->user['uid']);
+    }
+
+}
index 7c82b07..d7cc3f3 100644 (file)
@@ -59,16 +59,6 @@ namespace TYPO3\CMS\Core\FormProtection;
 class InstallToolFormProtection extends AbstractFormProtection
 {
     /**
-     * Creates or displays an error message telling the user that the submitted
-     * form token is invalid.
-     *
-     * @return void
-     */
-    protected function createValidationErrorMessage()
-    {
-    }
-
-    /**
      * Retrieves or generates the session token.
      *
      * @return void
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-56633-FormProtectionAPIForFrontEndUsage.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-56633-FormProtectionAPIForFrontEndUsage.rst
new file mode 100644 (file)
index 0000000..efb1f6b
--- /dev/null
@@ -0,0 +1,35 @@
+========================================================
+Feature: #56633 - Form protection API for frontend usage
+========================================================
+
+Description
+===========
+
+As of now frontend plugins needed to implement CSRF protection on their own. This change introduces a new class to allow usage of the FormProtection (CSRF protection) API in the frontend.
+
+Usage is the same as in backend context:
+
+.. code-block:: php
+
+       $formToken = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()
+               ->getFormProtection()->generateToken('news', 'edit', $uid);
+
+
+       if (
+               $dataHasBeenSubmitted
+               && \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->validateToken(
+                       \TYPO3\CMS\Core\Utility\GeneralUtility::_POST('formToken'),
+                       'User setup',
+                       'edit'
+               )
+       ) {
+               // Processes the data.
+       } else {
+               // Create a flash message for the invalid token or just discard this request.
+       }
+
+
+Impact
+======
+
+FormProtection API can now also be used in frontend context.
\ No newline at end of file
index 12ed856..cfb710e 100644 (file)
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Tests\Unit\Authentication;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Lang\LanguageService;
+
 /**
  * Testcase for \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
  */
@@ -73,7 +75,7 @@ class BackendUserAuthenticationTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $formProtection->expects($this->once())->method('clean');
 
         \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::set(
-            \TYPO3\CMS\Core\FormProtection\BackendFormProtection::class,
+            'default',
             $formProtection
         );
 
index 800fedd..f4a7cf3 100644 (file)
@@ -14,6 +14,10 @@ namespace TYPO3\CMS\Core\Tests\Unit\FormProtection;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\FormProtection\BackendFormProtection;
+use TYPO3\CMS\Core\Registry;
+
 /**
  * Testcase
  */
@@ -25,77 +29,43 @@ class BackendFormProtectionTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
     protected $subject;
 
     /**
-     * Backup of current singleton instances
+     * @var BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject
      */
-    protected $singletonInstances;
+    protected $backendUserMock;
 
     /**
-     * Set up
+     * @var Registry|\PHPUnit_Framework_MockObject_MockObject
      */
-    protected function setUp()
-    {
-        $this->singletonInstances = \TYPO3\CMS\Core\Utility\GeneralUtility::getSingletonInstances();
-
-        $GLOBALS['BE_USER'] = $this->getMock(
-            \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class,
-            array('getSessionData', 'setAndSaveSessionData')
-        );
-        $GLOBALS['BE_USER']->user['uid'] = 1;
-
-        $this->subject = $this->getAccessibleMock(
-            \TYPO3\CMS\Core\FormProtection\BackendFormProtection::class,
-            array('acquireLock', 'releaseLock', 'getLanguageService', 'isAjaxRequest')
-        );
-    }
-
-    protected function tearDown()
-    {
-        \TYPO3\CMS\Core\Utility\GeneralUtility::resetSingletonInstances($this->singletonInstances);
-        parent::tearDown();
-    }
-
-    //////////////////////
-    // Utility functions
-    //////////////////////
+    protected $registryMock;
 
     /**
-     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject
-     */
-    protected function getBackendUser()
-    {
-        return $GLOBALS['BE_USER'];
-    }
-
-    ////////////////////////////////////
-    // Tests for the utility functions
-    ////////////////////////////////////
-
-    /**
-     * @test
+     * Set up
      */
-    public function getBackendUserReturnsInstanceOfBackendUserAuthenticationClass()
+    protected function setUp()
     {
-        $this->assertInstanceOf(
-            \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class,
-            $this->getBackendUser()
+        $this->backendUserMock = $this->getMock(\TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class);
+        $this->backendUserMock->user['uid'] = 1;
+        $this->registryMock = $this->getMock(Registry::class);
+        $this->subject = new BackendFormProtection(
+            $this->backendUserMock,
+            $this->registryMock,
+            function () {
+                throw new \Exception('Closure called', 1442592030);
+            }
         );
     }
 
-    //////////////////////////////////////////////////////////
-    // Tests concerning the reading and saving of the tokens
-    //////////////////////////////////////////////////////////
-
     /**
      * @test
      */
-    public function retrieveTokenReadsTokenFromSessionData()
+    public function generateTokenReadsTokenFromSessionData()
     {
-        $this->getBackendUser()
+        $this->backendUserMock
             ->expects($this->once())
             ->method('getSessionData')
-            ->with('formSessionToken')
+            ->with('formProtectionSessionToken')
             ->will($this->returnValue(array()));
-        $this->subject->_call('retrieveSessionToken');
+        $this->subject->generateToken('foo');
     }
 
     /**
@@ -112,14 +82,12 @@ class BackendFormProtectionTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
             $formName . $action . $formInstanceName . $sessionToken
         );
 
-        $this->getBackendUser()
+        $this->backendUserMock
             ->expects($this->atLeastOnce())
             ->method('getSessionData')
-            ->with('formSessionToken')
+            ->with('formProtectionSessionToken')
             ->will($this->returnValue($sessionToken));
 
-        $this->subject->_call('retrieveSessionToken');
-
         $this->assertTrue(
             $this->subject->validateToken($tokenId, $formName, $action, $formInstanceName)
         );
@@ -131,9 +99,6 @@ class BackendFormProtectionTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function restoreSessionTokenFromRegistryThrowsExceptionIfSessionTokenIsEmpty()
     {
-        /** @var $registryMock \TYPO3\CMS\Core\Registry */
-        $registryMock = $this->getMock(\TYPO3\CMS\Core\Registry::class);
-        $this->subject->injectRegistry($registryMock);
         $this->subject->setSessionTokenFromRegistry();
     }
 
@@ -142,104 +107,20 @@ class BackendFormProtectionTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function persistSessionTokenWritesTokenToSession()
     {
-        $sessionToken = $this->getUniqueId('test_');
-        $this->subject->_set('sessionToken', $sessionToken);
-        $this->getBackendUser()
+        $this->backendUserMock
             ->expects($this->once())
-            ->method('setAndSaveSessionData')
-            ->with('formSessionToken', $sessionToken);
+            ->method('setAndSaveSessionData');
         $this->subject->persistSessionToken();
     }
 
-
-    //////////////////////////////////////////////////
-    // Tests concerning createValidationErrorMessage
-    //////////////////////////////////////////////////
-
     /**
      * @test
+     * @expectedException \Exception
+     * @expectedExceptionCode 1442592030
      */
-    public function createValidationErrorMessageAddsFlashMessage()
+    public function failingTokenValidationInvokesFailingTokenClosure()
     {
-        /** @var $flashMessageServiceMock \TYPO3\CMS\Core\Messaging\FlashMessageService|\PHPUnit_Framework_MockObject_MockObject */
-        $flashMessageServiceMock = $this->getMock(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
-        \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(
-            \TYPO3\CMS\Core\Messaging\FlashMessageService::class,
-            $flashMessageServiceMock
-        );
-        $flashMessageQueueMock = $this->getMock(
-            \TYPO3\CMS\Core\Messaging\FlashMessageQueue::class,
-            array(),
-            array(),
-            '',
-            false
-        );
-        $flashMessageServiceMock
-            ->expects($this->once())
-            ->method('getMessageQueueByIdentifier')
-            ->will($this->returnValue($flashMessageQueueMock));
-        $flashMessageQueueMock
-            ->expects($this->once())
-            ->method('enqueue')
-            ->with($this->isInstanceOf(\TYPO3\CMS\Core\Messaging\FlashMessage::class))
-            ->will($this->returnCallback(array($this, 'enqueueFlashMessageCallback')));
-
-        $languageServiceMock = $this->getMock(\TYPO3\CMS\Lang\LanguageService::class, array(), array(), '', false);
-        $languageServiceMock->expects($this->once())->method('sL')->will($this->returnValue('foo'));
-        $this->subject->expects($this->once())->method('getLanguageService')->will($this->returnValue($languageServiceMock));
-
-        $this->subject->_call('createValidationErrorMessage');
-    }
-
-    /**
-     * @param \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage
-     */
-    public function enqueueFlashMessageCallback(\TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage)
-    {
-        $this->assertEquals(\TYPO3\CMS\Core\Messaging\FlashMessage::ERROR, $flashMessage->getSeverity());
-    }
-
-    /**
-     * @test
-     */
-    public function createValidationErrorMessageAddsErrorFlashMessageButNotInSessionInAjaxRequest()
-    {
-        /** @var $flashMessageServiceMock \TYPO3\CMS\Core\Messaging\FlashMessageService|\PHPUnit_Framework_MockObject_MockObject */
-        $flashMessageServiceMock = $this->getMock(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
-        \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(
-            \TYPO3\CMS\Core\Messaging\FlashMessageService::class,
-            $flashMessageServiceMock
-        );
-        $flashMessageQueueMock = $this->getMock(
-            \TYPO3\CMS\Core\Messaging\FlashMessageQueue::class,
-            array(),
-            array(),
-            '',
-            false
-        );
-        $flashMessageServiceMock
-            ->expects($this->once())
-            ->method('getMessageQueueByIdentifier')
-            ->will($this->returnValue($flashMessageQueueMock));
-        $flashMessageQueueMock
-            ->expects($this->once())
-            ->method('enqueue')
-            ->with($this->isInstanceOf(\TYPO3\CMS\Core\Messaging\FlashMessage::class))
-            ->will($this->returnCallback(array($this, 'enqueueAjaxFlashMessageCallback')));
-
-        $languageServiceMock = $this->getMock(\TYPO3\CMS\Lang\LanguageService::class, array(), array(), '', false);
-        $languageServiceMock->expects($this->once())->method('sL')->will($this->returnValue('foo'));
-        $this->subject->expects($this->once())->method('getLanguageService')->will($this->returnValue($languageServiceMock));
-
-        $this->subject->expects($this->any())->method('isAjaxRequest')->will($this->returnValue(true));
-        $this->subject->_call('createValidationErrorMessage');
+        $this->subject->validateToken('foo', 'bar');
     }
 
-    /**
-     * @param \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage
-     */
-    public function enqueueAjaxFlashMessageCallback(\TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage)
-    {
-        $this->assertFalse($flashMessage->isSessionMessage());
-    }
 }
index fe4d078..6e34d29 100644 (file)
@@ -23,16 +23,6 @@ namespace TYPO3\CMS\Core\Tests\Unit\FormProtection\Fixtures;
 class FormProtectionTesting extends \TYPO3\CMS\Core\FormProtection\AbstractFormProtection
 {
     /**
-     * Creates or displayes an error message telling the user that the submitted
-     * form token is invalid.
-     *
-     * @return void
-     */
-    protected function createValidationErrorMessage()
-    {
-    }
-
-    /**
      * Retrieves all saved tokens.
      *
      * @return string The saved token
index 91150c7..0a2fe1c 100644 (file)
@@ -14,6 +14,9 @@ namespace TYPO3\CMS\Core\Tests\Unit\FormProtection;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
+use TYPO3\CMS\Core\Registry;
+
 /**
  * Testcase
  */
@@ -25,7 +28,7 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
 
     protected function tearDown()
     {
-        \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::purgeInstances();
+        FormProtectionFactory::purgeInstances();
         parent::tearDown();
     }
 
@@ -36,9 +39,9 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      * @test
      * @expectedException \InvalidArgumentException
      */
-    public function getForInexistentClassThrowsException()
+    public function getForNotExistingClassThrowsException()
     {
-        \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get('noSuchClass');
+        FormProtectionFactory::get('noSuchClass');
     }
 
     /**
@@ -47,7 +50,7 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function getForClassThatIsNoFormProtectionSubclassThrowsException()
     {
-        \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\FormProtectionFactoryTest::class);
+        FormProtectionFactory::get(\TYPO3\CMS\Core\Tests\Unit\FormProtection\FormProtectionFactoryTest::class);
     }
 
     /**
@@ -55,9 +58,16 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function getForTypeBackEndWithExistingBackEndReturnsBackEndFormProtection()
     {
-        $GLOBALS['BE_USER'] = $this->getMock(\TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class, array(), array(), '', false);
-        $GLOBALS['BE_USER']->user = array('uid' => $this->getUniqueId());
-        $this->assertTrue(\TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class) instanceof \TYPO3\CMS\Core\FormProtection\BackendFormProtection);
+        $userMock = $this->getMock(\TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class, array(), array(), '', false);
+        $userMock->user = array('uid' => $this->getUniqueId());
+        $this->assertInstanceOf(
+            \TYPO3\CMS\Core\FormProtection\BackendFormProtection::class,
+            FormProtectionFactory::get(
+                \TYPO3\CMS\Core\FormProtection\BackendFormProtection::class,
+                $userMock,
+                $this->getMock(Registry::class)
+            )
+        );
     }
 
     /**
@@ -65,9 +75,17 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function getForTypeBackEndCalledTwoTimesReturnsTheSameInstance()
     {
-        $GLOBALS['BE_USER'] = $this->getMock(\TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class, array(), array(), '', false);
-        $GLOBALS['BE_USER']->user = array('uid' => $this->getUniqueId());
-        $this->assertSame(\TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class), \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class));
+        $userMock = $this->getMock(\TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class, array(), array(), '', false);
+        $userMock->user = array('uid' => $this->getUniqueId());
+        $arguments = [
+            \TYPO3\CMS\Core\FormProtection\BackendFormProtection::class,
+            $userMock,
+            $this->getMock(Registry::class)
+        ];
+        $this->assertSame(
+            call_user_func_array([FormProtectionFactory::class, 'get'], $arguments),
+            call_user_func_array([FormProtectionFactory::class, 'get'], $arguments)
+        );
     }
 
     /**
@@ -75,7 +93,7 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function getForTypeInstallToolReturnsInstallToolFormProtection()
     {
-        $this->assertTrue(\TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class) instanceof \TYPO3\CMS\Core\FormProtection\InstallToolFormProtection);
+        $this->assertTrue(FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class) instanceof \TYPO3\CMS\Core\FormProtection\InstallToolFormProtection);
     }
 
     /**
@@ -83,17 +101,15 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     public function getForTypeInstallToolCalledTwoTimesReturnsTheSameInstance()
     {
-        $this->assertSame(\TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class), \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class));
+        $this->assertSame(FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class), FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class));
     }
 
     /**
      * @test
      */
-    public function getForTypesInstallToolAndBackEndReturnsDifferentInstances()
+    public function getForTypesInstallToolAndDisabledReturnsDifferentInstances()
     {
-        $GLOBALS['BE_USER'] = $this->getMock(\TYPO3\CMS\Core\Authentication\BackendUserAuthentication::class, array(), array(), '', false);
-        $GLOBALS['BE_USER']->user = array('uid' => $this->getUniqueId());
-        $this->assertNotSame(\TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class), \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class));
+        $this->assertNotSame(FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class), FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\DisabledFormProtection::class));
     }
 
     /////////////////////////
@@ -105,8 +121,8 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
     public function setSetsInstanceForType()
     {
         $instance = new \TYPO3\CMS\Core\Tests\Unit\FormProtection\Fixtures\FormProtectionTesting();
-        \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::set(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class, $instance);
-        $this->assertSame($instance, \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class));
+        FormProtectionFactory::set(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class, $instance);
+        $this->assertSame($instance, FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class));
     }
 
     /**
@@ -115,7 +131,39 @@ class FormProtectionFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
     public function setNotSetsInstanceForOtherType()
     {
         $instance = new \TYPO3\CMS\Core\Tests\Unit\FormProtection\Fixtures\FormProtectionTesting();
-        \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::set(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class, $instance);
-        $this->assertNotSame($instance, \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class));
+        FormProtectionFactory::set(\TYPO3\CMS\Core\FormProtection\BackendFormProtection::class, $instance);
+        $this->assertNotSame($instance, FormProtectionFactory::get(\TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class));
+    }
+
+    /**
+     * @test
+     */
+    public function createValidationErrorMessageAddsErrorFlashMessageButNotInSessionInAjaxRequest()
+    {
+        $flashMessageQueueMock = $this->getMock(
+            \TYPO3\CMS\Core\Messaging\FlashMessageQueue::class,
+            array(),
+            array(),
+            '',
+            false
+        );
+        $flashMessageQueueMock
+            ->expects($this->once())
+            ->method('enqueue')
+            ->with($this->isInstanceOf(\TYPO3\CMS\Core\Messaging\FlashMessage::class))
+            ->will($this->returnCallback(array($this, 'enqueueAjaxFlashMessageCallback')));
+        $languageServiceMock = $this->getMock(\TYPO3\CMS\Lang\LanguageService::class, array(), array(), '', false);
+        $languageServiceMock->expects($this->once())->method('sL')->will($this->returnValue('foo'));
+
+        FormProtectionFactory::getMessageClosure($languageServiceMock, $flashMessageQueueMock, true)->__invoke();
     }
+
+    /**
+     * @param \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage
+     */
+    public function enqueueAjaxFlashMessageCallback(\TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage)
+    {
+        $this->assertFalse($flashMessage->isSessionMessage());
+    }
+
 }