Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.75% covered (success)
93.75%
105 / 112
66.67% covered (warning)
66.67%
4 / 6
CRAP
n/a
0 / 0
inform_to_user_regenerate
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
2
password_encode_regenerate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
regenerate_password
90.91% covered (success)
90.91%
30 / 33
0.00% covered (danger)
0.00%
0 / 1
11.09
build_output_html_regenerate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
acquire_auth_lock
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
2.09
release_auth_lock
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * @file
5 * Login action: regenerate password from recovery link.
6 * Logic inlined from legacy/login/regenerate.php; no longer requires that file.
7 */
8
9declare(strict_types=1);
10
11require_once __DIR__.'/../../Legacy/common/authorize.php';
12require_once __DIR__.'/../../Legacy/mailing/mailing.php';
13
14/** Path to legacy login html templates (regenerate). */
15define('REGENERATE_HTML_DIR', __DIR__.'/../legacy/login/html/regenerate');
16
17/**
18 * Sends email to user with the new password.
19 */
20function inform_to_user_regenerate(object $language, string $username, string $password, string $usermail): bool {
21    $html = '';
22    email_begin($html, '@password_regenerate_title');
23    $content1 = file_get_contents(REGENERATE_HTML_DIR.'/1.html');
24    $content2 = file_get_contents(REGENERATE_HTML_DIR.'/2.html');
25    $content3 = file_get_contents(REGENERATE_HTML_DIR.'/3.html');
26    email_content($html, $content1, $content2, $content3);
27    email_end($html, '@password_regenerate_footer');
28
29    $replace = [];
30    $search = [
31        '@password_regenerate_title',
32        '@password_regenerate_footer',
33        '@regenerate_email_reason',
34        '@regenerate_email_norespond',
35        '@regenerate_email_accessdata',
36        '@regenerate_email_username',
37        '@regenerate_email_password',
38        '@regenerate_email_onlogin'
39    ];
40    foreach ($search as $str) {
41        $replace[] = $language->translate($str);
42    }
43    $search[] = '@REPLACE_USERNAME';
44    $replace[] = $username;
45    $search[] = '@REPLACE_PASSWORD';
46    $replace[] = $password;
47
48    $subject = $language->translate('@password_regenerate_title');
49    $html = str_replace($search, $replace, $html);
50    return email_send($html, AppConstants::NoReplyMail, $usermail, $subject);
51}
52
53function password_encode_regenerate(string $password): string {
54    for ($times = 0; $times < 2; $times++) {
55        $password = dechex((int) crc32($password));
56    }
57    return $password;
58}
59
60/**
61 * Regenerates user password, updates DB and sends email.
62 *
63 * @return int ApplicationError code.
64 */
65function regenerate_password(\conx $conx, object $language, string $username, \Psr\Log\LoggerInterface $logger): int {
66    $retval = ApplicationError::Success;
67    $user = null;
68    $transaction = null;
69
70    $result = [];
71    $res = $conx->query('USER', ['email' => $username, 'status' => ['PA', 'AC']], $result);
72    if (!$res || count($result) === 0) {
73        $logger->error("No valid user found for email '".$username."'.");
74        return ApplicationError::NotFound;
75    }
76    $user = $result[0];
77
78    srand();
79    $newpassword = '';
80    for ($i = 0; $i < 12; $i++) {
81        switch (rand(0, 2)) {
82            case 0:
83                $newpassword .= chr(rand(0x30, 0x39));
84                break;
85            case 1:
86                $newpassword .= chr(rand(0x41, 0x5A));
87                break;
88            case 2:
89                $newpassword .= chr(rand(0x61, 0x7A));
90                break;
91        }
92    }
93
94    $transaction = $conx->begin();
95    if (!$transaction) {
96        return ApplicationError::Generic;
97    }
98    $user['password'] = password_encode_regenerate($newpassword);
99    if (!$transaction->update('USER', $user)) {
100        $retval = ApplicationError::Generic;
101    }
102
103    if (success($retval)) {
104        if (!inform_to_user_regenerate($language, $username, $newpassword, $user['email'])) {
105            $logger->warning("Could not send password change for '".$username."' email ");
106        }
107    }
108
109    $transaction->flush(success($retval));
110
111    return $retval;
112}
113
114function build_output_html_regenerate(string $language, int $errorcode): string {
115    $result = ($errorcode === ApplicationError::Success) ? 'SUCCESS' : 'FAILURE';
116    $dsturl = AppConstants::IonUrl().'on-password?lang='.$language.'&result='.$result;
117    $html = "<!DOCTYPE html>";
118    $html .= "<html>";
119    $html .= "<head><meta http-equiv='refresh' content='0; URL=".$dsturl."'></head>";
120    $html .= "<body><p>You are being redirected. If not, please follow <a href='".$dsturl."'>this link</a>.</p></body>";
121    $html .= "</html>";
122    return $html;
123}
124
125/**
126 * Acquires an exclusive lock for the given auth key (prevents concurrent use).
127 *
128 * @param string $auth Authorization key.
129 * @param \Psr\Log\LoggerInterface $logger Logger for errors.
130 * @return resource|false File handle or false on failure.
131 */
132/**
133 * Acquires an exclusive lock for the given auth key (prevents concurrent regenerate for same key).
134 *
135 * @param string $auth Authorization key.
136 * @param \Psr\Log\LoggerInterface $logger Logger for errors.
137 * @return \Mutex|false Mutex instance holding the lock, or false on failure.
138 */
139function acquire_auth_lock(string $auth, \Psr\Log\LoggerInterface $logger) {
140    require_once __DIR__.'/../legacy/base/mutex.php';
141    $identifier = 'regenerate_'.preg_replace('#[^a-zA-Z0-9\-]#', '_', $auth);
142    $mutex = new Mutex($identifier);
143    if (!$mutex->lock()) {
144        $logger->error("Could not acquire lock for auth key");
145        return false;
146    }
147    return $mutex;
148}
149
150/**
151 * Releases the lock acquired by acquire_auth_lock.
152 *
153 * @param \Mutex|false $lock Mutex instance from acquire_auth_lock.
154 */
155function release_auth_lock($lock): void {
156    if ($lock instanceof Mutex) {
157        $lock->unlock();
158    }
159}
160
161/**
162 * Action callable: regenerate password from recovery link (GET authorization, lang).
163 *
164 * Acquires auth lock, validates authorization, regenerates password, sends email,
165 * returns HTML redirect to IonUrl on-password.
166 *
167 * @param string $body Request body (unused).
168 * @param array<string, mixed> $query Query params: authorization, lang.
169 * @param \ConxHelper $conx Connection helper (from LoginService; uses ->global for central DB).
170 * @param \Psr\Log\LoggerInterface $logger Logger (from LoggerFactory).
171 * @param \UppServices\SessionService $sessionService Session service (unused).
172 * @return array{output: string, contentType: string} HTML output and content type.
173 */
174return function (string $body, array $query, \ConxHelper $conx, \Psr\Log\LoggerInterface $logger, \UppServices\SessionService $sessionService): array {
175    $retval = ApplicationError::Success;
176    $auth = $query['authorization'] ?? null;
177    if (!$auth) {
178        $logger->error("No authorization key provided on URL. Operation cancelled.");
179        $retval = ApplicationError::Parameters;
180    }
181
182    $lock = false;
183    if (success($retval)) {
184        $lock = acquire_auth_lock((string) $auth, $logger);
185        if ($lock === false) {
186            $retval = ApplicationError::Generic;
187        }
188    }
189
190    if (success($retval)) {
191        $lang = (string) ($query['lang'] ?? 'EN');
192        $language = new Language($lang);
193        $logger->info("Checking authorization key for password recovery..");
194
195        $item = use_authorization($conx, $auth);
196        if (!$item) {
197            $logger->error("Authorization key '".$auth."' not found or invalid.");
198            $retval = ApplicationError::Unauthorized;
199        } 
200        else {
201            $logger->info("Authorization check test sucessfull.");
202            if (!del_authorization($conx, $item)) {
203                $logger->error("Could not invalidate authorization key '".$auth."'");
204            }
205        }
206        
207        if (success($retval)) {
208            $retval = regenerate_password($conx->global, $language, $item['email'], $logger);
209        }
210        release_auth_lock($lock);
211    }
212
213    $lang = (string) ($query['lang'] ?? 'EN');
214    $output = build_output_html_regenerate($lang, $retval);
215    return ['output' => $output, 'contentType' => 'text/html; charset=utf-8'];
216};