Follow-up to #17289: resolved endless loop with login/logout
[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 (!isset($GLOBALS['BE_USER'])) {
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 * Creates or displayes an error message telling the user that the submitted
143 * form token is invalid.
144 *
145 * @return void
146 */
147 protected function createValidationErrorMessage() {
148 $message = t3lib_div::makeInstance(
149 't3lib_FlashMessage',
150 $GLOBALS['LANG']->sL(
151 'LLL:EXT:lang/locallang_core.xml:error.formProtection.tokenInvalid'
152 ),
153 '',
154 t3lib_FlashMessage::ERROR,
155 TRUE
156 );
157 t3lib_FlashMessageQueue::addMessage($message);
158 }
159
160 /**
161 * Retrieves all saved tokens.
162 *
163 * @return array<array>
164 * the saved tokens as, will be empty if no tokens have been saved
165 */
166 protected function retrieveTokens() {
167 $tokens = $this->backendUser->getSessionData('formTokens');
168 if (!is_array($tokens)) {
169 $tokens = array();
170 }
171
172 return $tokens;
173 }
174
175 /**
176 * It might be that two (or more) scripts are executed at the same time,
177 * which would lead to a race condition, where both (all) scripts retrieve
178 * the same tokens from the session, so the script that is executed
179 * last will overwrite the tokens generated in the first scripts.
180 * So before writing all tokens back to the session we need to get the
181 * current tokens from the session again.
182 *
183 */
184 protected function updateTokens() {
185 if ($this->backendUser->user) {
186 $this->backendUser->user = $this->backendUser->fetchUserSession(TRUE);
187 $tokens = $this->retrieveTokens();
188 $this->tokens = array_merge($this->tokens, $tokens);
189 }
190 }
191
192 /**
193 * Saves the tokens so that they can be used by a later incarnation of this
194 * class.
195 *
196 * @return void
197 */
198 public function persistTokens() {
199 $lockObject = $this->acquireLock();
200
201 $this->updateTokens();
202 $this->backendUser->setAndSaveSessionData('formTokens', $this->tokens);
203
204 $this->releaseLock($lockObject);
205 }
206
207 /**
208 * Tries to acquire a lock to not allow a race condition.
209 *
210 * @return t3lib_lock|FALSE The lock object or FALSE
211 */
212 protected function acquireLock() {
213 $identifier = 'persistTokens' . $this->backendUser->id;
214 try {
215 $lockObject = t3lib_div::makeInstance('t3lib_lock', $identifier, 'simple');
216 $lockObject->setEnableLogging(FALSE);
217 $success = $lockObject->acquire();
218 } catch (Exception $e) {
219 t3lib_div::sysLog('Locking: Failed to acquire lock: '.$e->getMessage(), 't3lib_formprotection_BackendFormProtection', t3lib_div::SYSLOG_SEVERITY_ERROR);
220 $success = FALSE; // If locking fails, return with false and continue without locking
221 }
222
223 return $success ? $lockObject : FALSE;
224 }
225
226 /**
227 * Releases the lock if it was acquired before.
228 *
229 * @return boolean
230 */
231 protected function releaseLock(&$lockObject) {
232 $success = FALSE;
233 // If lock object is set and was acquired, release it:
234 if (is_object($lockObject) && $lockObject instanceof t3lib_lock && $lockObject->getLockStatus()) {
235 $success = $lockObject->release();
236 $lockObject = NULL;
237 }
238
239 return $success;
240 }
241 }
242
243 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/formprotection/class.t3lib_formprotection_backendformprotection.php'])) {
244 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/formprotection/class.t3lib_formprotection_backendformprotection.php']);
245 }
246 ?>