<?php

/**
 * @package Dotclear
 * @subpackage Backend
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Process\Backend;

use Dotclear\App;
use Dotclear\Helper\Html\Html;
use Dotclear\Helper\Network\Http;
use Dotclear\Helper\Network\Mail\Mail;
use Dotclear\Helper\Html\Form\Button;
use Dotclear\Helper\Html\Form\Checkbox;
use Dotclear\Helper\Html\Form\Details;
use Dotclear\Helper\Html\Form\Div;
use Dotclear\Helper\Html\Form\Email;
use Dotclear\Helper\Html\Form\Fieldset;
use Dotclear\Helper\Html\Form\Form;
use Dotclear\Helper\Html\Form\Hidden;
use Dotclear\Helper\Html\Form\Input;
use Dotclear\Helper\Html\Form\Label;
use Dotclear\Helper\Html\Form\Legend;
use Dotclear\Helper\Html\Form\Link;
use Dotclear\Helper\Html\Form\None;
use Dotclear\Helper\Html\Form\Note;
use Dotclear\Helper\Html\Form\Para;
use Dotclear\Helper\Html\Form\Password;
use Dotclear\Helper\Html\Form\Set;
use Dotclear\Helper\Html\Form\Submit;
use Dotclear\Helper\Html\Form\Summary;
use Dotclear\Helper\Html\Form\Text;
use Dotclear\Helper\Process\TraitProcess;
use Exception;

/**
 * @since 2.27 Before as admin/auth.php
 */
class Auth
{
    use TraitProcess;

    protected static string $dlang;

    protected static string $page_url;

    protected static string $login_data;

    protected static bool $change_pwd;

    protected static bool $recover;

    protected static ?string $akey;

    protected static string $user_id;

    protected static string $user_pwd;

    protected static string $user_key;

    protected static string $user_email;

    protected static string $msg;

    public static function init(): bool
    {
        // If we have a session cookie, go to index.php
        if (App::session()->get('sess_user_id') != '') {
            App::backend()->url()->redirect('admin.home');
        }

        // Loading locales for detected language
        // That's a tricky hack but it works ;)
        self::$dlang = Http::getAcceptLanguage();
        self::$dlang = (self::$dlang === '' ? 'en' : self::$dlang);
        if (self::$dlang !== 'en' && preg_match('/^[a-z]{2}(-[a-z]{2})?$/', self::$dlang)) {
            App::lang()->lang(self::$dlang);
            App::lang()->set(App::config()->l10nRoot() . '/' . self::$dlang . '/main');
        }

        if (App::config()->adminUrl() !== '') {
            self::$page_url = App::config()->adminUrl() . App::backend()->url()->get('admin.auth');
        } else {
            $request_uri    = isset($_SERVER['REQUEST_URI']) && is_string($request_uri = $_SERVER['REQUEST_URI']) ? $request_uri : '';
            self::$page_url = Http::getHost() . $request_uri;
        }

        self::$login_data = isset($_POST['login_data']) && is_string($login_data = $_POST['login_data']) ? $login_data : '';

        self::$change_pwd = App::auth()->allowPassChange()
            && isset($_POST['new_pwd'])
            && is_string($_POST['new_pwd'])
            && isset($_POST['new_pwd_c'])
            && is_string($_POST['new_pwd_c'])
            && isset($_POST['login_data']);

        self::$recover = App::auth()->allowPassChange() && !empty($_REQUEST['recover']);
        self::$akey    = App::auth()->allowPassChange() && !empty($_GET['akey']) && is_string($_GET['akey']) ? $_GET['akey'] : null;

        App::backend()->safe_mode = !empty($_REQUEST['safe_mode']);

        self::$user_id    = '';
        self::$user_pwd   = '';
        self::$user_key   = '';
        self::$user_email = '';

        // Auto upgrade, keep it for backward compatibility. (now on Dotclear\Process\Upgrade\Auth)
        if ((count($_GET) === 1 && $_POST === []) || App::backend()->safe_mode) {
            try {
                App::task()->addContext('UPGRADE');
                if (($changes = App::upgrade()->upgrade()->dotclearUpgrade()) !== false) {
                    self::$msg = __('Dotclear has been upgraded.') . '<!-- ' . $changes . ' -->';
                }
            } catch (Exception $e) {
                App::error()->add($e->getMessage());
            }
        }

        if (!empty($_POST['user_id'])
            && is_string($_POST['user_id'])
            && !empty($_POST['user_pwd'])
            && is_string($_POST['user_pwd'])
        ) {
            // If we have POST login informations, go throug auth process

            self::$user_id  = $_POST['user_id'];
            self::$user_pwd = $_POST['user_pwd'];
        } else {
            $cookie = isset($_COOKIE[App::backend()::COOKIE_NAME]) && is_string($cookie = $_COOKIE[App::backend()::COOKIE_NAME]) ? $cookie : '';
            if ($cookie !== '' && strlen($cookie) === 104) {
                // If we have a remember cookie, go through auth process with user_key

                $user_id = substr($cookie, 40);
                $user_id = @unpack('a32', @pack('H*', $user_id));
                if (is_array($user_id) && isset($user_id[1]) && is_string($user_id[1])) {
                    $user_id        = trim($user_id[1]);
                    self::$user_key = substr($cookie, 0, 40);
                    self::$user_pwd = '';
                } else {
                    $user_id = null;
                }

                self::$user_id = $user_id ?? '';
            }
        }

        // Enable REST service if disabled
        if (!App::rest()->serveRestRequests()) {
            App::rest()->enableRestServer(true);
        }

        // Set oAuth2 redirect URL
        App::backend()->auth()->oauth2(App::config()->adminUrl() . App::backend()->url()->get('admin.auth'));

        // 2fa verification
        App::backend()->verify_code = App::backend()->auth()->otp() !== false && isset($_POST['user_code']) && isset($_POST['login_data']);
        App::backend()->require_2fa = false;

        return self::status(true);
    }

