123838e4f51a4efc2c830a9e5a6250980e67e506
[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 * @TODO: This could be set with an additional field later on.
39 * The idea is to not disable the task after all initial users where handled.
40 * This could be handy for example if new users are imported regularily from some external source.
41 */
42 protected $canDeactivateSelf = TRUE;
43
44 /**
45 * Converting a password to a salted hash takes some milliseconds (~100ms on an entry system in 2010).
46 * If all users are updated in one run, the task might run a long time if a lot of users must be handled.
47 * Therefore only a small number of frontend and backend users are processed.
48 * If saltedpasswords is enabled for both frontend and backend 2 * numberOfRecords will be handled.
49 *
50 * @var integer Number of records
51 * @TODO: This could be set with an additional field later on
52 */
53 protected $numberOfRecords = 250;
54
55 /**
56 * @var integer Pointer to last handled frontend and backend user row
57 */
58 protected $userRecordPointer = array();
59
60 /**
61 * Constructor initializes user record pointer
62 */
63 public function __construct() {
64 parent::__construct();
65
66 $this->userRecordPointer = array(
67 'FE' => 0,
68 'BE' => 0,
69 );
70 }
71
72 /**
73 * Execute task
74 *
75 * @return void
76 */
77 public function execute() {
78 $processedAllRecords = TRUE;
79
80 // For frontend and backend
81 foreach ($this->userRecordPointer as $mode => $pointer) {
82 // If saltedpasswords is active for frontend / backend
83 if (tx_saltedpasswords_div::isUsageEnabled($mode)) {
84 $usersToUpdate = $this->findUsersToUpdate($mode);
85 $numberOfRows = count($usersToUpdate);
86 if ($numberOfRows > 0) {
87 $processedAllRecords = FALSE;
88 $this->incrementUserRecordPointer($mode, $numberOfRows);
89 $this->convertPasswords($mode, $usersToUpdate);
90 }
91 }
92 }
93
94 // Determine if task should disable itself
95 if ($this->canDeactivateSelf && $processedAllRecords) {
96 $this->deactivateSelf();
97 }
98
99 // Use save() of parent class tx_scheduler_Task to persist
100 // changed task variables: $this->userRecordPointer and $this->disabled
101 $this->save();
102
103 return(TRUE);
104 }
105
106 /**
107 * Find next set of frontend or backend users to update.
108 *
109 * @param string $mode 'FE' for frontend, 'BE' for backend user records
110 * @return array Rows with uid and password
111 */
112 protected function findUsersToUpdate($mode) {
113 $usersToUpdate = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
114 'uid, password',
115 strtolower($mode) . '_users',
116 // retrieve and update all records (also disabled/deleted) for security reasons
117 '1 = 1',
118 '',
119 'uid ASC',
120 $this->userRecordPointer[$mode] . ', ' . $this->numberOfRecords
121 );
122
123 return $usersToUpdate;
124 }
125
126 /**
127 * Iterate over given user records and update password if needed.
128 *
129 * @param string $mode 'FE' for frontend, 'BE' for backend user records
130 * @param array $users With user uids and passwords
131 * @return void
132 */
133 protected function convertPasswords($mode, $users) {
134 $updateUsers = array();
135 foreach ($users as $user) {
136 // If a password is already a salted hash it must not be updated
137 if ($this->isSaltedHash($user['password'])) {
138 continue;
139 }
140
141 $updateUsers[] = $user;
142 }
143
144 if (count($updateUsers) > 0) {
145 $this->updatePasswords($mode, $updateUsers);
146 }
147 }
148
149 /**
150 * Update password and persist salted hash.
151 *
152 * @param string $mode 'FE' for frontend, 'BE' for backend user records
153 * @param array $users With user uids and passwords
154 * @return void
155 */
156 protected function updatePasswords($mode, $users) {
157 // Get a default saltedpasswords instance
158 $saltedpasswordsInstance = tx_saltedpasswords_salts_factory::getSaltingInstance(NULL, $mode);
159
160 foreach ($users as $user) {
161 $newPassword = $saltedpasswordsInstance->getHashedPassword($user['password']);
162
163 // If a given password is a md5 hash (usually default be_users without saltedpasswords activated),
164 // result of getHasedPassword() is a salted hashed md5 hash.
165 // We prefix those with 'M', saltedpasswords will then update this password
166 // to a usual salted hash upon first login of the user.
167 if ($this->isMd5Password($user['password'])) {
168 $newPassword = 'M' . $newPassword;
169 }
170
171 // Persist updated password
172 $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
173 strtolower($mode) . '_users',
174 'uid = ' . $user['uid'],
175 array(
176 'password' => $newPassword
177 )
178 );
179 }
180 }
181
182 /**
183 * Passwords prefixed with M or C might be salted passwords:
184 * M means: originally a md5 hash before it was salted (eg. default be_users).
185 * C means: originally a cleartext password with lower hash looping count generated by t3sec_saltedpw.
186 * Both M and C will be updated to usual salted hashes on first login of user.
187 *
188 * If a password does not start with M or C determine if a password is already a usual salted hash.
189 *
190 * @param string $password Password
191 * @return boolean TRUE if password is a salted hash
192 */
193 protected function isSaltedHash($password) {
194 $isSaltedHash = FALSE;
195 if (strlen($password) > 2 && (t3lib_div::isFirstPartOfStr($password, 'C$') || t3lib_div::isFirstPartOfStr($password, 'M$'))) {
196 // Cut off M or C and test if we have a salted hash
197 $isSaltedHash = tx_saltedpasswords_salts_factory::determineSaltingHashingMethod(substr($password, 1));
198 }
199
200 // Test if given password is a already a usual salted hash
201 if (!$isSaltedHash) {
202 $isSaltedHash = tx_saltedpasswords_salts_factory::determineSaltingHashingMethod($password);
203 }
204
205 return $isSaltedHash;
206 }
207
208 /**
209 * Check if a given password is a md5 hash, the default for be_user records before saltedpasswords.
210 *
211 * @param string $password
212 * @return boolean TRUE if password is md5
213 */
214 protected function isMd5Password($password) {
215 return (bool) preg_match('/[0-9abcdef]{32,32}/i', $password);
216 }
217
218 /**
219 * Increment current user record counter by number of handled rows.
220 *
221 * @param string $mode 'FE' for frontend, 'BE' for backend user records
222 * @param integer $number Number of handled rows
223 * @return void
224 */
225 protected function incrementUserRecordPointer($mode, $number) {
226 $this->userRecordPointer[$mode] += $number;
227 }
228
229 /**
230 * Deactivate this task instance.
231 * Uses setDisabled() method of parent class tx_scheduler_Task.
232 *
233 * @return void
234 */
235 protected function deactivateSelf() {
236 $this->setDisabled(TRUE);
237 }
238 }
239 ?>