Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace factor_email;
18
 
19
use stdClass;
20
use tool_mfa\local\factor\object_factor_base;
21
 
22
/**
23
 * Email factor class.
24
 *
25
 * @package     factor_email
26
 * @subpackage  tool_mfa
27
 * @author      Mikhail Golenkov <golenkovm@gmail.com>
28
 * @copyright   Catalyst IT
29
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 */
31
class factor extends object_factor_base {
32
 
33
    /** @var string Factor icon */
34
    protected $icon = 'fa-envelope';
35
 
36
    /**
37
     * E-Mail Factor implementation.
38
     *
39
     * @param \MoodleQuickForm $mform
40
     * @return \MoodleQuickForm $mform
41
     */
42
    public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
43
        $mform->addElement(new \tool_mfa\local\form\verification_field());
44
        $mform->setType('verificationcode', PARAM_ALPHANUM);
45
        return $mform;
46
    }
47
 
48
    /**
49
     * E-Mail Factor implementation.
50
     *
51
     * @param \MoodleQuickForm $mform Form to inject global elements into.
52
     * @return \MoodleQuickForm $mform
53
     */
54
    public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
55
        $this->generate_and_email_code();
56
        return $mform;
57
    }
58
 
59
    /**
60
     * Sends and e-mail to user with given verification code.
61
     *
62
     * @param int $instanceid
63
     * @return void
64
     */
65
    public static function email_verification_code(int $instanceid): void {
66
        global $PAGE, $USER;
67
        $noreplyuser = \core_user::get_noreply_user();
68
        $subject = get_string('email:subject', 'factor_email');
69
        $renderer = $PAGE->get_renderer('factor_email');
70
        $body = $renderer->generate_email($instanceid);
71
        email_to_user($USER, $noreplyuser, $subject, $body, $body);
72
    }
73
 
74
    /**
75
     * E-Mail Factor implementation.
76
     *
77
     * @param array $data
78
     * @return array
79
     */
80
    public function login_form_validation(array $data): array {
81
        global $USER;
82
        $return = [];
83
 
84
        if (!$this->check_verification_code($data['verificationcode'])) {
85
            $return['verificationcode'] = get_string('error:wrongverification', 'factor_email');
86
        }
87
 
88
        return $return;
89
    }
90
 
91
    /**
92
     * E-Mail Factor implementation.
93
     *
94
     * @param stdClass $user the user to check against.
95
     * @return array
96
     */
97
    public function get_all_user_factors(stdClass $user): array {
98
        global $DB;
99
 
100
        $records = $DB->get_records('tool_mfa', [
101
            'userid' => $user->id,
102
            'factor' => $this->name,
103
            'label' => $user->email,
104
        ]);
105
 
106
        if (!empty($records)) {
107
            return $records;
108
        }
109
 
110
        // Null records returned, build new record.
111
        $record = [
112
            'userid' => $user->id,
113
            'factor' => $this->name,
114
            'label' => $user->email,
115
            'createdfromip' => $user->lastip,
116
            'timecreated' => time(),
117
            'revoked' => 0,
118
        ];
119
        $record['id'] = $DB->insert_record('tool_mfa', $record, true);
120
        return [(object) $record];
121
    }
122
 
123
    /**
124
     * E-Mail Factor implementation.
125
     *
126
     * {@inheritDoc}
127
     */
128
    public function has_input(): bool {
129
        if (self::is_ready()) {
130
            return true;
131
        }
132
        return false;
133
    }
134
 
135
    /**
136
     * E-Mail Factor implementation.
137
     *
138
     * {@inheritDoc}
139
     */
140
    public function get_state(): string {
141
        if (!self::is_ready()) {
142
            return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
143
        }
144
 
145
        return parent::get_state();
146
    }
147
 
148
    /**
149
     * Checks whether user email is correctly configured.
150
     *
151
     * @return bool
152
     */
153
    private static function is_ready(): bool {
154
        global $DB, $USER;
155
 
156
        if (empty($USER->email)) {
157
            return false;
158
        }
159
        if (!validate_email($USER->email)) {
160
            return false;
161
        }
162
        if (over_bounce_threshold($USER)) {
163
            return false;
164
        }
165
 
166
        // If this factor is revoked, set to not ready.
167
        if ($DB->record_exists('tool_mfa', ['userid' => $USER->id, 'factor' => 'email', 'revoked' => 1])) {
168
            return false;
169
        }
170
        return true;
171
    }
172
 
173
    /**
174
     * Generates and emails the code for login to the user, stores codes in DB.
175
     *
176
     * @return void
177
     */
