PasswordResetModel.php (12947B)
1 <?php 2 3 /** 4 * Class PasswordResetModel 5 * 6 * Handles all the stuff that is related to the password-reset process 7 */ 8 class PasswordResetModel 9 { 10 /** 11 * Perform the necessary actions to send a password reset mail 12 * 13 * @param $user_name_or_email string Username or user's email 14 * 15 * @return bool success status 16 */ 17 public static function requestPasswordReset($user_name_or_email) 18 { 19 if (empty($user_name_or_email)) { 20 Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_EMAIL_FIELD_EMPTY')); 21 return false; 22 } 23 24 // check if that username exists 25 $result = UserModel::getUserDataByUserNameOrEmail($user_name_or_email); 26 if (!$result) { 27 Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST')); 28 return false; 29 } 30 31 // generate integer-timestamp (to see when exactly the user (or an attacker) requested the password reset mail) 32 // generate random hash for email password reset verification (40 char string) 33 $temporary_timestamp = time(); 34 $user_password_reset_hash = sha1(uniqid(mt_rand(), true)); 35 36 // set token (= a random hash string and a timestamp) into database ... 37 $token_set = self::setPasswordResetDatabaseToken($result->user_name, $user_password_reset_hash, $temporary_timestamp); 38 if (!$token_set) { 39 return false; 40 } 41 42 // ... and send a mail to the user, containing a link with username and token hash string 43 $mail_sent = self::sendPasswordResetMail($result->user_name, $user_password_reset_hash, $result->user_email); 44 if ($mail_sent) { 45 return true; 46 } 47 48 // default return 49 return false; 50 } 51 52 /** 53 * Set password reset token in database (for DEFAULT user accounts) 54 * 55 * @param string $user_name username 56 * @param string $user_password_reset_hash password reset hash 57 * @param int $temporary_timestamp timestamp 58 * 59 * @return bool success status 60 */ 61 public static function setPasswordResetDatabaseToken($user_name, $user_password_reset_hash, $temporary_timestamp) 62 { 63 $database = DatabaseFactory::getFactory()->getConnection(); 64 65 $sql = "UPDATE users 66 SET user_password_reset_hash = :user_password_reset_hash, user_password_reset_timestamp = :user_password_reset_timestamp 67 WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1"; 68 $query = $database->prepare($sql); 69 $query->execute(array( 70 ':user_password_reset_hash' => $user_password_reset_hash, ':user_name' => $user_name, 71 ':user_password_reset_timestamp' => $temporary_timestamp, ':provider_type' => 'DEFAULT' 72 )); 73 74 // check if exactly one row was successfully changed 75 if ($query->rowCount() == 1) { 76 return true; 77 } 78 79 // fallback 80 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_FAIL')); 81 return false; 82 } 83 84 /** 85 * Send the password reset mail 86 * 87 * @param string $user_name username 88 * @param string $user_password_reset_hash password reset hash 89 * @param string $user_email user email 90 * 91 * @return bool success status 92 */ 93 public static function sendPasswordResetMail($user_name, $user_password_reset_hash, $user_email) 94 { 95 // create email body 96 $body = Config::get('EMAIL_PASSWORD_RESET_CONTENT') . ' ' . Config::get('URL') . 97 Config::get('EMAIL_PASSWORD_RESET_URL') . '/' . urlencode($user_name) . '/' . urlencode($user_password_reset_hash); 98 99 // create instance of Mail class, try sending and check 100 $mail = new Mail; 101 $mail_sent = $mail->sendMail($user_email, Config::get('EMAIL_PASSWORD_RESET_FROM_EMAIL'), 102 Config::get('EMAIL_PASSWORD_RESET_FROM_NAME'), Config::get('EMAIL_PASSWORD_RESET_SUBJECT'), $body 103 ); 104 105 if ($mail_sent) { 106 Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL')); 107 return true; 108 } 109 110 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR') . $mail->getError() ); 111 return false; 112 } 113 114 /** 115 * Verifies the password reset request via the verification hash token (that's only valid for one hour) 116 * @param string $user_name Username 117 * @param string $verification_code Hash token 118 * @return bool Success status 119 */ 120 public static function verifyPasswordReset($user_name, $verification_code) 121 { 122 $database = DatabaseFactory::getFactory()->getConnection(); 123 124 // check if user-provided username + verification code combination exists 125 $sql = "SELECT user_id, user_password_reset_timestamp 126 FROM users 127 WHERE user_name = :user_name 128 AND user_password_reset_hash = :user_password_reset_hash 129 AND user_provider_type = :user_provider_type 130 LIMIT 1"; 131 $query = $database->prepare($sql); 132 $query->execute(array( 133 ':user_password_reset_hash' => $verification_code, ':user_name' => $user_name, 134 ':user_provider_type' => 'DEFAULT' 135 )); 136 137 // if this user with exactly this verification hash code does NOT exist 138 if ($query->rowCount() != 1) { 139 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST')); 140 return false; 141 } 142 143 // get result row (as an object) 144 $result_user_row = $query->fetch(); 145 146 // 3600 seconds are 1 hour 147 $timestamp_one_hour_ago = time() - 3600; 148 149 // if password reset request was sent within the last hour (this timeout is for security reasons) 150 if ($result_user_row->user_password_reset_timestamp > $timestamp_one_hour_ago) { 151 // verification was successful 152 Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_LINK_VALID')); 153 return true; 154 } else { 155 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_LINK_EXPIRED')); 156 return false; 157 } 158 } 159 160 /** 161 * Writes the new password to the database 162 * 163 * @param string $user_name username 164 * @param string $user_password_hash 165 * @param string $user_password_reset_hash 166 * 167 * @return bool 168 */ 169 public static function saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash) 170 { 171 $database = DatabaseFactory::getFactory()->getConnection(); 172 173 $sql = "UPDATE users SET user_password_hash = :user_password_hash, user_password_reset_hash = NULL, 174 user_password_reset_timestamp = NULL 175 WHERE user_name = :user_name AND user_password_reset_hash = :user_password_reset_hash 176 AND user_provider_type = :user_provider_type LIMIT 1"; 177 $query = $database->prepare($sql); 178 $query->execute(array( 179 ':user_password_hash' => $user_password_hash, ':user_name' => $user_name, 180 ':user_password_reset_hash' => $user_password_reset_hash, ':user_provider_type' => 'DEFAULT' 181 )); 182 183 // if one result exists, return true, else false. Could be written even shorter btw. 184 return ($query->rowCount() == 1 ? true : false); 185 } 186 187 /** 188 * Set the new password (for DEFAULT user, FACEBOOK-users don't have a password) 189 * Please note: At this point the user has already pre-verified via verifyPasswordReset() (within one hour), 190 * so we don't need to check again for the 60min-limit here. In this method we authenticate 191 * via username & password-reset-hash from (hidden) form fields. 192 * 193 * @param string $user_name 194 * @param string $user_password_reset_hash 195 * @param string $user_password_new 196 * @param string $user_password_repeat 197 * 198 * @return bool success state of the password reset 199 */ 200 public static function setNewPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat) 201 { 202 // validate the password 203 if (!self::validateResetPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)) { 204 return false; 205 } 206 207 // crypt the password (with the PHP 5.5+'s password_hash() function, result is a 60 character hash string) 208 $user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT); 209 210 // write the password to database (as hashed and salted string), reset user_password_reset_hash 211 if (self::saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash)) { 212 Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL')); 213 return true; 214 } else { 215 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CHANGE_FAILED')); 216 return false; 217 } 218 } 219 220 /** 221 * Validate the password submission 222 * 223 * @param $user_name 224 * @param $user_password_reset_hash 225 * @param $user_password_new 226 * @param $user_password_repeat 227 * 228 * @return bool 229 */ 230 public static function validateResetPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat) 231 { 232 if (empty($user_name)) { 233 Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_FIELD_EMPTY')); 234 return false; 235 } else if (empty($user_password_reset_hash)) { 236 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_MISSING')); 237 return false; 238 } else if (empty($user_password_new) || empty($user_password_repeat)) { 239 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY')); 240 return false; 241 } else if ($user_password_new !== $user_password_repeat) { 242 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG')); 243 return false; 244 } else if (strlen($user_password_new) < 6) { 245 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT')); 246 return false; 247 } 248 249 return true; 250 } 251 252 253 /** 254 * Writes the new password to the database 255 * 256 * @param string $user_name 257 * @param string $user_password_hash 258 * 259 * @return bool 260 */ 261 public static function saveChangedPassword($user_name, $user_password_hash) 262 { 263 $database = DatabaseFactory::getFactory()->getConnection(); 264 265 $sql = "UPDATE users SET user_password_hash = :user_password_hash 266 WHERE user_name = :user_name 267 AND user_provider_type = :user_provider_type LIMIT 1"; 268 $query = $database->prepare($sql); 269 $query->execute(array( 270 ':user_password_hash' => $user_password_hash, ':user_name' => $user_name, 271 ':user_provider_type' => 'DEFAULT' 272 )); 273 274 // if one result exists, return true, else false. Could be written even shorter btw. 275 return ($query->rowCount() == 1 ? true : false); 276 } 277 278 279 /** 280 * Validates fields, hashes new password, saves new password 281 * 282 * @param string $user_name 283 * @param string $user_password_current 284 * @param string $user_password_new 285 * @param string $user_password_repeat 286 * 287 * @return bool 288 */ 289 public static function changePassword($user_name, $user_password_current, $user_password_new, $user_password_repeat) 290 { 291 // validate the passwords 292 if (!self::validatePasswordChange($user_name, $user_password_current, $user_password_new, $user_password_repeat)) { 293 return false; 294 } 295 296 // crypt the password (with the PHP 5.5+'s password_hash() function, result is a 60 character hash string) 297 $user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT); 298 299 // write the password to database (as hashed and salted string) 300 if (self::saveChangedPassword($user_name, $user_password_hash)) { 301 Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL')); 302 return true; 303 } else { 304 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CHANGE_FAILED')); 305 return false; 306 } 307 } 308 309 310 /** 311 * Validates current and new passwords 312 * 313 * @param string $user_name 314 * @param string $user_password_current 315 * @param string $user_password_new 316 * @param string $user_password_repeat 317 * 318 * @return bool 319 */ 320 public static function validatePasswordChange($user_name, $user_password_current, $user_password_new, $user_password_repeat) 321 { 322 $database = DatabaseFactory::getFactory()->getConnection(); 323 324 $sql = "SELECT user_password_hash, user_failed_logins FROM users WHERE user_name = :user_name LIMIT 1;"; 325 $query = $database->prepare($sql); 326 $query->execute(array( 327 ':user_name' => $user_name 328 )); 329 330 $user = $query->fetch(); 331 332 if ($query->rowCount() == 1) { 333 $user_password_hash = $user->user_password_hash; 334 } else { 335 Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST')); 336 return false; 337 } 338 339 if (!password_verify($user_password_current, $user_password_hash)) { 340 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CURRENT_INCORRECT')); 341 return false; 342 } else if (empty($user_password_new) || empty($user_password_repeat)) { 343 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY')); 344 return false; 345 } else if ($user_password_new !== $user_password_repeat) { 346 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG')); 347 return false; 348 } else if (strlen($user_password_new) < 6) { 349 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT')); 350 return false; 351 } else if ($user_password_current == $user_password_new){ 352 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_NEW_SAME_AS_CURRENT')); 353 return false; 354 } 355 356 return true; 357 } 358 }