[FEATURE] Make saltedpasswords conversion task options configurable
[Packages/TYPO3.CMS.git] / typo3 / sysext / saltedpasswords / classes / tasks / class.tx_saltedpasswords_tasks_bulkupdate.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2010-2011 Christian Kuhn <lolli@schwarzbu.ch>
6 * Marcus Krause <marcus#exp2010@t3sec.info>
7 *
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26
27 /**
28 * Update plaintext and hashed passwords of existing users to salted passwords.
29 *
30 * @author Christian Kuhn <lolli@schwarzbu.ch>
31 * @author Marcus Krause <marcus#exp2010@t3sec.info>
32 * @package TYPO3
33 * @subpackage saltedpasswords
34 */
35 class tx_saltedpasswords_Tasks_BulkUpdate extends tx_scheduler_Task {
36 /**
37 * @var boolean Whether or not the task is allowed to deactivate itself after processing all existing user records.
38 */
39 protected $canDeactivateSelf = TRUE;
40
41 /**
42 * Converting a password to a salted hash takes some milliseconds (~100ms on an entry system in 2010).
43 * If all users are updated in one run, the task might run a long time if a lot of users must be handled.
44 * Therefore only a small number of frontend and backend users are processed.
45 * If saltedpasswords is enabled for both frontend and backend 2 * numberOfRecords will be handled.
46 *
47 * @var integer Number of records
48 */
49 protected $numberOfRecords = 250;
50
51 /**
52 * @var integer Pointer to last handled frontend and backend user row
53 */
54 protected $userRecordPointer = array();
55
56 /**
57 * Constructor initializes user record pointer
58 */
59 public function __construct() {
60 parent::__construct();
61
62 $this->userRecordPointer = array(
63 'FE' => 0,
64 'BE' => 0,
65 );
66 }
67
68 /**
69 * Execute task
70 *
71 * @return boolean
72 */
73 public function execute() {
74 $processedAllRecords = TRUE;
75
76 // For frontend and backend
77 foreach ($this->userRecordPointer as $mode => $pointer) {
78 // If saltedpasswords is active for frontend / backend
79 if (tx_saltedpasswords_div::isUsageEnabled($mode)) {
80 $usersToUpdate = $this->findUsersToUpdate($mode);
81 $numberOfRows = count($usersToUpdate);
82 if ($numberOfRows > 0) {
83 $processedAllRecords = FALSE;
84 $this->incrementUserRecordPointer($mode, $numberOfRows);
85 $this->convertPasswords($mode, $usersToUpdate);
86 }
87 }
88 }
89
90 if ($processedAllRecords) {
91 // Reset the user record pointer
92 $this->userRecordPointer = array(
93 'FE' => 0,
94 'BE' => 0,
95 );
96 // Determine if task should disable itself
97 if ($this->canDeactivateSelf) {
98 $this->deactivateSelf();
99 }
100 }
101
102 // Use save() of parent class tx_scheduler_Task to persist changed task variables
103 $this->save();
104
105 return TRUE;
106 }
107
108 /**
109 * Get additional information
110 *
111 * @return string Additional information
112 */
113 public function getAdditionalInformation() {
114 $information =
115 $GLOBALS['LANG']->sL(
116 'LLL:EXT:saltedpasswords/locallang.xml:ext.saltedpasswords.tasks.bulkupdate.label.additionalinformation.deactivateself'
117 ) .
118 $this->getCanDeactivateSelf() . '; ' .
119 $GLOBALS['LANG']->sL(
120 'LLL:EXT:saltedpasswords/locallang.xml:ext.saltedpasswords.tasks.bulkupdate.label.additionalinformation.numberofrecords'
121 ) .
122 $this->getNumberOfRecords();
123
124 return $information;
125 }
126
127 /**
128 * Finds next set of frontend or backend users to update.
129 *
130 * @param string $mode 'FE' for frontend, 'BE' for backend user records
131 * @return array Rows with uid and password
132 */
133 protected function findUsersToUpdate($mode) {
134 $usersToUpdate = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
135 'uid, password',
136 strtolower($mode) . '_users',
137 // Retrieve and update all records (also disabled/deleted) for security reasons
138 '1 = 1',
139 '',
140 'uid ASC',
141 $this->userRecordPointer[$mode] . ', ' . $this->numberOfRecords
142 );
143
144 return $usersToUpdate;
145 }
146
147 /**
148 * Iterates over given user records and update password if needed.
149 *
150 * @param string $mode 'FE' for frontend, 'BE' for backend user records
151 * @param array $users With user uids and passwords
152 * @return void
153 */
154 protected function convertPasswords($mode, array $users) {
155 $updateUsers = array();
156 foreach ($users as $user) {
157 // If a password is already a salted hash it must not be updated
158 if ($this->isSaltedHash($user['password'])) {
159 continue;
160 }
161
162 $updateUsers[] = $user;
163 }
164
165 if (count($updateUsers) > 0) {
166 $this->updatePasswords($mode, $updateUsers);
167 }
168 }
169
170 /**
171 * Updates password and persist salted hash.
172 *
173 * @param string $mode 'FE' for frontend, 'BE' for backend user records
174 * @param array $users With user uids and passwords
175 * @return void
176 */
177 protected function updatePasswords($mode, array $users) {
178 /** @var $saltedpasswordsInstance tx_saltedpasswords_salts */
179 $saltedpasswordsInstance = tx_saltedpasswords_salts_factory::getSaltingInstance(NULL, $mode);
180
181 foreach ($users as $user) {
182 $newPassword = $saltedpasswordsInstance->getHashedPassword($user['password']);
183
184 // If a given password is a md5 hash (usually default be_users without saltedpasswords activated),
185 // result of getHashedPassword() is a salted hashed md5 hash.
186 // We prefix those with 'M', saltedpasswords will then update this password
187 // to a usual salted hash upon first login of the user.
188 if ($this->isMd5Password($user['password'])) {
189 $newPassword = 'M' . $newPassword;
190 }
191
192 // Persist updated password
193 $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
194 strtolower($mode) . '_users',
195 'uid = ' . $user['uid'],
196 array(
197 'password' => $newPassword
198 )
199 );
200 }
201 }
202
203 /**
204 * Passwords prefixed with M or C might be salted passwords:
205 * M means: originally a md5 hash before it was salted (eg. default be_users).
206 * C means: originally a cleartext password with lower hash looping count generated by t3sec_saltedpw.
207 * Both M and C will be updated to usual salted hashes on first login of user.
208 *
209 * If a password does not start with M or C determine if a password is already a usual salted hash.
210 *
211 * @param string $password Password
212 * @return boolean TRUE if password is a salted hash
213 */
214 protected function isSaltedHash($password) {
215 $isSaltedHash = FALSE;
216 if (strlen($password) > 2 && (t3lib_div::isFirstPartOfStr($password, 'C$') || t3lib_div::isFirstPartOfStr($password, 'M$'))) {
217 // Cut off M or C and test if we have a salted hash
218 $isSaltedHash = tx_saltedpasswords_salts_factory::determineSaltingHashingMethod(substr($password, 1));
219 }
220
221 // Test if given password is a already a usual salted hash
222 if (!$isSaltedHash) {
223 $isSaltedHash = tx_saltedpasswords_salts_factory::determineSaltingHashingMethod($password);
224 }
225
226 return $isSaltedHash;
227 }
228
229 /**
230 * Checks if a given password is a md5 hash, the default for be_user records before saltedpasswords.
231 *
232 * @param string $password The password to test
233 * @return boolean TRUE if password is md5
234 */
235 protected function isMd5Password($password) {
236 return (bool) preg_match('/[0-9abcdef]{32,32}/i', $password);
237 }
238
239 /**
240 * Increments current user record counter by number of handled rows.
241 *
242 * @param string $mode 'FE' for frontend, 'BE' for backend user records
243 * @param integer $number Number of handled rows
244 * @return void
245 */
246 protected function incrementUserRecordPointer($mode, $number) {
247 $this->userRecordPointer[$mode] += $number;
248 }
249
250 /**
251 * Deactivates this task instance.
252 * Uses setDisabled() method of parent class tx_scheduler_Task.
253 *
254 * @return void
255 */
256 protected function deactivateSelf() {
257 $this->setDisabled(TRUE);
258 }
259
260 /**
261 * Set if it can deactivate self
262 *
263 * @param boolean $canDeactivateSelf
264 * @return void
265 */
266 public function setCanDeactivateSelf($canDeactivateSelf) {
267 $this->canDeactivateSelf = $canDeactivateSelf;
268 }
269
270 /**
271 * Get if it can deactivate self
272 *
273 * @return boolean TRUE if task shall deactivate itself, FALSE otherwise
274 */
275 public function getCanDeactivateSelf() {
276 return $this->canDeactivateSelf;
277 }
278
279 /**
280 * Set number of records
281 *
282 * @param integer $numberOfRecords
283 * @return void
284 */
285 public function setNumberOfRecords($numberOfRecords) {
286 $this->numberOfRecords = $numberOfRecords;
287 }
288
289 /**
290 * Get number of records
291 *
292 * @return integer The number of records
293 */
294 public function getNumberOfRecords() {
295 return $this->numberOfRecords;
296 }
297 }
298
299 ?>