Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.15% covered (success)
95.15%
98 / 103
75.00% covered (warning)
75.00%
3 / 4
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
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
9.02
build_output_html_regenerate
100.00% covered (success)
100.00%
8 / 8
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    
41    foreach ($search as $str) {
42        $replace[] = $language->translate($str);
43    }
44    
45    $search[] = '@REPLACE_USERNAME';
46    $replace[] = $username;
47    $search[] = '@REPLACE_PASSWORD';
48    $replace[] = $password;
49
50    $subject = $language->translate('@password_regenerate_title');
51    $html = str_replace($search, $replace, $html);
52    
53    return email_send($html, AppConstants::NoReplyMail, $usermail, $subject);
54}
55
56function password_encode_regenerate(string $password): string {
57    for ($times = 0; $times < 2; $times++) {
58        $password = dechex((int) crc32($password));
59    }
60    
61    return $password;
62}
63
64/**
65 * Regenerates user password, updates DB and sends email.
66 *
67 * @return int ApplicationError code.
68 */
69function regenerate_password(\conx $conx, object $language, string $username, \Psr\Log\LoggerInterface $logger): int {
70    $retval = ApplicationError::Success;
71    $user = null;
72
73    $result = [];
74    $res = $conx->query('USER', ['email' => $username, 'status' => ['PA', 'AC']], $result);
75    if (!$res || count($result) === 0) {
76        $logger->error("No valid user found for email '".$username."'.");
77        return ApplicationError::NotFound;
78    }
79    
80    $user = $result[0];
81    srand();
82    $newpassword = '';
83    
84    for ($i = 0; $i < 12; $i++) {
85        switch (rand(0, 2)) {
86            case 0:
87                $newpassword .= chr(rand(0x30, 0x39));
88                break;
89            case 1:
90                $newpassword .= chr(rand(0x41, 0x5A));
91                break;
92            case 2:
93                $newpassword .= chr(rand(0x61, 0x7A));
94                break;
95        }
96    }
97
98    $user['password'] = password_encode_regenerate($newpassword);
99    
100    $ok = $conx->transactional(function (\transaction $transaction) use (&$user) {
101        return (bool) $transaction->update('USER', $user);
102    });
103    
104    if (!$ok) {
105        return ApplicationError::Generic;
106    }
107
108    if (!inform_to_user_regenerate($language, $username, $newpassword, $user['email'])) {
109        $logger->warning("Could not send password change for '".$username."' email ");
110    }
111
112    return $retval;
113}
114
115function build_output_html_regenerate(string $language, int $errorcode): string {
116    $result = ($errorcode === ApplicationError::Success) ? 'SUCCESS' : 'FAILURE';
117    $dsturl = AppConstants::IonUrl().'on-password?lang='.$language.'&result='.$result;
118    
119    $html = "<!DOCTYPE html>";
120    $html .= "<html>";
121    $html .= "<head><meta http-equiv='refresh' content='0; URL=".$dsturl."'></head>";
122    $html .= "<body><p>You are being redirected. If not, please follow <a href='".$dsturl."'>this link</a>.</p></body>";
123    $html .= "</html>";
124    
125    return $html;
126}
127
128/**
129 * Action callable: regenerate password from recovery link (GET authorization, lang).
130 *
131 * Under a per-auth mutex (`with_lock`), validates and invalidates the AUTHORIZATION
132 * row. Password update + email (`regenerate_password`) run after the lock is
133 * released so SMTP cannot outlive the Redis mutex TTL.
134 *
135 * @param string $body Request body (unused).
136 * @param array<string, mixed> $query Query params: authorization, lang.
137 * @param \ConxHelper $conx Connection helper (from LoginService; uses ->global for central DB).
138 * @param \Psr\Log\LoggerInterface $logger Logger (from LoggerFactory).
139 * @param \UppServices\SessionService $sessionService Session service (unused).
140 * @return array{output: string, contentType: string} HTML output and content type.
141 */
142return function (string $body, array $query, \ConxHelper $conx, \Psr\Log\LoggerInterface $logger, \UppServices\SessionService $sessionService): array {
143    require_once __DIR__.'/../legacy/base/mutex.php';
144
145    $retval = ApplicationError::Success;
146    $auth = $query['authorization'] ?? null;
147    if (!$auth) {
148        $logger->error("No authorization key provided on URL. Operation cancelled.");
149        $retval = ApplicationError::Parameters;
150    }
151
152    $email = null;
153    if (success($retval)) {
154        $identifier = 'regenerate_'.preg_replace('#[^a-zA-Z0-9\-]#', '_', (string) $auth);
155        if (!with_lock($identifier, MUTEX_NAME, function () use ($conx, $auth, $logger, &$retval, &$email) {
156            $logger->info("Checking authorization key for password recovery..");
157
158            $item = use_authorization($conx, $auth);
159            if (!$item) {
160                $logger->error("Authorization key '".$auth."' not found or invalid.");
161                $retval = ApplicationError::Unauthorized;
162            }
163            else {
164                $logger->info("Authorization check test sucessfull.");
165                if (!del_authorization($conx, $item)) {
166                    $logger->error("Could not invalidate authorization key '".$auth."'");
167                }
168                
169                if (success($retval)) {
170                    $email = $item['email'];
171                }
172            }
173        })) {
174            $logger->error("Could not acquire lock for auth key");
175            $retval = ApplicationError::Generic;
176        }
177    }
178
179    if (success($retval) && $email !== null) {
180        $lang = (string) ($query['lang'] ?? 'EN');
181        $language = new Language($lang);
182        $retval = regenerate_password($conx->global, $language, $email, $logger);
183    }
184
185    $lang = (string) ($query['lang'] ?? 'EN');
186    $output = build_output_html_regenerate($lang, $retval);
187    
188    return ['output' => $output, 'contentType' => 'text/html; charset=utf-8'];
189};