178
    private function generate_and_email_code(): void {
179
        global $DB, $USER;
180
 
181
        // Get instance that isnt parent email type (label check).
182
        // This check must exclude the main singleton record, with the label as the email.
183
        // It must only grab the record with the user agent as the label.
184
        $sql = 'SELECT *
185
                  FROM {tool_mfa}
186
                 WHERE userid = ?
187
                   AND factor = ?
188
               AND NOT label = ?';
189
 
190
        $record = $DB->get_record_sql($sql, [$USER->id, 'email', $USER->email]);
191
        $duration = get_config('factor_email', 'duration');
192
        $newcode = random_int(100000, 999999);
193
 
194
        if (empty($record)) {
195
            // No code active, generate new code.
196
            $instanceid = $DB->insert_record('tool_mfa', [
197
                'userid' => $USER->id,
198
                'factor' => 'email',
199
                'secret' => $newcode,
200
                'label' => $_SERVER['HTTP_USER_AGENT'],
201
                'timecreated' => time(),
202
                'createdfromip' => $USER->lastip,
203
                'timemodified' => time(),
204
                'lastverified' => time(),
205
                'revoked' => 0,
206
            ], true);
207
            $this->email_verification_code($instanceid);
208
        } else if ($record->timecreated + $duration < time()) {
209
            // Old code found. Keep id, update fields.
210
            $DB->update_record('tool_mfa', [
211
                'id' => $record->id,
212
                'secret' => $newcode,
213
                'label' => $_SERVER['HTTP_USER_AGENT'],
214
                'timecreated' => time(),
215
                'createdfromip' => $USER->lastip,
216
                'timemodified' => time(),
217
                'lastverified' => time(),
218
                'revoked' => 0,
219
            ]);
220
            $instanceid = $record->id;
221
            $this->email_verification_code($instanceid);
222
        }
223
    }
224
 
225
    /**
226
     * Verifies entered code against stored DB record.
227
     *
228
     * @param string $enteredcode
229
     * @return bool
230
     */
231
    private function check_verification_code(string $enteredcode): bool {
232
        global $DB, $USER;
233
        $duration = get_config('factor_email', 'duration');
234
 
235
        // Get instance that isnt parent email type (label check).
236
        // This check must exclude the main singleton record, with the label as the email.
237
        // It must only grab the record with the user agent as the label.
238
        $sql = 'SELECT *
239
                  FROM {tool_mfa}
240
                 WHERE userid = ?
241
                   AND factor = ?
242
               AND NOT label = ?';
243
        $record = $DB->get_record_sql($sql, [$USER->id, 'email', $USER->email]);
244
 
245
        if ($enteredcode == $record->secret) {
246
            if ($record->timecreated + $duration > time()) {
247
                return true;
248
            }
249
        }
250
        return false;
251
    }
252
 
253
    /**
254
     * Cleans up email records once MFA passed.
255
     *
256
     * {@inheritDoc}
257
     */
258
    public function post_pass_state(): void {
259
        global $DB, $USER;
260
        // Delete all email records except base record.
261
        $selectsql = 'userid = ?
262
                  AND factor = ?
263
              AND NOT label = ?';
264
        $DB->delete_records_select('tool_mfa', $selectsql, [$USER->id, 'email', $USER->email]);
265
 
266
        // Update factor timeverified.
267
        parent::post_pass_state();
268
    }
269
 
270
    /**
271
     * Email factor implementation.
272
     * Email page must be safe to authorise session from link.
273
     *
274
     * {@inheritDoc}
275
     */
276
    public function get_no_redirect_urls(): array {
277
        $email = new \moodle_url('/admin/tool/mfa/factor/email/email.php');
278
        return [$email];
279
    }
280
 
281
    /**
282
     * Email factor implementation.
283
     *
284
     * @param stdClass $user
285
     */
286
    public function possible_states(stdClass $user): array {
287
        // Email can return all states.
288
        return [
289
            \tool_mfa\plugininfo\factor::STATE_FAIL,
290
            \tool_mfa\plugininfo\factor::STATE_PASS,
291
            \tool_mfa\plugininfo\factor::STATE_NEUTRAL,
292
            \tool_mfa\plugininfo\factor::STATE_UNKNOWN,
293
        ];
294
    }
295
 
296
    /**
297
     * Obscure an email address by replacing all but the first and last character of the local part with a dot.
298
     * So the users full email isn't displayed during login.
299
     *
300
     * @param string $email The email address to obfuscate.
301
     * @return string
302
     * @throws \coding_exception
303
     */
304
    protected function obfuscate_email(string $email): string {
305
        // Split the email address at the '@' symbol.
306
        $parts = explode('@', $email);
307
 
308
        if (count($parts) != 2) {
309
            throw new \coding_exception('Invalid email format');
310
        }
311
 
312
        $local = $parts[0];
313
        $domain = $parts[1];
314
 
315
        // Obfuscate all but the first and last character of the local part.
316
        $length = strlen($local);
317
        $middledot = "\u{00B7}";
318
        if ($length > 2) {
319
            $local = $local[0] . str_repeat($middledot, $length - 2) . $local[$length - 1];
320
        }
321
 
322
        // Put the email address back together and return it.
323
        return $local . '@' . $domain;
324
    }
325
 
326
    /**
327
     * Get the login description associated with this factor.
328
     * Override for factors that have a user input.
329
     *
330
     * @return string The login option.
331
     */
332
    public function get_login_desc(): string {
333
        global $USER;
334
        $email = $this->obfuscate_email($USER->email);
335
 
336
        return get_string('logindesc', 'factor_' . $this->name, $email);
337
    }
338
}