Fixed bug #17383: Open forms cannot be saved after "Relogin" (Security Token errors...
[Packages/TYPO3.CMS.git] / t3lib / formprotection / class.t3lib_formprotection_backendformprotection.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2010-2011 Oliver Klee <typo3-coding@oliverklee.de>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25 /**
26 * Class t3lib_formprotection_BackendFormProtection.
27 *
28 * This class provides protection against cross-site request forgery (XSRF/CSRF)
29 * for forms in the BE.
30 *
31 * How to use:
32 *
33 * For each form in the BE (or link that changes some data), create a token and
34 * insert is as a hidden form element. The name of the form element does not
35 * matter; you only need it to get the form token for verifying it.
36 *
37 * <pre>
38 * $formToken = t3lib_formprotection_Factory::get(
39 * t3lib_formprotection_Factory::TYPE_BACK_END
40 * )->generateToken(
41 * 'BE user setup', 'edit'
42 * );
43 * $this->content .= '<input type="hidden" name="formToken" value="' .
44 * $formToken . '" />';
45 * </pre>
46 *
47 * The three parameters $formName, $action and $formInstanceName can be
48 * arbitrary strings, but they should make the form token as specific as
49 * possible. For different forms (e.g. BE user setup and editing a tt_content
50 * record) or different records (with different UIDs) from the same table,
51 * those values should be different.
52 *
53 * For editing a tt_content record, the call could look like this:
54 *
55 * <pre>
56 * $formToken = t3lib_formprotection_Factory::get(
57 * t3lib_formprotection_Factory::TYPE_BACK_END
58 * )->getFormProtection()->generateToken(
59 * 'tt_content', 'edit', $uid
60 * );
61 * </pre>
62 *
63 * At the end of the form, you need to persist the tokens. This makes sure that
64 * generated tokens get saved, and also that removed tokens stay removed:
65 *
66 * <pre>
67 * t3lib_formprotection_Factory::get(
68 * t3lib_formprotection_Factory::TYPE_BACK_END
69 * )->persistTokens();
70 * </pre>
71 *
72 * In BE lists, it might be necessary to generate hundreds of tokens. So the
73 * tokens do not get automatically persisted after creation for performance
74 * reasons.
75 *
76 *
77 * When processing the data that has been submitted by the form, you can check
78 * that the form token is valid like this:
79 *
80 * <pre>
81 * if ($dataHasBeenSubmitted && t3lib_formprotection_Factory::get(
82 * t3lib_formprotection_Factory::TYPE_BACK_END
83 * )->validateToken(
84 * (string) t3lib_div::_POST('formToken'),
85 * 'BE user setup', 'edit
86 * )
87 * ) {
88 * // processes the data
89 * } else {
90 * // no need to do anything here as the BE form protection will create a
91 * // flash message for an invalid token
92 * }
93 * </pre>
94 *
95 * Note that validateToken invalidates the token with the token ID. So calling
96 * validate with the same parameters two times in a row will always return FALSE
97 * for the second call.
98 *
99 * It is important that the tokens get validated <em>before</em> the tokens are
100 * persisted. This makes sure that the tokens that get invalidated by
101 * validateToken cannot be used again.
102 *
103 * $Id$
104 *
105 * @package TYPO3
106 * @subpackage t3lib
107 *
108 * @author Oliver Klee <typo3-coding@oliverklee.de>
109 */
110 class t3lib_formprotection_BackendFormProtection extends t3lib_formprotection_Abstract {
111 /**
112 * the maximum number of tokens that can exist at the same time
113 *
114 * @var integer
115 */
116 protected $maximumNumberOfTokens = 20000;
117
118 /**
119 * Keeps the instance of the user which existed during creation
120 * of the object.
121 *
122 * @var t3lib_beUserAuth
123 */
124 protected $backendUser;
125
126 /**
127 * Only allow construction if we have a backend session
128 */
129 public function __construct() {
130 if (!$this->isAuthorizedBackendSession()) {
131 throw new t3lib_error_Exception(
132 'A back-end form protection may only be instantiated if there' .
133 ' is an active back-end session.',
134 1285067843
135 );
136 }
137 $this->backendUser = $GLOBALS['BE_USER'];
138 parent::__construct();
139 }
140
141 /**
142 * Overrule the method in the absract class, because we can drop the
143 * whole locking procedure, which is done in persistTokens, if we
144 * simply want to delete all tokens.
145 *
146 * @see t3lib/formprotection/t3lib_formprotection_Abstract::clean()
147 */
148 public function clean() {
149 $this->tokens = array();
150 $this->backendUser->setAndSaveSessionData('formTokens', $this->tokens);
151 $this->resetPersistingRequiredStatus();
152 }
153
154 /**
155 * Override the abstract class to be able to strip out
156 * the token id from the POST variable.
157 *
158 * @see t3lib/formprotection/t3lib_formprotection_Abstract::validateToken()
159 */
160 public function validateToken(
161 $token, $formName, $action = '', $formInstanceName = ''
162 ) {
163 list($tokenId, $_) = explode('-', (string)$token);
164
165 return parent::validateToken($tokenId, $formName, $action, $formInstanceName);
166 }
167
168 /**
169 * Creates or displayes an error message telling the user that the submitted
170 * form token is invalid.
171 *
172 * @return void
173 */
174 protected function createValidationErrorMessage() {
175 $message = t3lib_div::makeInstance(
176 't3lib_FlashMessage',
177 $GLOBALS['LANG']->sL(
178 'LLL:EXT:lang/locallang_core.xml:error.formProtection.tokenInvalid'
179 ),
180 '',
181 t3lib_FlashMessage::ERROR,
182 TRUE
183 );
184 t3lib_FlashMessageQueue::addMessage($message);
185 }
186
187 /**
188 * Retrieves all saved tokens.
189 *
190 * @return array<array>
191 * the saved tokens as, will be empty if no tokens have been saved
192 */
193 protected function retrieveTokens() {
194 $tokens = $this->backendUser->getSessionData('formTokens');
195 if (!is_array($tokens)) {
196 $tokens = array();
197 }
198
199 return $tokens;
200 }
201
202 /**
203 * It might be that two (or more) scripts are executed at the same time,
204 * which would lead to a race condition, where both (all) scripts retrieve
205 * the same tokens from the session, so the script that is executed
206 * last will overwrite the tokens generated in the first scripts.
207 * So before writing all tokens back to the session we need to get the
208 * current tokens from the session again.
209 *
210 */
211 protected function updateTokens() {
212 $this->backendUser->user = $this->backendUser->fetchUserSession(TRUE);
213 $tokens = $this->retrieveTokens();
214 $this->tokens = array_merge($tokens, $this->addedTokens);
215 foreach ($this->droppedTokenIds as $tokenId) {
216 unset($this->tokens[$tokenId]);
217 }
218 }
219
220 /**
221 * Saves the tokens so that they can be used by a later incarnation of this
222 * class.
223 *
224 * @return void
225 */
226 public function persistTokens() {
227 if ($this->isPersistingRequired()) {
228 $lockObject = $this->acquireLock();
229
230 $this->updateTokens();
231 $this->backendUser->setAndSaveSessionData('formTokens', $this->tokens);
232 $this->resetPersistingRequiredStatus();
233
234 $this->releaseLock($lockObject);
235 }
236 }
237
238 /**
239 * Tries to acquire a lock to not allow a race condition.
240 *
241 * @return t3lib_lock|FALSE The lock object or FALSE
242 */
243 protected function acquireLock() {
244 $identifier = 'persistTokens' . $this->backendUser->id;
245 try {
246 /** @var t3lib_lock $lockObject */
247 $lockObject = t3lib_div::makeInstance('t3lib_lock', $identifier, 'simple');
248 $lockObject->setEnableLogging(FALSE);
249 $success = $lockObject->acquire();
250 } catch (Exception $e) {
251 t3lib_div::sysLog('Locking: Failed to acquire lock: '.$e->getMessage(), 't3lib_formprotection_BackendFormProtection', t3lib_div::SYSLOG_SEVERITY_ERROR);
252 $success = FALSE; // If locking fails, return with false and continue without locking
253 }
254
255 return $success ? $lockObject : FALSE;
256 }
257
258 /**
259 * Releases the lock if it was acquired before.
260 *
261 * @return boolean
262 */
263 protected function releaseLock(&$lockObject) {
264 $success = FALSE;
265 // If lock object is set and was acquired, release it:
266 if (is_object($lockObject) && $lockObject instanceof t3lib_lock && $lockObject->getLockStatus()) {
267 $success = $lockObject->release();
268 $lockObject = NULL;
269 }
270
271 return $success;
272 }
273
274 /**
275 * Checks if a user is logged in and the session is active.
276 *
277 * @return boolean
278 */
279 protected function isAuthorizedBackendSession() {
280 return (isset($GLOBALS['BE_USER']) && $GLOBALS['BE_USER'] instanceof t3lib_beUserAuth && isset($GLOBALS['BE_USER']->user['uid']));
281 }
282 }
283
284 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/formprotection/class.t3lib_formprotection_backendformprotection.php'])) {
285 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/formprotection/class.t3lib_formprotection_backendformprotection.php']);
286 }
287 ?>