Proyectos de Subversion Moodle

Rev

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