    public static function process(): bool
    {
        // process oAuth2 client action
        if (App::backend()->auth()->oauth2() !== false) {
            App::backend()->auth()->oauth2()->requestAction((string) App::auth()->userID());
        }

        $headers = [];
        if (self::$recover
            && !empty($_POST['user_id'])
            && is_string($_POST['user_id'])
            && !empty($_POST['user_email'])
            && is_string($_POST['user_email'])
        ) {
            self::$user_id    = $_POST['user_id'];
            self::$user_email = Html::escapeHTML($_POST['user_email']);

            // Recover password

            try {
                $recover_key = App::auth()->setRecoverKey(self::$user_id, self::$user_email);

                $subject = Mail::B64Header('Dotclear ' . __('Password reset'));
                $message = __('Someone has requested to reset the password for the following site and username.') . "\n\n" . self::$page_url . "\n" . __('Username:') . ' ' . self::$user_id . "\n\n" . __('To reset your password visit the following address, otherwise just ignore this email and nothing will happen.') . "\n" . self::$page_url . '&akey=' . $recover_key;

                $headers[] = 'From: ' . App::config()->adminMailfrom();
                $headers[] = 'Content-Type: text/plain; charset=UTF-8;';

                Mail::sendMail(self::$user_email, $subject, $message, $headers);
                self::$msg = sprintf(__('The e-mail was sent successfully to %s.'), self::$user_email);

                // message saying that 2fa authentication be be removed
                if (App::backend()->auth()->otp() !== false && App::backend()->auth()->otp()->setUser(self::$user_id)->isVerified()) {
                    self::$msg .= ' ' . __('This removes two factors authentication.');
                }
            } catch (Exception $e) {
                App::error()->add($e->getMessage());
            }
        } elseif (self::$akey) {
            // Send new password

            try {
                $recover_res = App::auth()->recoverUserPassword(self::$akey);

                $subject   = mb_encode_mimeheader('Dotclear ' . __('Your new password'), 'UTF-8', 'B');
                $message   = __('Username:') . ' ' . $recover_res['user_id'] . "\n" . __('Password:') . ' ' . $recover_res['new_pass'] . "\n\n" . preg_replace('/\?(.*)$/', '', self::$page_url);
                $headers[] = 'From: ' . App::config()->adminMailfrom();
                $headers[] = 'Content-Type: text/plain; charset=UTF-8;';

                Mail::sendMail($recover_res['user_email'], $subject, $message, $headers);
                self::$msg = __('Your new password is in your mailbox.');

                // Remove 2fa authentication on password change
                if (App::backend()->auth()->otp() !== false && App::backend()->auth()->otp()->setUser($recover_res['user_id'])->isVerified()) {
                    App::backend()->auth()->otp()->delCredential();
                }
            } catch (Exception $e) {
                App::error()->add($e->getMessage());
            }
        } elseif (self::$change_pwd) {
            // Change password and retry to log

            try {
                $tmp_data = explode('/', self::$login_data);
                if (count($tmp_data) !== 3) {
                    throw new Exception();
                }

                $data = [
                    'user_id'       => base64_decode($tmp_data[0], true),
                    'cookie_admin'  => $tmp_data[1],
                    'user_remember' => $tmp_data[2] === '1',
                ];
                if ($data['user_id'] === false) {
                    throw new Exception();
                }

                // Check login informations
                $check_user = false;
                if (strlen($data['cookie_admin']) === 104) {
                    $user_id = substr($data['cookie_admin'], 40);
                    $user_id = @unpack('a32', @pack('H*', $user_id));
                    if (is_array($user_id)) {
                        $user_id        = trim($data['user_id']);
                        self::$user_key = substr($data['cookie_admin'], 0, 40);
                        $check_user     = App::auth()->checkUser($user_id, null, self::$user_key);
                    } else {
                        $user_id = trim((string) $user_id);
                    }

                    self::$user_id = $user_id;
                }

                if (!App::auth()->allowPassChange() || !$check_user) {
                    self::$change_pwd = false;

                    throw new Exception();
                }

                $new_pwd   = isset($_POST['new_pwd'])   && is_string($new_pwd = $_POST['new_pwd']) ? $new_pwd : '';
                $new_pwd_c = isset($_POST['new_pwd_c']) && is_string($new_pwd_c = $_POST['new_pwd_c']) ? $new_pwd_c : '';

                if ($new_pwd === '' || $new_pwd !== $new_pwd_c) {
                    throw new Exception(__("Passwords don't match"));
                }

                if (App::auth()->checkUser(self::$user_id, $new_pwd)) {
                    throw new Exception(__("You didn't change your password."));
                }

                $cur                  = App::auth()->openUserCursor();
                $cur->user_change_pwd = 0;
                $cur->user_pwd        = $new_pwd;
                App::users()->updUser((string) App::auth()->userID(), $cur);

                App::session()->set('sess_user_id', self::$user_id);
                App::session()->set('sess_browser_uid', Http::browserUID(App::config()->masterKey()));

                if ($data['user_remember']) {
                    setcookie(
                        App::backend()::COOKIE_NAME,
                        $data['cookie_admin'],
                        [
                            'expires' => strtotime('+15 days'),
                            'path'    => '',
                            'domain'  => '',
                            'secure'  => App::config()->adminSsl(),
                        ]
                    );
                }

                App::backend()->url()->redirect('admin.home');
            } catch (Exception $e) {
                App::error()->add($e->getMessage());
            }
        } elseif (App::backend()->verify_code) {
            //Check 2fa code
            try {
                $tmp_data = explode('/', self::$login_data);
                if (count($tmp_data) !== 4) {
                    throw new Exception();
                }

                $data = [
                    'user_id'       => base64_decode($tmp_data[0], true),
                    'cookie_admin'  => $tmp_data[1],
                    'user_remember' => $tmp_data[2] === '1',
                    'safe_mode'     => $tmp_data[3] === '1',
                ];
                if ($data['user_id'] === false) {
                    throw new Exception();
                }

                // Check login informations
                $check_user = false;
                if (strlen($data['cookie_admin']) === 104) {
                    $user_id = substr($data['cookie_admin'], 40);
                    $user_id = @unpack('a32', @pack('H*', $user_id));
                    if (is_array($user_id)) {
                        $user_id        = trim($data['user_id']);
                        self::$user_key = substr($data['cookie_admin'], 0, 40);
                        $check_user     = App::auth()->checkUser($user_id, null, self::$user_key);
                    } else {
                        $user_id = null;
                    }

                    self::$user_id = $user_id ?? '';
                }

                // Check user permissions
                if (!$check_user || App::auth()->findUserBlog() === false) {
                    throw new Exception();
                }

                $user_code = isset($_POST['user_code']) && is_string($user_code = $_POST['user_code']) ? $user_code : '';
                if (App::backend()->auth()->otp() !== false && !App::backend()->auth()->otp()->setUser(self::$user_id)->verifyCode($user_code)) {
                    throw new Exception(__('Code validation failed.'));
                }

                App::session()->set('sess_user_id', self::$user_id);
                App::session()->set('sess_browser_uid', Http::browserUID(App::config()->masterKey()));

                if ($data['safe_mode'] && App::auth()->isSuperAdmin()) {
                    App::session()->set('sess_safe_mode', true);
                }

                if ($data['user_remember']) {
                    setcookie(App::backend()::COOKIE_NAME, $data['cookie_admin'], ['expires' => strtotime('+15 days'), 'path' => '', 'domain' => '', 'secure' => App::config()->adminSsl()]);
                }

                App::backend()->url()->redirect('admin.home');
            } catch (Exception $e) {
                App::error()->add($e->getMessage());
            }
        } elseif (self::$user_id !== '' && (self::$user_pwd !== '' || self::$user_key !== '')) {
            // Try to log

            // We check the user
            $check_user = App::auth()->checkUser(
                self::$user_id,
                self::$user_pwd,
                self::$user_key,
                false
            );

            // Check user permissions
            $check_perms = $check_user && App::auth()->findUserBlog() !== false;

            $cookie_admin = Http::browserUID(App::config()->masterKey() . self::$user_id . App::auth()->cryptLegacy(self::$user_id)) . bin2hex(pack('a32', self::$user_id));

            if ($check_perms && App::auth()->mustChangePassword()) {
                // User need to change password

                self::$login_data = implode('/', [
                    base64_encode(self::$user_id),
                    $cookie_admin,
                    empty($_POST['user_remember']) ? '0' : '1',
                ]);

                if (!App::auth()->allowPassChange()) {
                    App::error()->add(__('You have to change your password before you can login.'));
                } else {
                    App::error()->add(__('In order to login, you have to change your password now.'));
                    self::$change_pwd = true;
                }
            } elseif ($check_perms && App::backend()->safe_mode && !App::auth()->isSuperAdmin()) {
                // Non super-admin user cannot use safe mode

                App::error()->add(__('Safe Mode can only be used for super administrators.'));
            } elseif ($check_perms) {
                // User may log-in

                // Check if user need 2fa
                App::backend()->require_2fa = App::backend()->auth()->otp() !== false && App::backend()->auth()->otp()->setUser(self::$user_id)->isVerified();

                if (App::backend()->require_2fa) {
                    // Required 2fa authentication. Skip normal login and go to 2fa form

                    self::$login_data = implode('/', [
                        base64_encode(self::$user_id),
                        $cookie_admin,
                        empty($_POST['user_remember']) ? '0' : '1',
                        App::backend()->safe_mode ? '1' : '0',
                    ]);
                } else {
                    // normal login

                    App::session()->set('sess_user_id', self::$user_id);
                    App::session()->set('sess_browser_uid', Http::browserUID(App::config()->masterKey()));

                    if (!empty($_POST['blog'])) {
                        App::session()->set('sess_blog_id', $_POST['blog']);
                    }

                    if (App::backend()->safe_mode && App::auth()->isSuperAdmin()) {
                        App::session()->set('sess_safe_mode', true);
                    }

                    if (!empty($_POST['user_remember'])) {
                        setcookie(App::backend()::COOKIE_NAME, $cookie_admin, ['expires' => strtotime('+15 days'), 'path' => '', 'domain' => '', 'secure' => App::config()->adminSsl()]);
                    }

                    if (isset($_REQUEST['go']) && is_string($_REQUEST['go']) && $url = self::thenGo($_REQUEST['go'])) {
                        Http::redirect($url);
                    }

                    App::backend()->url()->redirect('admin.home');
                }
            } else {
                // User cannot login

                if ($check_user) {
                    // Insufficient permissions

                    App::error()->add(__('Insufficient permissions'));
                } else {
                    // Session expired

                    App::error()->add(isset($_COOKIE[App::backend()::COOKIE_NAME]) ? __('Administration session expired') : __('Wrong username or password'));
                }

                if (isset($_COOKIE[App::backend()::COOKIE_NAME])) {
                    unset($_COOKIE[App::backend()::COOKIE_NAME]);
                    setcookie(App::backend()::COOKIE_NAME, '', ['expires' => -600, 'path' => '', 'domain' => '', 'secure' => App::config()->adminSsl()]);
                }
            }
        }

        if (isset($_GET['user']) && is_string($_GET['user'])) {
            self::$user_id = $_GET['user'];
        }

        return true;
    }

