1<?php
2
3/**
4 * OrangeHRM is a comprehensive Human Resource Management (HRM) System that captures
5 * all the essential functionalities required for any enterprise.
6 * Copyright (C) 2006 OrangeHRM Inc., http://www.orangehrm.com
7 *
8 * OrangeHRM is free software; you can redistribute it and/or modify it under the terms of
9 * the GNU General Public License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * OrangeHRM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14 * See the GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along with this program;
17 * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA  02110-1301, USA
19 */
20class PasswordResetService extends BaseService {
21
22    const RESET_PASSWORD_TOKEN_RANDOM_BYTES_LENGTH = 16;
23
24    private $passwordResetDao = null;
25    private $securityAuthenticationConfigService = null;
26    private $authenticationService = null;
27    private $emailService = null;
28    private $systemUserService = null;
29    private $employeeService = null;
30
31    /**
32     *
33     * @return SecurityAuthenticationConfigService
34     */
35    public function getSecurityAuthenticationConfigService() {
36        if (empty($this->securityAuthenticationConfigService)) {
37            $this->securityAuthenticationConfigService = new SecurityAuthenticationConfigService();
38        }
39        return $this->securityAuthenticationConfigService;
40    }
41
42    /**
43     *
44     * @param SecurityAuthenticationConfigService $securityAuthConfigService
45     */
46    public function setSecurityAuthenticationConfigService($securityAuthConfigService) {
47        $this->securityAuthenticationConfigService = $securityAuthConfigService;
48    }
49
50    /**
51     *
52     * @return AuthenticationService
53     */
54    public function getAuthenticationService() {
55        if (empty($this->authenticationService)) {
56            $this->authenticationService = new AuthenticationService();
57        }
58        return $this->authenticationService;
59    }
60
61    /**
62     *
63     * @param AuthenticationService $authService
64     */
65    public function setAuthenticationService($authService) {
66        $this->authenticationService = $authService;
67    }
68
69    /**
70     * @param $username
71     * @return array
72     * @throws ServiceException
73     */
74    public function searchForUserRecord($username) {
75        if (empty($username)){
76            throw new ServiceException(__('Could not find a user with given details'));
77        }
78        $userService = $this->getSystemUserService();
79        $users = $userService->searchSystemUsers(array('userName' => $username));
80        if ($users->count() > 0) {
81            $user = $users->get(0);
82            $associatedEmployee = $user->getEmployee();
83            if (!($associatedEmployee instanceof Employee)) {
84                throw new ServiceException(__('User account is not associated with an employee'));
85            } else {
86                $emailConfiguration=new EmailConfigurationService();
87                $companyEmail=$emailConfiguration->getEmailConfiguration();
88                if(!empty($companyEmail['sentAs'])) {
89                    if (empty($associatedEmployee['termination_id'])) {
90                        $workEmail = trim($associatedEmployee->getEmpWorkEmail());
91                        if (!empty($workEmail)) {
92                            $passResetCode=$this->hasPasswordResetRequest($workEmail);
93                            if (!$passResetCode) {
94                                return $user;
95                            } else {
96                                throw new ServiceException(__('There is a password reset request already in the system.'));
97                            }
98
99                        } else {
100                            throw new ServiceException(__('Work email is not set. Please contact HR admin in order to reset the password'));
101                        }
102                    } else {
103                        throw new ServiceException(__('Please contact HR admin in order to reset the password'));
104                    }
105                } else {
106                    throw new ServiceException(__('Password reset email could not be sent'));
107                }
108            }
109
110        } else {
111            throw new ServiceException(__('Please contact HR admin in order to reset the password'));
112        }
113        return null;
114    }
115
116    public function getEmployeeService() {
117        if (is_null($this->employeeService)) {
118            $this->employeeService = new EmployeeService();
119        }
120        return $this->employeeService;
121    }
122
123    /**
124     * @param $employeeService
125     */
126    public function setEmployeeService($employeeService) {
127        $this->employeeService = $employeeService;
128    }
129
130    /**
131     * @param $email
132     * @return bool
133     */
134    public function hasPasswordResetRequest($email) {
135
136        $resetPassword = $this->getResetPasswordLogByEmail($email);
137        if ($resetPassword instanceof ResetPassword) {
138            return $this->hasPasswordResetRequestNotExpired($resetPassword);
139        }
140
141        return false;
142    }
143
144    /**
145     * @param $email
146     * @return mixed
147     * @throws ServiceException
148     */
149    public function getResetPasswordLogByEmail($email) {
150
151        try {
152            return $this->getPasswordResetDao()->getResetPasswordLogByEmail($email);
153        } catch (Exception $e) {
154            throw new ServiceException($e->getMessage());
155        }
156    }
157
158    /**
159     * @return PasswordResetDao
160     */
161    public function getPasswordResetDao() {
162        return $this->passwordResetDao = new PasswordResetDao();
163    }
164
165    /**
166     * @param $passwordResetDao
167     */
168    public function setPasswordResetDao($passwordResetDao) {
169        $this->passwordResetDao = $passwordResetDao;
170    }
171
172    /**
173     * @param ResetPassword $resetPassword
174     * @return bool
175     */
176    public function hasPasswordResetRequestNotExpired(ResetPassword $resetPassword) {
177        $strExpireTime = strtotime("+1 days " . $resetPassword->getResetRequestDate());
178        $strCurrentTime = strtotime(date("Y-m-d H:i:s"));
179        return ($strExpireTime > $strCurrentTime);
180    }
181
182    /**
183     * @param $user
184     * @return bool
185     * @throws ServiceException
186     */
187    public function logPasswordResetRequest($user) {
188        $identifier = $user->getUserName();
189        $resetCode = $this->generatePasswordResetCode($identifier);
190
191        $resetPassword = new ResetPassword();
192        $resetPassword->setResetEmail($user->getEmployee()->getEmpWorkEmail());
193        $resetPassword->setResetRequestDate(date('Y-m-d H:i:s'));
194        $resetPassword->setResetCode($resetCode);
195
196        $emailSent = $this->sendPasswordResetCodeEmail($user->getEmployee(), $resetCode);
197        if (!$emailSent) {
198            throw new ServiceException(__('Password reset email could not be sent.'));
199        }
200        $this->saveResetPasswordLog($resetPassword);
201
202        return true;
203    }
204
205    /**
206     *
207     * @return string
208     */
209    public function generatePasswordResetCode($identfier) {
210        return Base64Url::encode("{$identfier}#SEPARATOR#" .
211            random_bytes(static::RESET_PASSWORD_TOKEN_RANDOM_BYTES_LENGTH));
212    }
213
214    /**
215     * @param Employee $receiver
216     * @param $resetCode
217     * @return bool
218     */
219    public function sendPasswordResetCodeEmail(Employee $receiver, $resetCode) {
220        $this->getEmailService()->setMessageTo(array($receiver->getEmpWorkEmail()));
221        $this->getEmailService()->setMessageFrom(
222            array($this->getEmailService()->getEmailConfig()->getSentAs() => 'OrangeHRM System'));
223        $this->getEmailService()->setMessageSubject('OrangeHRM Password Reset');
224        $this->getEmailService()->setMessageBody($this->generatePasswordResetEmailBody($receiver, $resetCode));
225        return $this->getEmailService()->sendEmail();
226    }
227
228    /**
229     *
230     * @return EmailService
231     */
232    public function getEmailService() {
233        if (!($this->emailService instanceof EmailService)) {
234            $this->emailService = new EmailService();
235        }
236        return $this->emailService;
237    }
238
239    /**
240     *
241     * @param EmailService $emailService
242     */
243    public function setEmailService(EmailService $emailService) {
244        $this->emailService = $emailService;
245    }
246
247    /**
248     * @param Employee $receiver
249     * @param $resetCode
250     * @return string
251     */
252    protected function generatePasswordResetEmailBody(Employee $receiver, $resetCode) {
253        $resetLink = public_path('index.php/auth/resetPassword', true);
254
255        $placeholders = array('firstName', 'lastName', 'passwordResetLink', 'code', 'passwordResetCodeLink');
256        $replacements = array(
257            $receiver->getFirstName(),
258            $receiver->getLastName(),
259            $resetLink,
260            $resetCode,
261        );
262
263        return $this->generateEmailBody('password-reset-request.txt', $placeholders, $replacements);
264    }
265
266    /**
267     *
268     * @param string $templateFile
269     * @param array $placeholders
270     * @param array $replacements
271     * @return string
272     */
273    protected function generateEmailBody($templateFile, array $placeholders, array $replacements) {
274        $body = file_get_contents(sfConfig::get('sf_plugins_dir') .
275            DIRECTORY_SEPARATOR.'orangehrmSecurityAuthenticationPlugin'.DIRECTORY_SEPARATOR.
276            'config'.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR . $templateFile);
277
278        foreach ($placeholders as $key => $value) {
279            $placeholders[$key] = "/\{{$value}\}/";
280        }
281
282        $body = preg_replace($placeholders, $replacements, $body);
283        return $body;
284    }
285
286    /**
287     * @param ResetPassword $resetPassword
288     * @return mixed
289     * @throws ServiceException
290     */
291    public function saveResetPasswordLog(ResetPassword $resetPassword) {
292        try {
293            return $this->getPasswordResetDao()->saveResetPasswordLog($resetPassword);
294        } catch (Exception $e) {
295            throw new ServiceException($e->getMessage());
296        }
297    }
298
299    /**
300     * @param $passwordResetData
301     * @param $resetCode
302     * @return bool
303     * @throws ServiceException
304     */
305    public function saveNewPassword($passwordResetData, $resetCode) {
306
307        $userNameMetaData= $this->extractPasswordResetMetaData($resetCode);
308        $username=$userNameMetaData[0];
309        $newPrimaryPassword = $passwordResetData['newPrimaryPassword'];
310        $primaryPasswordConfirmation = $passwordResetData['primaryPasswordConfirmation'];
311
312        $user = $this->getSystemUserService()->searchSystemUsers(array('userName' => $username, 'limit' => 1))->get(0);
313        $email = $user->getEmployee()->getEmpWorkEmail();
314        $resetPasswordLogByEmail = $this->getPasswordResetDao()->getResetPasswordLogByEmail($email);
315
316        $expireDate = $this->hasPasswordResetRequest($email);
317
318        if ($newPrimaryPassword !== $primaryPasswordConfirmation) {
319            throw new ServiceException(__('New primary password and the confirmation does not match'));
320        } else if(!$expireDate) {
321            $this->getPasswordResetDao()->deletePasswordResetRequestsByEmail($email);
322            throw new ServiceException(__('This link is expired, Please request again'));
323        }elseif($resetPasswordLogByEmail['reset_code']!==$resetCode) {
324            throw new ServiceException(__('Key does not match'));
325        }else {
326            try {
327                $primaryHash = $this->getSystemUserService()->hashPassword($newPrimaryPassword);
328                $success = (bool)$this->getPasswordResetDao()->saveNewPrimaryPassword($username, $primaryHash);
329                $this->getPasswordResetDao()->deletePasswordResetRequestsByEmail($email);
330                return $success;
331            } catch (Exception $e) {
332                throw new ServiceException($e->getMessage());
333            }
334        }
335    }
336
337    /**
338     *
339     * @param string $resetCode
340     * @return array
341     */
342    public function extractPasswordResetMetaData($resetCode) {
343        $code = Base64Url::decode($resetCode);
344
345        $metaData = explode('#SEPARATOR#', $code);
346
347        array_pop($metaData);
348
349        return $metaData;
350    }
351
352    /**
353     *
354     * @return SystemUserService
355     */
356    public function getSystemUserService() {
357        if (!($this->systemUserService instanceof SystemUserService)) {
358            $this->systemUserService = new SystemUserService();
359        }
360        return $this->systemUserService;
361    }
362
363    /**
364     *
365     * @param SystemUserService $systemUserService
366     */
367    public function setSystemUserService($systemUserService) {
368        $this->systemUserService = $systemUserService;
369    }
370
371}
372