TYPO3 API  SVNRelease
class.t3lib_formprotection_backendformprotection.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003  * Copyright notice
00004  *
00005  * (c) 2010-2011 Oliver Klee <typo3-coding@oliverklee.de>
00006  * All rights reserved
00007  *
00008  * This script is part of the TYPO3 project. The TYPO3 project is
00009  * free software; you can redistribute it and/or modify
00010  * it under the terms of the GNU General Public License as published by
00011  * the Free Software Foundation; either version 2 of the License, or
00012  * (at your option) any later version.
00013  *
00014  * The GNU General Public License can be found at
00015  * http://www.gnu.org/copyleft/gpl.html.
00016  *
00017  * This script is distributed in the hope that it will be useful,
00018  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00019  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00020  * GNU General Public License for more details.
00021  *
00022  * This copyright notice MUST APPEAR in all copies of the script!
00023  ***************************************************************/
00024 
00025 /**
00026  * Class t3lib_formprotection_BackendFormProtection.
00027  *
00028  * This class provides protection against cross-site request forgery (XSRF/CSRF)
00029  * for forms in the BE.
00030  *
00031  * How to use:
00032  *
00033  * For each form in the BE (or link that changes some data), create a token and
00034  * insert is as a hidden form element. The name of the form element does not
00035  * matter; you only need it to get the form token for verifying it.
00036  *
00037  * <pre>
00038  * $formToken = t3lib_formprotection_Factory::get(
00039  *   t3lib_formprotection_Factory::TYPE_BACK_END
00040  * )->generateToken(
00041  *   'BE user setup', 'edit'
00042  * );
00043  * $this->content .= '<input type="hidden" name="formToken" value="' .
00044  *   $formToken . '" />';
00045  * </pre>
00046  *
00047  * The three parameters $formName, $action and $formInstanceName can be
00048  * arbitrary strings, but they should make the form token as specific as
00049  * possible. For different forms (e.g. BE user setup and editing a tt_content
00050  * record) or different records (with different UIDs) from the same table,
00051  * those values should be different.
00052  *
00053  * For editing a tt_content record, the call could look like this:
00054  *
00055  * <pre>
00056  * $formToken = t3lib_formprotection_Factory::get(
00057  *   t3lib_formprotection_Factory::TYPE_BACK_END
00058  * )->getFormProtection()->generateToken(
00059  *  'tt_content', 'edit', $uid
00060  * );
00061  * </pre>
00062  *
00063  * At the end of the form, you need to persist the tokens. This makes sure that
00064  * generated tokens get saved, and also that removed tokens stay removed:
00065  *
00066  * <pre>
00067  * t3lib_formprotection_Factory::get(
00068  *   t3lib_formprotection_Factory::TYPE_BACK_END
00069  * )->persistTokens();
00070  * </pre>
00071  *
00072  * In BE lists, it might be necessary to generate hundreds of tokens. So the
00073  * tokens do not get automatically persisted after creation for performance
00074  * reasons.
00075  *
00076  *
00077  * When processing the data that has been submitted by the form, you can check
00078  * that the form token is valid like this:
00079  *
00080  * <pre>
00081  * if ($dataHasBeenSubmitted && t3lib_formprotection_Factory::get(
00082  *       t3lib_formprotection_Factory::TYPE_BACK_END
00083  *   )->validateToken(
00084  *       (string) t3lib_div::_POST('formToken'),
00085  *       'BE user setup', 'edit
00086  *   )
00087  * ) {
00088  *   // processes the data
00089  * } else {
00090  *   // no need to do anything here as the BE form protection will create a
00091  *   // flash message for an invalid token
00092  * }
00093  * </pre>
00094  *
00095  * Note that validateToken invalidates the token with the token ID. So calling
00096  * validate with the same parameters two times in a row will always return FALSE
00097  * for the second call.
00098  *
00099  * It is important that the tokens get validated <em>before</em> the tokens are
00100  * persisted. This makes sure that the tokens that get invalidated by
00101  * validateToken cannot be used again.
00102  *
00103  * $Id$
00104  *
00105  * @package TYPO3
00106  * @subpackage t3lib
00107  *
00108  * @author Oliver Klee <typo3-coding@oliverklee.de>
00109  */
00110 class t3lib_formprotection_BackendFormProtection extends t3lib_formprotection_Abstract {
00111     /**
00112      * the maximum number of tokens that can exist at the same time
00113      *
00114      * @var integer
00115      */
00116     protected $maximumNumberOfTokens = 20000;
00117 
00118     /**
00119      * Keeps the instance of the user which existed during creation
00120      * of the object.
00121      *
00122      * @var t3lib_beUserAuth
00123      */
00124     protected $backendUser;
00125 
00126     /**
00127      * Only allow construction if we have a backend session
00128      */
00129     public function __construct() {
00130         if (!$this->isAuthorizedBackendSession()) {
00131             throw new t3lib_error_Exception(
00132                 'A back-end form protection may only be instantiated if there' .
00133                 ' is an active back-end session.',
00134                 1285067843
00135             );
00136         }
00137         $this->backendUser = $GLOBALS['BE_USER'];
00138         parent::__construct();
00139     }
00140 
00141     /**
00142      * Overrule the method in the absract class, because we can drop the
00143      * whole locking procedure, which is done in persistTokens, if we
00144      * simply want to delete all tokens.
00145      *
00146      * @see t3lib/formprotection/t3lib_formprotection_Abstract::clean()
00147      */
00148     public function clean() {
00149         $this->tokens = array();
00150         $this->backendUser->setAndSaveSessionData('formTokens', $this->tokens);
00151         $this->resetPersistingRequiredStatus();
00152     }
00153 
00154     /**
00155      * Override the abstract class to be able to strip out
00156      * the token id from the POST variable.
00157      *
00158      * @see t3lib/formprotection/t3lib_formprotection_Abstract::validateToken()
00159      */
00160     public function validateToken(
00161         $token, $formName, $action = '', $formInstanceName = ''
00162     ) {
00163         list($tokenId, $_) = explode('-', (string)$token);
00164 
00165         return parent::validateToken($tokenId, $formName, $action, $formInstanceName);
00166     }
00167 
00168     /**
00169      * Creates or displayes an error message telling the user that the submitted
00170      * form token is invalid.
00171      *
00172      * @return void
00173      */
00174     protected function createValidationErrorMessage() {
00175         $message = t3lib_div::makeInstance(
00176             't3lib_FlashMessage',
00177             $GLOBALS['LANG']->sL(
00178                 'LLL:EXT:lang/locallang_core.xml:error.formProtection.tokenInvalid'
00179             ),
00180             '',
00181             t3lib_FlashMessage::ERROR,
00182             TRUE
00183         );
00184         t3lib_FlashMessageQueue::addMessage($message);
00185     }
00186 
00187     /**
00188      * Retrieves all saved tokens.
00189      *
00190      * @return array<array>
00191      *       the saved tokens as, will be empty if no tokens have been saved
00192      */
00193     protected function retrieveTokens() {
00194         $tokens = $this->backendUser->getSessionData('formTokens');
00195         if (!is_array($tokens)) {
00196             $tokens = array();
00197         }
00198 
00199         return $tokens;
00200     }
00201 
00202     /**
00203      * It might be that two (or more) scripts are executed at the same time,
00204      * which would lead to a race condition, where both (all) scripts retrieve
00205      * the same tokens from the session, so the script that is executed
00206      * last will overwrite the tokens generated in the first scripts.
00207      * So before writing all tokens back to the session we need to get the
00208      * current tokens from the session again.
00209      *
00210      */
00211     protected function updateTokens() {
00212         $this->backendUser->user = $this->backendUser->fetchUserSession(TRUE);
00213         $tokens = $this->retrieveTokens();
00214         $this->tokens = array_merge($tokens, $this->addedTokens);
00215         foreach ($this->droppedTokenIds as $tokenId) {
00216             unset($this->tokens[$tokenId]);
00217         }
00218     }
00219 
00220     /**
00221      * Saves the tokens so that they can be used by a later incarnation of this
00222      * class.
00223      *
00224      * @return void
00225      */
00226     public function persistTokens() {
00227         if ($this->isPersistingRequired()) {
00228             $lockObject = $this->acquireLock();
00229 
00230             $this->updateTokens();
00231             $this->backendUser->setAndSaveSessionData('formTokens', $this->tokens);
00232             $this->resetPersistingRequiredStatus();
00233 
00234             $this->releaseLock($lockObject);
00235         }
00236     }
00237 
00238     /**
00239      * Tries to acquire a lock to not allow a race condition.
00240      *
00241      * @return t3lib_lock|FALSE The lock object or FALSE
00242      */
00243     protected function acquireLock() {
00244         $identifier = 'persistTokens' . $this->backendUser->id;
00245         try {
00246             /** @var t3lib_lock $lockObject */
00247             $lockObject = t3lib_div::makeInstance('t3lib_lock', $identifier, 'simple');
00248             $lockObject->setEnableLogging(FALSE);
00249             $success = $lockObject->acquire();
00250         } catch (Exception $e) {
00251             t3lib_div::sysLog('Locking: Failed to acquire lock: '.$e->getMessage(), 't3lib_formprotection_BackendFormProtection', t3lib_div::SYSLOG_SEVERITY_ERROR);
00252             $success = FALSE;   // If locking fails, return with false and continue without locking
00253         }
00254 
00255         return $success ? $lockObject : FALSE;
00256     }
00257 
00258     /**
00259      * Releases the lock if it was acquired before.
00260      *
00261      * @return boolean
00262      */
00263     protected function releaseLock(&$lockObject) {
00264         $success = FALSE;
00265             // If lock object is set and was acquired, release it:
00266         if (is_object($lockObject) && $lockObject instanceof t3lib_lock && $lockObject->getLockStatus()) {
00267             $success = $lockObject->release();
00268             $lockObject = NULL;
00269         }
00270 
00271         return $success;
00272     }
00273 
00274     /**
00275      * Checks if a user is logged in and the session is active.
00276      *
00277      * @return boolean
00278      */
00279     protected function isAuthorizedBackendSession() {
00280         return (isset($GLOBALS['BE_USER']) && $GLOBALS['BE_USER'] instanceof t3lib_beUserAuth && isset($GLOBALS['BE_USER']->user['uid']));
00281     }
00282 }
00283 
00284 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/formprotection/class.t3lib_formprotection_backendformprotection.php'])) {
00285     include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/formprotection/class.t3lib_formprotection_backendformprotection.php']);
00286 }
00287 ?>