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 tool_mfa\output;
18
 
19
use core\context\system;
20
use tool_mfa\local\factor\object_factor;
21
use tool_mfa\local\form\login_form;
22
use \html_writer;
23
use tool_mfa\plugininfo\factor;
24
 
25
/**
26
 * MFA renderer.
27
 *
28
 * @package     tool_mfa
29
 * @author      Mikhail Golenkov <golenkovm@gmail.com>
30
 * @copyright   Catalyst IT
31
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 */
33
class renderer extends \plugin_renderer_base {
34
 
35
    /**
36
     * Returns the state of the factor as a badge.
37
     *
38
     * @param string $state
39
     * @return string
40
     */
41
    public function get_state_badge(string $state): string {
42
 
43
        switch ($state) {
44
            case factor::STATE_PASS:
45
                return html_writer::tag('span', get_string('state:pass', 'tool_mfa'), ['class' => 'badge bg-success text-white']);
46
 
47
            case factor::STATE_FAIL:
48
                return html_writer::tag('span', get_string('state:fail', 'tool_mfa'), ['class' => 'badge bg-danger text-white']);
49
 
50
            case factor::STATE_NEUTRAL:
51
                return html_writer::tag('span', get_string('state:neutral', 'tool_mfa'),
52
                    ['class' => 'badge bg-warning text-dark']);
53
 
54
            case factor::STATE_UNKNOWN:
55
                return html_writer::tag('span', get_string('state:unknown', 'tool_mfa'),
56
                    ['class' => 'badge bg-secondary text-dark']);
57
 
58
            case factor::STATE_LOCKED:
59
                return html_writer::tag('span', get_string('state:locked', 'tool_mfa'), ['class' => 'badge bg-danger text-white']);
60
 
61
            default:
62
                return html_writer::tag('span', get_string('pending', 'tool_mfa'), ['class' => 'badge bg-secondary text-dark']);
63
        }
64
    }
65
 
66
    /**
67
     * Returns a list of factors which a user can add.
68
     *
69
     * @return string
70
     */
71
    public function available_factors(): string {
72
        global $USER;
73
        $factors = factor::get_enabled_factors();
74
        $data = [];
75
 
76
        foreach ($factors as $factor) {
77
 
78
            // Allow all factors with setup and button.
79
            // Make an exception for email factor as this is currently set up by admins only and required on this list.
80
            if ((!$factor->has_setup() || !$factor->show_setup_buttons()) && !$factor instanceof \factor_email\factor) {
81
                continue;
82
            }
83
 
84
            $userfactors = $factor->get_active_user_factors($USER);
85
            $active = !empty($userfactors) ?? false;
86
            $button = null;
87
            $icon = $factor->get_icon();
88
            $params = [
89
                'action' => 'setup',
90
                'factor' => $factor->name,
91
            ];
92
 
93
            if (!$active) {
94
                // Not active yet and requires set up.
95
                $info = $factor->get_info();
96
 
97
                if ($factor->show_setup_buttons()) {
98
                    $params['action'] = 'setup';
99
                    $button = new \single_button(
100
                        url: new \moodle_url('action.php', $params),
101
                        label: $factor->get_setup_string(),
102
                        method: 'post',
103
                        type: \single_button::BUTTON_PRIMARY,
104
                        attributes: [
105
                            'aria-label' => get_string('setupfactor', 'factor_' . $factor->name),
106
                        ],
107
                    );
108
                    $button = $button->export_for_template($this->output);
109
                }
110
 
111
            } else {
112
                // Active and can be managed.
113
                $factorid = reset($userfactors)->id;
114
                $info = $factor->get_manage_info($factorid);
115
 
116
                if ($factor->show_setup_buttons()) {
117
                    $params['action'] = 'manage';
118
                    $button = new \single_button(
119
                        url: new \moodle_url('action.php', $params),
120
                        label: $factor->get_manage_string(),
121
                        method: 'post',
122
                        type: \single_button::BUTTON_PRIMARY,
123
                        attributes: [
124
                            'aria-label' => get_string('managefactor', 'factor_' . $factor->name),
125
                        ],
126
                    );
127
                    $button = $button->export_for_template($this->output);
128
                }
129
            }
130
 
131
            // Prepare data for template.
132
            $data['factors'][] = [
133
                'active' => $active,
134
                'label' => $factor->get_display_name(),
135
                'name' => $factor->name,
136
                'info' => $info,
137
                'icon' => $icon,
138
                'button' => $button,
139
            ];
140
        }
141
 
142
        return $this->render_from_template('tool_mfa/mfa_selector', $data);
143
    }
144
 
145
    /**
146
     * Returns the html section for factor setup
147
     *
148
     * @param object $factor object of the factor class
149
     * @return string
150
     * @deprecated since Moodle 4.4
151
     * @todo Final deprecation in Moodle 4.8 MDL-80995
152
     */
153
    public function setup_factor(object $factor): string {
154
        debugging('The method setup_factor() has been deprecated. The HTML derived from this method is no longer needed.
155
            Similar HTML is now achieved as part of available_factors().', DEBUG_DEVELOPER);
156
        $html = '';
157
 
158
        $html .= html_writer::start_tag('div', ['class' => 'card']);
159
 
160
        $html .= html_writer::tag('h4', $factor->get_display_name(), ['class' => 'card-header']);
161
        $html .= html_writer::start_tag('div', ['class' => 'card-body']);
162
        $html .= $factor->get_info();
163
 
164
        $setupparams = ['action' => 'setup', 'factor' => $factor->name, 'sesskey' => sesskey()];
165
        $setupurl = new \moodle_url('action.php', $setupparams);
166
        $html .= $this->output->single_button($setupurl, $factor->get_setup_string());
167
        $html .= html_writer::end_tag('div');
168
        $html .= html_writer::end_tag('div');
169
        $html .= '<br>';
170
 
171
        return $html;
172
    }
173
 
174
    /**
175
     * Show a table displaying a users active factors.
176
     *
177
     * @param string|null $filterfactor The factor name to filter on.
178
     * @return string $html
179
     * @throws \coding_exception
180
     */
181
    public function active_factors(string $filterfactor = null): string {
182
        global $USER, $CFG;
183
 
184
        require_once($CFG->dirroot . '/iplookup/lib.php');
185
 
186
        $html = '';
187
 
188
        $headers = get_strings([
189
            'devicename',
190
            'added',
191
            'lastused',
192
            'replace',
193
            'remove',
194
        ], 'tool_mfa');
195
 
196
        $table = new \html_table();
197
        $table->id = 'active_factors';
198
        $table->attributes['class'] = 'generaltable table table-bordered';
199
        $table->head  = [
200
            $headers->devicename,
201
            $headers->added,
202
            $headers->lastused,
203
            $headers->replace,
204
            $headers->remove,
205
        ];
206
        $table->colclasses = [
207
            'text-left',
208
            'text-left',
209
            'text-left',
210
            'text-center',
211
            'text-center',
212
        ];
213
        $table->data  = [];
214
 
215
        $factors = factor::get_enabled_factors();
216
        $hasmorethanone = factor::user_has_more_than_one_active_factors();
217
 
218
        foreach ($factors as $factor) {
219
 
220
            // Filter results to match the specified factor.
221
            if (!empty($filterfactor) && $factor->name !== $filterfactor) {
222
                continue;
223
            }
224
 
225
            $userfactors = $factor->get_active_user_factors($USER);
226
 
227
            if (!$factor->has_setup()) {
228
                continue;
229
            }
230
 
231
            foreach ($userfactors as $userfactor) {
232
 
233
                // Revoke option.
234
                if ($factor->has_revoke() && $hasmorethanone) {
235
                    $content = $headers->remove;
236
                    $attributes = [
237
                        'data-action' => 'revoke',
238
                        'data-factor' => $factor->name,
239
                        'data-factorid' => $userfactor->id,
240
                        'data-factorname' => $factor->get_display_name(),
241
                        'data-devicename' => $userfactor->label,
242
                        'aria-label' => get_string('revokefactor', 'tool_mfa'),
243
                        'class' => 'btn btn-primary mfa-action-button',
244
                    ];
245
                    $revokebutton = \html_writer::tag('button', $content, $attributes);
246
                } else {
247
                    $revokebutton = get_string('statusna');
248
                }
249
 
250
                // Replace option.
251
                if ($factor->has_replace()) {
252
                    $content = $headers->replace;
253
                    $attributes = [
254
                        'data-action' => 'replace',
255
                        'data-factor' => $factor->name,
256
                        'data-factorid' => $userfactor->id,
257
                        'data-factorname' => $factor->get_display_name(),
258
                        'data-devicename' => $userfactor->label,
259
                        'aria-label' => get_string('replacefactor', 'tool_mfa'),
260
                        'class' => 'btn btn-primary mfa-action-button',
261
                    ];
262
                    $replacebutton = \html_writer::tag('button', $content, $attributes);
263
                } else {
264
                    $replacebutton = get_string('statusna');
265
                }
266
 
267
                $timecreated  = $userfactor->timecreated == '-' ? '-'
268
                    : userdate($userfactor->timecreated,  get_string('strftimedatetime'));
269
                $lastverified = $userfactor->lastverified;
270
                if ($lastverified == 0) {
271
                    $lastverified = '-';
272
                } else if ($lastverified != '-') {
273
                    $lastverified = userdate($userfactor->lastverified, get_string('strftimedatetime'));
274
                    $lastverified .= '<br>';
275
                    $lastverified .= get_string('ago', 'core_message', format_time(time() - $userfactor->lastverified));
276
                }
277
 
278
                $row = new \html_table_row([
279
                    $userfactor->label,
280
                    $timecreated,
281
                    $lastverified,
282
                    $replacebutton,
283
                    $revokebutton,
284
                ]);
285
                $table->data[] = $row;
286
            }
287
        }
288
        // If table has no data, don't output.
289
        if (count($table->data) == 0) {
290
            return '';
291
        }
292
        $html .= \html_writer::table($table);
293
        $html .= '<br>';
294
 
295
        return $html;
296
    }
297
 
298
    /**
299
     * Generates notification text for display when user cannot login.
300
     *
301
     * @return string $notification
302
     */
303
    public function not_enough_factors(): string {
304
        global $CFG, $SITE;
305
 
306
        $notification = \html_writer::tag('h4', get_string('error:notenoughfactors', 'tool_mfa'));
307
        $notification .= \html_writer::tag('p', get_string('error:reauth', 'tool_mfa'));
308
 
309
        // Support link.
310
        $supportemail = $CFG->supportemail;
311
        if (!empty($supportemail)) {
312
            $subject = get_string('email:subject', 'tool_mfa',
313
                format_string($SITE->fullname, true, ['context' => system::instance()]));
314
            $maillink = \html_writer::link("mailto:$supportemail?Subject=$subject", $supportemail);
315
            $notification .= get_string('error:support', 'tool_mfa');
316
            $notification .= \html_writer::tag('p', $maillink);
317
        }
318
 
319
        // Support page link.
320
        $supportpage = $CFG->supportpage;
321
        if (!empty($supportpage)) {
322
            $linktext = \html_writer::link($supportpage, $supportpage);
323
            $notification .= $linktext;
324
        }
325
        $return = $this->output->notification($notification, 'notifyerror', false);
326
 
327
        // Logout button.
328
        $url = new \moodle_url('/admin/tool/mfa/auth.php', ['logout' => 1]);
329
        $btn = new \single_button($url, get_string('logout'), 'post', \single_button::BUTTON_PRIMARY);
330
        $return .= $this->render($btn);
331
 
332
        $return .= $this->get_support_link();
333
 
334
        return $return;
335
    }
336
 
337
    /**
338
     * Displays a table of all factors in use currently.
339
     *
340
     * @param int $lookback the period to view.
341
     * @return string the HTML for the table
342
     */
343
    public function factors_in_use_table(int $lookback): string {
344
        global $DB;
345
 
346
        $factors = factor::get_factors();
347
 
348
        // Setup 2 arrays, one with internal names, one pretty.
349
        $columns = [''];
350
        $displaynames = $columns;
351
        $colclasses = ['center', 'center', 'center', 'center', 'center'];
352
 
353
        // Force the first 4 columns to custom data.
354
        $displaynames[] = get_string('totalusers', 'tool_mfa');
355
        $displaynames[] = get_string('usersauthedinperiod', 'tool_mfa');
356
        $displaynames[] = get_string('nonauthusers', 'tool_mfa');
357
        $displaynames[] = get_string('nologinusers', 'tool_mfa');
358
 
359
        foreach ($factors as $factor) {
360
            $columns[] = $factor->name;
361
            $displaynames[] = get_string('pluginname', 'factor_'.$factor->name);
362
            $colclasses[] = 'right';
363
        }
364
 
365
        // Add total column to the end.
366
        $displaynames[] = get_string('total');
367
        $colclasses[] = 'center';
368
 
369
        $table = new \html_table();
370
        $table->head = $displaynames;
371
        $table->align = $colclasses;
372
        $table->attributes['class'] = 'generaltable table table-bordered w-auto';
373
        $table->attributes['style'] = 'width: auto; min-width: 50%; margin-bottom: 0;';
374
 
375
        // Manually handle Total users and MFA users.
376
        $alluserssql = "SELECT auth,
377
                            COUNT(id)
378
                        FROM {user}
379
                        WHERE deleted = 0
380
                        AND suspended = 0
381
                    GROUP BY auth";
382
        $allusersinfo = $DB->get_records_sql($alluserssql, []);
383
 
384
        $noncompletesql = "SELECT u.auth, COUNT(u.id)
385
                             FROM {user} u
386
                        LEFT JOIN {tool_mfa_auth} mfaa ON u.id = mfaa.userid
387
                            WHERE u.lastlogin >= ?
388
                              AND (mfaa.lastverified < ?
389
                               OR mfaa.lastverified IS NULL)
390
                         GROUP BY u.auth";
391
        $noncompleteinfo = $DB->get_records_sql($noncompletesql, [$lookback, $lookback]);
392
 
393
        $nologinsql = "SELECT auth, COUNT(id)
394
                         FROM {user}
395
                        WHERE deleted = 0
396
                          AND suspended = 0
397
                          AND lastlogin < ?
398
                     GROUP BY auth";
399
        $nologininfo = $DB->get_records_sql($nologinsql, [$lookback]);
400
 
401
        $mfauserssql = "SELECT auth,
402
                            COUNT(DISTINCT tm.userid)
403
                        FROM {tool_mfa} tm
404
                        JOIN {user} u ON u.id = tm.userid
405
                        WHERE tm.lastverified >= ?
406
                        AND u.deleted = 0
407
                        AND u.suspended = 0
408
                    GROUP BY u.auth";
409
        $mfausersinfo = $DB->get_records_sql($mfauserssql, [$lookback]);
410
 
411
        $factorsusedsql = "SELECT CONCAT(u.auth, '_', tm.factor) as id,
412
                                COUNT(*)
413
                            FROM {tool_mfa} tm
414
                            JOIN {user} u ON u.id = tm.userid
415
                            WHERE tm.lastverified >= ?
416
                            AND u.deleted = 0
417
                            AND u.suspended = 0
418
                            AND (tm.revoked = 0 OR (tm.revoked = 1 AND tm.timemodified > ?))
419
                        GROUP BY CONCAT(u.auth, '_', tm.factor)";
420
        $factorsusedinfo = $DB->get_records_sql($factorsusedsql, [$lookback, $lookback]);
421
 
422
        // Auth rows.
423
        $authtypes = get_enabled_auth_plugins(true);
424
        foreach ($authtypes as $authtype) {
425
            $row = [];
426
            $row[] = \html_writer::tag('b', $authtype);
427
 
428
            // Setup the overall totals columns.
429
            $row[] = $allusersinfo[$authtype]->count ?? '-';
430
            $row[] = $mfausersinfo[$authtype]->count ?? '-';
431
            $row[] = $noncompleteinfo[$authtype]->count ?? '-';
432
            $row[] = $nologininfo[$authtype]->count ?? '-';
433
 
434
            // Create a running counter for the total.
435
            $authtotal = 0;
436
 
437
            // Now for each factor add the count from the factor query, and increment the running total.
438
            foreach ($columns as $column) {
439
                if (!empty($column)) {
440
                    // Get the information from the data key.
441
                    $key = $authtype . '_' . $column;
442
                    $count = $factorsusedinfo[$key]->count ?? 0;
443
                    $authtotal += $count;
444
 
445
                    $row[] = $count ? format_float($count, 0) : '-';
446
                }
447
            }
448
 
449
            // Append the total of all factors to final column.
450
            $row[] = $authtotal ? format_float($authtotal, 0) : '-';
451
 
452
            $table->data[] = $row;
453
        }
454
 
455
        // Total row.
456
        $totals = [0 => html_writer::tag('b', get_string('total'))];
457
        for ($colcounter = 1; $colcounter < count($row); $colcounter++) {
458
            $column = array_column($table->data, $colcounter);
459
            // Transform string to int forcibly, remove -.
460
            $column = array_map(function ($element) {
461
                return $element === '-' ? 0 : (int) $element;
462
            }, $column);
463
            $columnsum = array_sum($column);
464
            $colvalue = $columnsum === 0 ? '-' : $columnsum;
465
            $totals[$colcounter] = $colvalue;
466
        }
467
        $table->data[] = $totals;
468
 
469
        // Wrap in a div to cleanly scroll.
470
        return \html_writer::div(\html_writer::table($table), '', ['style' => 'overflow:auto;']);
471
    }
472
 
473
    /**
474
     * Displays a table of all factors in use currently.
475
     *
476
     * @return string the HTML for the table
477
     */
478
    public function factors_locked_table(): string {
479
        global $DB;
480
 
481
        $factors = factor::get_factors();
482
 
483
        $table = new \html_table();
484
 
485
        $table->attributes['class'] = 'generaltable table table-bordered w-auto';
486
        $table->attributes['style'] = 'width: auto; min-width: 50%';
487
 
488
        $table->head = [
489
            'factor' => get_string('factor', 'tool_mfa'),
490
            'active' => get_string('active'),
491
            'locked' => get_string('state:locked', 'tool_mfa'),
492
            'actions' => get_string('actions'),
493
        ];
494
        $table->align = [
495
            'left',
496
            'left',
497
            'right',
498
            'right',
499
        ];
500
        $table->data = [];
501
        $locklevel = (int) get_config('tool_mfa', 'lockout');
502
 
503
        foreach ($factors as $factor) {
504
            $sql = "SELECT COUNT(DISTINCT(userid))
505
                      FROM {tool_mfa}
506
                     WHERE factor = ?
507
                       AND lockcounter >= ?
508
                       AND revoked = 0";
509
            $lockedusers = $DB->count_records_sql($sql, [$factor->name, $locklevel]);
510
            $enabled = $factor->is_enabled() ? \html_writer::tag('b', get_string('yes')) : get_string('no');
511
 
512
            $actions = \html_writer::link( new \moodle_url($this->page->url,
513
                ['reset' => $factor->name, 'sesskey' => sesskey()]), get_string('performbulk', 'tool_mfa'));
514
            $lockedusers = \html_writer::link(new \moodle_url($this->page->url, ['view' => $factor->name]), $lockedusers);
515
 
516
            $table->data[] = [
517
                $factor->get_display_name(),
518
                $enabled,
519
                $lockedusers,
520
                $actions,
521
            ];
522
        }
523
 
524
        return \html_writer::table($table);
525
    }
526
 
527
    /**
528
     * Displays a table of all users with a locked instance of the given factor.
529
     *
530
     * @param object_factor $factor the factor class
531
     * @return string the HTML for the table
532
     */
533
    public function factor_locked_users_table(object_factor $factor): string {
534
        global $DB;
535
 
536
        $table = new \html_table();
537
        $table->attributes['class'] = 'generaltable table table-bordered w-auto';
538
        $table->attributes['style'] = 'width: auto; min-width: 50%';
539
        $table->head = [
540
            'userid' => get_string('userid', 'grades'),
541
            'fullname' => get_string('fullname'),
542
            'factorip' => get_string('ipatcreation', 'tool_mfa'),
543
            'lastip' => get_string('lastip'),
544
            'modified' => get_string('modified'),
545
            'actions' => get_string('actions'),
546
        ];
547
        $table->align = [
548
            'left',
549
            'left',
550
            'left',
551
            'left',
552
            'left',
553
            'right',
554
        ];
555
        $table->data = [];
556
 
557
        $locklevel = (int) get_config('tool_mfa', 'lockout');
558
        $sql = "SELECT mfa.id as mfaid, u.*, mfa.createdfromip, mfa.timemodified
559
                  FROM {tool_mfa} mfa
560
                  JOIN {user} u ON mfa.userid = u.id
561
                 WHERE factor = ?
562
                   AND lockcounter >= ?
563
                   AND revoked = 0";
564
        $records = $DB->get_records_sql($sql, [$factor->name, $locklevel]);
565
 
566
        foreach ($records as $record) {
567
            // Construct profile link.
568
            $proflink = \html_writer::link(new \moodle_url('/user/profile.php',
569
                ['id' => $record->id]), fullname($record));
570
 
571
            // IP link.
572
            $creatediplink = \html_writer::link(new \moodle_url('/iplookup/index.php',
573
                ['ip' => $record->createdfromip]), $record->createdfromip);
574
            $lastiplink = \html_writer::link(new \moodle_url('/iplookup/index.php',
575
                ['ip' => $record->lastip]), $record->lastip);
576
 
577
            // Deep link to logs.
578
            $logicon = $this->pix_icon('i/report', get_string('userlogs', 'tool_mfa'));
579
            $actions = \html_writer::link(new \moodle_url('/report/log/index.php', [
580
                'id' => 1, // Site.
581
                'user' => $record->id,
582
            ]), $logicon);
583
 
584
            $action = new \confirm_action(get_string('resetfactorconfirm', 'tool_mfa', fullname($record)));
585
            $actions .= $this->action_link(
586
                new \moodle_url($this->page->url, ['reset' => $factor->name, 'id' => $record->id, 'sesskey' => sesskey()]),
587
                $this->pix_icon('t/delete', get_string('resetconfirm', 'tool_mfa')),
588
                $action
589
            );
590
 
591
            $table->data[] = [
592
                $record->id,
593
                $proflink,
594
                $creatediplink,
595
                $lastiplink,
596
                userdate($record->timemodified, get_string('strftimedatetime', 'langconfig')),
597
                $actions,
598
            ];
599
        }
600
 
601
        return \html_writer::table($table);
602
    }
603
 
604
    /**
605
     * Returns a rendered support link.
606
     * If the MFA guidance page is enabled, this is returned.
607
     * Otherwise, the site support link is returned.
608
     * If neither support link is configured, an empty string is returned.
609
     *
610
     * @return string
611
     */
612
    public function get_support_link(): string {
613
        // Try the guidance page link first.
614
        if (get_config('tool_mfa', 'guidance')) {
615
            return $this->render_from_template('tool_mfa/guide_link', []);
616
        } else {
617
            return $this->output->supportemail([], true);
618
        }
619
    }
620
 
621
    /**
622
     * Renders an mform element from a template
623
     *
624
     * In certain situations, includes a script element which adds autosubmission behaviour.
625
     *
626
     * @param mixed $element element
627
     * @param bool $required if input is required field
628
     * @param bool $advanced if input is an advanced field
629
     * @param string|null $error error message to display
630
     * @param bool $ingroup True if this element is rendered as part of a group
631
     * @return mixed string|bool
632
     */
633
    public function mform_element(mixed $element, bool $required,
634
        bool $advanced, string|null $error, bool $ingroup): string|bool {
635
        $script = null;
636
        if ($element instanceof \tool_mfa\local\form\verification_field) {
637
            if ($this->page->pagelayout === 'secure') {
638
                $script = $element->secure_js();
639
            }
640
        }
641
 
642
        $result = parent::mform_element($element, $required, $advanced, $error, $ingroup);
643
 
644
        if (!empty($script) && $result !== false) {
645
            $result .= $script;
646
        }
647
 
648
        return $result;
649
    }
650
 
651
    /**
652
     * Renders the verification form.
653
     *
654
     * @param object_factor $factor The factor to render the form for.
655
     * @param login_form $form The login form object.
656
     * @return string
657
     * @throws \coding_exception
658
     * @throws \dml_exception
659
     * @throws \moodle_exception
660
     */
661
    public function verification_form(object_factor $factor, login_form $form): string {
662
        $allloginfactors = factor::get_all_user_login_factors();
663
        $additionalfactors = [];
664
        $disabledfactors = [];
665
        $displaycount = 0;
666
        $disablefactor = false;
667
 
668
        foreach ($allloginfactors as $loginfactor) {
669
            if ($loginfactor->name != $factor->name) {
670
                $additionalfactor = [
671
                        'name' => $loginfactor->name,
672
                        'icon' => $loginfactor->get_icon(),
673
                        'loginoption' => get_string('loginoption', 'factor_' . $loginfactor->name),
674
                ];
675
                // We mark the factor as disabled if it is locked.
676
                // We store the disabled factors in a separate array so that they can be displayed at the bottom of the template.
677
                if ($loginfactor->get_state() == factor::STATE_LOCKED) {
678
                    $additionalfactor['loginoption'] = get_string('locked', 'tool_mfa', $additionalfactor['loginoption']);
679
                    $additionalfactor['disable'] = true;
680
                    $disabledfactors[] = $additionalfactor;
681
                } else {
682
                    $additionalfactors[] = $additionalfactor;
683
                }
684
                $displaycount++;
685
            }
686
        }
687
 
688
        // We merge the additional factors placing the disabled ones last.
689
        $alladitionalfactors = array_merge($additionalfactors, $disabledfactors);
690
        $hasadditionalfactors = $displaycount > 0;
691
        $authurl = new \moodle_url('/admin/tool/mfa/auth.php');
692
 
693
        // Set the form to better display vertically.
694
        $form->set_display_vertical();
695
 
696
        // Check if we need to display a remaining attempts message.
697
        $remattempts = $factor->get_remaining_attempts();
698
        $verificationerror = $form->get_element_error('verificationcode');
699
        if ($remattempts < get_config('tool_mfa', 'lockout') && !empty($verificationerror)) {
700
            // Update the validation error for the code form field to include the remaining attempts.
701
            $remattemptsstr = get_string('lockoutnotification', 'tool_mfa', $factor->get_remaining_attempts());
702
            $updatederror = $verificationerror . '&nbsp;' . $remattemptsstr;
703
            $form->set_element_error('verificationcode', $updatederror);
704
        }
705
 
706
        // If all attempts for this factor have been used, disable the form.
707
        // This forces the user to choose another factor or cancel their login.
708
        if ($remattempts <= 0) {
709
            $disablefactor = true;
710
            $form->freeze('verificationcode');
711
 
712
            // Handle the trust factor if present.
713
            if ($form->element_exists('factor_token_trust')) {
714
                $form->freeze('factor_token_trust');
715
            }
716
        }
717
 
718
        $context = [
719
                'logintitle' => get_string('logintitle', 'factor_'.$factor->name),
720
                'logindesc' => $factor->get_login_desc(),
721
                'factoricon' => $factor->get_icon(),
722
                'form' => $form->render(),
723
                'hasadditionalfactors' => $hasadditionalfactors,
724
                'additionalfactors' => $alladitionalfactors,
725
                'authurl' => $authurl->out(),
726
                'sesskey' => sesskey(),
727
                'supportlink' => $this->get_support_link(),
728
                'disablefactor' => $disablefactor
729
        ];
730
        return $this->render_from_template('tool_mfa/verification_form', $context);
731
    }
732
}