    public static function render(): void
    {
        // nullsafe before header sent
        if (!App::task()->checkContext('BACKEND')) {
            throw new Exception('Application is not in administrative context.', 500);
        }

        header('Content-Type: text/html; charset=UTF-8');
        header('X-Frame-Options: SAMEORIGIN');  // Prevents Clickjacking as far as possible

        $vendor = Html::escapeHTML(App::config()->vendorName());

        echo
        '<!DOCTYPE html>' . "\n" .
        '<html lang="' . self::$dlang . '">' . "\n";

        // Head part

        $buffer = '<head>' . "\n" .
            '<meta charset="UTF-8">' . "\n" .
            '<meta http-equiv="Content-Script-Type" content="text/javascript">' . "\n" .
            '<meta http-equiv="Content-Style-Type" content="text/css">' . "\n" .
            '<meta http-equiv="Content-Language" content="' . self::$dlang . '">' . "\n" .
            '<meta name="ROBOTS" content="NOARCHIVE,NOINDEX,NOFOLLOW">' . "\n" .
            '<meta name="GOOGLEBOT" content="NOSNIPPET">' . "\n" .
            '<meta name="viewport" content="width=device-width, initial-scale=1.0">' . "\n" .
            '<title>' . $vendor . '</title>' . "\n" .
            '<link rel="icon" type="image/png" href="images/favicon-logout.png">' . "\n" .
            '<link rel="icon" type="image/svg+xml" href="images/favicon-logout.svg" />' . "\n" .
            '<link rel="stylesheet" href="style/default.css" type="text/css" media="screen">';

        echo
        $buffer . App::backend()->page()->jsCommon();

        # --BEHAVIOR-- loginPageHTMLHead --
        App::behavior()->callBehavior('loginPageHTMLHead');

        echo
        App::backend()->page()->jsJson('pwstrength', [
            'min' => sprintf(__('Password strength: %s'), __('weak')),
            'avg' => sprintf(__('Password strength: %s'), __('medium')),
            'max' => sprintf(__('Password strength: %s'), __('strong')),
        ]) .
        App::backend()->page()->jsLoad('js/pwstrength.js') .
        App::backend()->page()->jsLoad('js/_auth.js') .
        '</head>';

        // Body part

        $action = App::backend()->url()->get('admin.auth');
        $banner = Html::escapeHTML(App::config()->vendorName());

        echo
        '<body id="dotclear-admin" class="auth">' . "\n";

        // Authentication form

        $parts = [
            (new Text('h1', $banner))->role('banner'),
        ];

        if (App::error()->flag()) {
            $errors = [];
            foreach (App::error()->dump() as $msg) {
                $errors[] = (new Text(null, $msg));
            }

            $parts[] = (new Div())
                ->role('alert')
                ->class(self::$change_pwd ? 'info' : 'error')
                ->separator('<br>') // If more than one error, add a br between each
                ->items($errors);
        }

        if (isset(self::$msg)) {
            $parts[] = (new Para())
                ->role('alert')
                ->class('success')
                ->items([
                    (new Text(null, self::$msg)),
                ]);
        }

        if (self::$akey) {
            // Recovery key has been sent
            $parts[] = (new Para())
                ->items([
                    (new Link())
                        ->href(App::backend()->url()->get('admin.auth'))
                        ->text(__('Back to login screen')),
                ]);
        } elseif (self::$recover) {
            // User request a new password
            $parts[] = (new Set())
                ->items([
                    (new Fieldset())
                        ->role('main')
                        ->legend((new Legend(__('Request a new password'))))
                        ->fields([
                            (new Para())
                                ->items([
                                    (new Input('user_id'))
                                        ->label((new Label(__('Username:'), Label::IL_TF)))
                                        ->size(20)
                                        ->maxlength(32)
                                        ->default(Html::escapeHTML(self::$user_id))
                                        ->translate(false)
                                        ->autocomplete('username'),
                                ]),
                            (new Para())
                                ->items([
                                    (new Email('user_email'))
                                        ->label((new Label(__('Email:'), Label::IL_TF)))
                                        ->size(20)
                                        ->maxlength(255)
                                        ->default(Html::escapeHTML(self::$user_email))
                                        ->translate(false)
                                        ->autocomplete('email'),
                                ]),
                            (new Para())
                                ->items([
                                    (new Submit('recover_submit', __('recover'))),
                                    (new Hidden('recover', '1')),
                                ]),
                        ]),
                    (new Details('issue'))
                        ->open(true)
                        ->summary((new Summary(__('Other option'))))
                        ->items([
                            (new Para())
                                ->items([
                                    (new Link())
                                        ->href(App::backend()->url()->get('admin.auth'))
                                        ->text(__('Back to login screen')),
                                ]),
                        ]),
                ]);
        } elseif (self::$change_pwd) {
            // User need to change password
            $parts[] = (new Fieldset())
                ->legend((new Legend(__('Change your password'))))
                ->fields([
                    (new Para())
                        ->items([
                            (new Password('new_pwd'))
                                ->label((new Label(__('New password:'), Label::IL_TF)))
                                ->size(20)
                                ->maxlength(255)
                                ->autocomplete('new-password')
                                ->translate(false)
                                ->class('pw-strength'),
                        ]),
                    (new Para())
                        ->items([
                            (new Password('new_pwd_c'))
                                ->label((new Label(__('Confirm password:'), Label::IL_TF)))
                                ->size(20)
                                ->maxlength(255)
                                ->translate(false)
                                ->autocomplete('new-password'),
                        ]),
                    (new Para())
                        ->items([
                            (new Submit('change_submit', __('change'))),
                            (new Hidden('login_data', self::$login_data)),
                        ]),
                ]);
        } elseif (App::backend()->auth()->otp() !== false && App::backend()->require_2fa && self::$user_id !== '') {
            // 2FA verification
            $parts[] = (new Set())
                ->items([
                    (new Fieldset())
                        ->role('main')
                        ->legend((new Legend(__('Two factors authentication'))))
                        ->fields([
                            (new Para())
                                ->items([
                                    (new Input('user_code'))
                                        ->label((new Label(__('Enter code:'), Label::IL_TF)))
                                        ->size(20)
                                        ->maxlength(App::backend()->auth()->otp()->getDigits())
                                        ->default('')
                                        ->translate(false)
                                        ->autocomplete('one-time-code')
                                        ->autofocus(true),
                                ]),
                            (new Para())
                                ->items([
                                    (new Submit('verify_sbumit', __('Verify'))),
                                    (new Hidden('login_data', self::$login_data)),
                                ]),
                            (new Para())
                                ->items([
                                    (new Link())
                                        ->href(App::backend()->url()->get('admin.auth'))
                                        ->text(__('Back to login screen')),
                                ]),
                        ]),
                ]);
        } else {
            // Authentication
            $user_defined_auth_form = null;
            if (is_callable([App::auth(), 'authForm'], false, $user_defined_auth_form)) {
                // User-defined authentication form

                $output = is_string($output = $user_defined_auth_form(self::$user_id)) ? $output : '';

                /*
                 * @todo Is it mandatory to echo this part as it will be echoed below? I think not.
                 */
                // echo $output;

                $parts[] = (new Text(null, $output));
            } else {
                // Standard authentication form

                $fields = [];
                $legend = null;

                if (App::backend()->safe_mode) {
                    $legend   = (new Legend(__('Safe mode login')));
                    $fields[] = (new Set())
                        ->items([
                            (new Note())->class('form-note')->text(__('This mode allows you to login without activating any of your plugins. This may be useful to solve compatibility problems')),
                            (new Note())->class('form-note')->text(__('Update, disable or delete any plugin suspected to cause trouble, then log out and log back in normally.')),
                        ]);
                } elseif (isset($_REQUEST['go']) && is_string($_REQUEST['go'])) {
                    $fields[] = (new Hidden('go', Html::escapeHTML($_REQUEST['go'])));
                }

                $fields[] = (new Set())
                    ->items([
                        (new Para())
                            ->items([
                                (new Input('user_id'))
                                    ->label((new Label(__('Username:'), Label::IL_TF)))
                                    ->size(20)
                                    ->maxlength(32)
                                    ->default(Html::escapeHTML(self::$user_id))
                                    ->translate(false)
                                    ->autocomplete('username'),
                            ]),
                        (new Para())
                            ->items([
                                (new Password('user_pwd'))
                                    ->label((new Label(__('Password:'), Label::IL_TF)))
                                    ->size(20)
                                    ->maxlength(255)
                                    ->translate(false)
                                    ->autocomplete('current-password'),
                            ]),
                        (new Para())
                            ->items([
                                (new Checkbox('user_remember'))
                                    ->value(1)
                                    ->label((new Label(__('Remember my ID on this device'), Label::IL_FT))),
                            ]),
                        (new Para())
                            ->items([
                                (new Submit('login', __('log in')))
                                    ->class('login'),
                            ]),
                        App::backend()->auth()->webauthn() === false ? new None() : (new Para('webauthn_action'))
                            ->class('hidden-if-no-js')
                            ->items([
                                (new Button(['webauthn_button'], __('Sign in with a passkey')))
                                    ->class('wide-button'),
                            ]),
                    ]);

                if (!empty($_REQUEST['blog']) && is_string($_REQUEST['blog'])) {
                    $fields[] = (new Hidden('blog', Html::escapeHTML($_REQUEST['blog'])));
                }

                if (App::backend()->safe_mode) {
                    $fields[] = (new Hidden('safe_mode', '1'));
                }

                $fieldset = (new Fieldset())
                    ->role('main')
                    ->fields($fields);
                if ($legend instanceof Legend) {
                    $fieldset->legend($legend);
                }

                $parts[] = $fieldset;

                // Oauth2 client buttons
                if (App::backend()->auth()->oauth2() !== false) {
                    $oauth2_items = [];
                    foreach (App::backend()->auth()->oauth2()->services()->getProviders() as $oauth2_service) {
                        $oauth2_id = is_string($oauth2_id = $oauth2_service::getId()) ? $oauth2_id : '';
                        if (App::backend()->auth()->oauth2()->services()->hasDisabledProvider($oauth2_id)) {
                            continue;
                        }

                        if (!App::backend()->auth()->oauth2()->store()->hasConsumer($oauth2_id)) {
                            continue;
                        }

                        $link = App::backend()->auth()->oauth2()->getActionButton(
                            '',
                            $oauth2_id,
                            App::config()->adminUrl() . App::backend()->url()->get('admin.auth')
                        );

                        if (!is_null($link)) {
                            $oauth2_items[] = (new Para())
                                ->class('wide-button')
                                ->items([$link]);
                        }
                    }

                    if ($oauth2_items !== []) {
                        $parts[] = (new Fieldset())
                            ->legend(new Legend(__('Third party applications connections')))
                            ->items($oauth2_items);
                    }
                }

                // Cookie warning
                $parts[] = (new Para('cookie_help'))
                    ->class('error')
                    ->items([
                        (new Text(null, __('You must accept cookies in order to use the private area.'))),
                    ]);

                // Authentification helpers
                if (App::backend()->safe_mode) {
                    $parts[] = (new Details('issue'))
                        ->summary((new Summary(__('Other option'))))
                        ->items([
                            (new Para())
                                ->items([
                                    (new Link('normal_mode_link'))
                                        ->href(App::backend()->url()->get('admin.auth'))
                                        ->text(__('Get back to normal authentication')),
                                ]),
                        ]);
                } else {
                    $parts[] = (new Details('issue'))
                        ->summary((new Summary(__('Connection issue?'))))
                            ->items([
                                App::auth()->allowPassChange() ?
                                (new Para())
                                    ->items([
                                        (new Link('forgot_pwd'))
                                            ->href(App::backend()->url()->get('admin.auth', ['recover' => 1]))
                                            ->text(__('I forgot my password')),
                                    ]) :
                                (new None()),
                                (new Para())
                                    ->items([
                                        (new Link('safe_mode_link'))
                                            ->href(App::backend()->url()->get('admin.auth', ['safe_mode' => 1]))
                                            ->text(__('I want to log in in safe mode')),
                                    ]),
                            ]);
                }
            }
        }

        echo (new Form('login-screen'))
            ->action($action)
            ->method('post')
            ->fields($parts)
        ->render();

        echo self::html_end();
    }

    private static function thenGo(string $go): string
    {
        // Go requested URL (in query params)
        $url            = App::backend()->url()->get('admin.home');
        $url_components = parse_url($url);
        if ($url_components !== false) {
            // Keep only URL part before query (if any)
            $url = substr($url, 0, strlen($url) - strlen($url_components['query'] ?? ''));

            // Decode requested go params
            $query = urldecode($go);

            // Basic check of params begining with process=…[&…]
            if (preg_match('/^process=([A-Za-z]+)(&)?/', $query)) {
                // Return redirect URL
                return $url . $query;
            }
        }

        return '';
    }

    private static function html_end(): string
    {
        // Tricky code to avoid xgettext bug on indented end heredoc identifier (see https://savannah.gnu.org/bugs/?62158)
        // Warning: don't use <<< if there is some __() l10n calls after as xgettext will not find them
        return <<<HTML_END
            </body>
            </html>
            HTML_END;
    }
}
