<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\controllers;

use seekquarry\yioop as B;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\library\mail\MailSiteFactory;
use seekquarry\yioop\library\mail\MimeMessage;
use seekquarry\yioop\library\mail\UnsubscribeToken;
use seekquarry\yioop\library\UrlParser;
use seekquarry\yioop\models\LocaleModel;

/**
 * Controller used to handle account registration and retrieval for
 * the Yioop website. Also handles data for suggest a url
 *
 * @author Mallika Perepa (Creator), Chris Pollett (extensive rewrite)
 */
class RegisterController extends Controller implements CrawlConstants
{
    /**
     * Holds a list of the allowed activities. These encompass various
     * stages of the account creation and account recovery processes
     * @var array
     */
    public $activities = ["createAccount", "emailVerification",
        "processAccountData", "processRecoverData", "recoverPassword",
        "recoverComplete", "resendComplete", "resendRegistration",
        "signinCode", "processSigninData", "signinCodeComplete",
        "suggestUrl"];
    /**
     * Non-recovery question fields needed to register a Yioop account.
     * @var array
     */
    public $register_fields = ["first", "last", "user", "email", "password",
        "repassword"];
    /**
     * Forbidden username
     * @var array
     */
    public $forbidden_usernames = "admin moderator root bot";
    /**
     * The two register-form field names a member fills in when the site
     * uses question-based recovery: the personal question they choose and
     * the answer to it.
     * @var array
     */
    public $recovery_fields = ["recovery_question", "recovery_answer"];
    /**
     * Mixed into the signed text of an account-activation token so the
     * site's secret key produces a signature unique to this purpose. A
     * token minted for activation can therefore never be mistaken for,
     * or reused as, any other value the site signs with the same key.
     */
    const ACTIVATION_SIGN_PREFIX = "register-activate:";
    /**
     * Mixed into the signed text of an emailed sign-in link so its
     * signature is unique to signing in. This keeps a sign-in link from
     * being mistaken for, or reused as, an activation link or any other
     * value the site signs with the same secret key.
     */
    const SIGNIN_SIGN_PREFIX = "register-signin:";
    /**
     * Besides invoking the base controller, narrows the available
     * activities when the site offers no account recovery at all.
     *
     * @param seekquarry\atto\WebSite $web_site is the web server
     *      when Yioop runs in CLI mode, it acts as request router in non-CLI
     *      mode. In CLI, mode it is useful for caching files in RAM as they
     *      are read
     */
    public function __construct($web_site = null)
    {
        if (C\p('RECOVERY_MODE') == C\NO_RECOVERY) {
            $this->activities = ["createAccount", "emailVerification",
                "processAccountData", "suggestUrl"];
        }
        parent::__construct($web_site);
    }
    /**
     * Main entry method for this controller. Determine which account
     * creation/recovery activity needs to be performed. Calls the
     * appropriate method, then sends the return $data to a view
     * determined by that activity. $this->displayView then renders that
     * view
     */
    public function processRequest()
    {
        $visitor_model = $this->model("visitor");
        if (isset($_SESSION['USER_ID'])) {
            if ($this->getCSRFTime(C\p('CSRF_TOKEN')) == 0 &&
                $_SERVER['REQUEST_METHOD'] == "GET") {
                $_REQUEST[C\p('CSRF_TOKEN')] = $this->generateCSRFToken(
                    $_SESSION['USER_ID']);
                $this->redirectLocation(C\SHORT_BASE_URL . "?" .
                    http_build_query($_REQUEST));
                exit();
            }
            $user = $_SESSION['USER_ID'];
        } else {
            $user = L\remoteAddress();
        }
        $visitor_check_names = ['captcha_time_out','suggest_day_exceeded'];
        foreach ($visitor_check_names as $name) {
            $visitor = $visitor_model->getVisitor(L\remoteAddress(), $name);
            if (isset($visitor['END_TIME']) && $visitor['END_TIME'] > time()) {
                $_SESSION['value'] = date('Y-m-d H:i:s', $visitor['END_TIME']);
                $url = B\wikiUrl($visitor['PAGE_NAME']);
                $this->web_site->header("Location:" . $url);
                \seekquarry\atto\webExit();
            }
        }
        $data = [];
        $data['REFRESH'] = "register";
        $data['SCRIPT'] = "";
        $activity = isset($_REQUEST['a']) ?
            $this->clean($_REQUEST['a'], 'string') : 'createAccount';
        $token_okay = $this->checkCSRFToken(C\p('CSRF_TOKEN'), $user);
        if (!in_array($activity, $this->activities) || (!$token_okay
            && in_array($activity, ["processAccountData",
            "processRecoverData", "processSigninData"]) )) {
            $activity = 'createAccount';
        }
        $data["check_user"] = true;
        $data["check_fields"] = $this->register_fields;
        if (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY) {
            foreach ($this->recovery_fields as $recovery_field) {
                $data["check_fields"][] = $recovery_field;
            }
        }
        $this->preactivityPrerequisiteCheck($activity,
            'processAccountData', 'createAccount', $data);
        unset($data["check_user"]);
        $data["check_fields"] = ["user"];
        $this->preactivityPrerequisiteCheck($activity,
            'processRecoverData', 'recoverPassword', $data);
        $this->preactivityPrerequisiteCheck($activity,
            'processSigninData', 'signinCode', $data);
        $this->preactivityPrerequisiteCheck($activity,
            'resendComplete', 'resendRegistration', $data);
        unset($data["check_fields"]);
        $new_data = $this->call($activity);
        $data = array_merge($new_data, $data);
        if (isset($new_data['REFRESH'])) {
            $data['REFRESH'] = $new_data['REFRESH'];
        }
        if (isset($new_data['SCRIPT']) && $new_data['SCRIPT'] != "") {
            $data['SCRIPT'] .= $new_data['SCRIPT'];
        }
        $data[C\p('CSRF_TOKEN')] = $this->generateCSRFToken($user);
        $view = $data['REFRESH'];
        if (!isset($_SESSION['REMOTE_ADDR'])) {
            if (empty($_REQUEST['a']) || ($_REQUEST['a'] != 'createAccount' &&
                !($_REQUEST['a'] == 'suggestUrl' && !isset($_REQUEST['arg'])))){
                $view = "signin";
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_need_cookies')."</h1>');";
            }
            $visitor_model->updateVisitor(
                L\remoteAddress(), "captcha_time_out");
        }
        if (!isset($data['INCLUDE_SCRIPTS'])) {
            $data['INCLUDE_SCRIPTS']= [];
        }
        $data['INCLUDE_SCRIPTS'] = array_unique(
            array_merge($data['INCLUDE_SCRIPTS'],
            ["sha1", "hash_captcha"]));
        //used to ensure that we have sessions active
        $_SESSION['REMOTE_ADDR'] = L\remoteAddress();
        /*used to manage locales
        *
        * localization of the warning messages
        */
        $tl = ["register_validator_js_retype_password_matched" =>
            tl('register_validator_js_retype_password_matched'),
            "register_validator_js_retype_password_not_matched" =>
            tl('register_validator_js_retype_password_not_matched'),
            ];
        if (!empty($data['INCLUDE_SCRIPTS']) &&
            in_array('register_validator', $data['INCLUDE_SCRIPTS'])) {
            $data['SCRIPT'] .= 'tl = '.json_encode($tl).';';
        }
        $this->displayView($view, $data);
    }
    /**
     * Sets up the form variables need to present the initial account creation
     * form. If this form is submitted with missing fields, this method
     * would also be called to set up an appropriate MISSING field
     *
     * @return array $data field correspond to values needed for account
     *     creation form
     */
    public function createAccount()
    {
        $data = $this->setupQuestionViewData();
        return $data;
    }
    /**
     * Used to process account data from completely filled in create account
     * forms. Depending on the registration type: no_activation,
     * email registration, or admin activation, either the account is
     * immediately activated or it is created in an active state and an email
     * to the person who could activate it is sent.
     *
     * @return array $data will contain a SCRIPT field with the
     *     Javascript doMessage call saying whether this step was successful
     *     or not
     */
    public function processAccountData()
    {
        $data = [];
        $this->getCleanFields($data);
        $data['SCRIPT'] = "";
        $user_model = $this->model("user");
        $group_model = $this->model("group");
        if (!empty($data['EMAIL']) &&
            MailSiteFactory::isForbiddenAccountEmail($data['EMAIL'])) {
            $data['REFRESH'] = "register";
            $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >" .
                tl('register_controller_email_system_address') . "</h1>');";
            return $data;
        }
        switch (C\p('REGISTRATION_TYPE')) {
            case 'no_activation':
                $data['REFRESH'] = "signin";
                $user_model->addUser($data['USER'], $data['PASSWORD'],
                    $data['FIRST'], $data['LAST'], $data['EMAIL']);
                $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_account_created')."</h1>')";
                break;
            case 'email_registration':
                $data['REFRESH'] = "signin";
                $user_model->addUser($data['USER'], $data['PASSWORD'],
                    $data['FIRST'], $data['LAST'], $data['EMAIL'],
                    C\INACTIVE_STATUS);
                $user = $user_model->getUser($data['USER']);
                $this->sendActivationMail($user, $data);
                break;
            case 'admin_activation':
                $data['REFRESH'] = "signin";
                $user_model->addUser($data['USER'], $data['PASSWORD'],
                    $data['FIRST'], $data['LAST'], $data['EMAIL'],
                    C\INACTIVE_STATUS);
                $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_account_request_made')."</h1>');";
                $smtp_client = MailSiteFactory::outboundSmtpClient();
                $subject = tl('register_controller_admin_activation_request');
                $message = tl('register_controller_admin_activation_message',
                    $first_name, $last_name, $user_name);
                $smtp_client->send($subject, C\p('MAIL_SENDER'),
                    C\p('MAIL_SENDER'),
                    $message);
                break;
        }
        $user = $user_model->getUser($data['USER']);
        if (!empty($user['USER_ID'])) {
            $user_model->setUserSession($user['USER_ID'], $_SESSION);
            if (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY) {
                $question = mb_substr($_REQUEST['recovery_question'], 0,
                    C\RECOVERY_QUESTION_LEN);
                $user_model->setRecoveryQuestion($user['USER_ID'],
                    $question, $_REQUEST['recovery_answer']);
            }
            $personal_group_name = C\PERSONAL_GROUP_PREFIX .
                $user['USER_ID'];
            if ($group_model->getGroupId($personal_group_name) < 0) {
                $group_model->addGroup($personal_group_name,
                    $user['USER_ID'], C\INVITE_ONLY_JOIN, C\ACTIVE_STATUS,
                    C\NON_VOTING_GROUP, C\FOREVER, 0);
            }
        }
        return $data;
    }
    /**
     * Builds the opaque token that names a new member in their
     * account-activation link. The token holds the member's numeric id
     * and an expiry time, followed by a signature made with this site's
     * secret key, so the email address never appears in the link, the
     * link stops working once it is older than
     * C\ACTIVATION_LINK_TIMEOUT, and a link naming a different member or
     * a later expiry cannot be forged: only this site can produce a
     * signature that will later verify.
     *
     * @param int $user_id the id of the member the link activates
     * @return string the id, the expiry time, and their signature joined
     *      by dots, safe to drop straight into a URL
     */
    private function makeActivationToken($user_id)
    {
        $expires = time() + C\ACTIVATION_LINK_TIMEOUT;
        $payload = strval(intval($user_id)) . "." . strval($expires);
        return $payload . "." . L\crawlAuthHash(
            self::ACTIVATION_SIGN_PREFIX . $payload);
    }
    /**
     * Reads an account-activation token and, when its signature checks
     * out and it has not expired, returns the member id it names.
     * Returns false for anything that does not parse, whose signature
     * does not match, or whose expiry time has passed, so a tampered,
     * made-up, or stale link is simply rejected.
     *
     * @param string $token the token taken from the activation link
     * @return int|bool the member's id when the token is valid and still
     *      current, or false when it is not
     */
    private function parseActivationToken($token)
    {
        $parts = explode(".", (string)$token);
        if (count($parts) !== 3) {
            return false;
        }
        list($user_id, $expires, $signature) = $parts;
        if (!ctype_digit($user_id) || !ctype_digit($expires)) {
            return false;
        }
        $payload = $user_id . "." . $expires;
        if (!hash_equals(L\crawlAuthHash(
            self::ACTIVATION_SIGN_PREFIX . $payload), $signature)) {
            return false;
        }
        if (intval($expires) < time()) {
            return false;
        }
        return intval($user_id);
    }
    /**
     * Sends the account-activation email: a greeting, the link the new
     * member follows to verify their address, and a way to stop all of
     * this site's mail to that address (a visible unsubscribe link at the
     * foot of the message and the matching List-Unsubscribe headers for
     * the mail program). A suppressed address never reaches this point
     * because registration with it is refused, so offering the
     * whole-site unsubscribe here cannot lock anyone out.
     *
     * @param array $user associative array of user
     * @param array $data field to be sent to view so can set sent mail
     *  interface message
     */
    function sendActivationMail($user, &$data)
    {
        $smtp_client = MailSiteFactory::outboundSmtpClient();
        $base_url = C\baseUrl();
        $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
            tl('register_controller_registration_email_sent').
            "</h1>');";
        $subject = tl('register_controller_email_subject');
        $activation_url = $base_url .
            "?c=register&a=emailVerification&token=".
            $this->makeActivationToken($user['USER_ID']).
            "&hash=" . urlencode(L\crawlCrypt($user['HASH']));
        $unsubscribe_token = UnsubscribeToken::makeEmail($user['EMAIL']);
        $unsubscribe_url = UnsubscribeToken::unsubscribeUrl(
            $unsubscribe_token, $base_url);
        /* Plain-text version, kept so text-only mail apps and the spam
           filters that expect a text part still get the whole message. */
        $text_message = tl('register_controller_admin_email_salutation',
            $user['FIRST_NAME'], $user['LAST_NAME']) . "\n" .
            tl('register_controller_email_body') . "\n" .
            $activation_url . "\n\n" .
            tl('register_controller_unsubscribe_all') . "\n" .
            $unsubscribe_url;
        /* Light HTML version: the same content with the two links made
           clickable. Names and URLs are escaped so nothing the member
           typed can inject markup into the message. */
        $first_name = htmlspecialchars($user['FIRST_NAME']);
        $last_name = htmlspecialchars($user['LAST_NAME']);
        $activation_link = htmlspecialchars($activation_url);
        $unsubscribe_link = htmlspecialchars($unsubscribe_url);
        $html_message = "<p>" .
            tl('register_controller_admin_email_salutation',
            $first_name, $last_name) . "</p>\n" .
            "<p>" . tl('register_controller_email_body') . "</p>\n" .
            "<p><a href=\"" . $activation_link . "\">" .
            $activation_link . "</a></p>\n" .
            "<p>" . tl('register_controller_unsubscribe_all') . "<br>\n" .
            "<a href=\"" . $unsubscribe_link . "\">" .
            $unsubscribe_link . "</a></p>\n";
        list($content_type, $message) =
            MimeMessage::alternativeBody($text_message, $html_message);
        $extra_headers =
            UnsubscribeToken::listUnsubscribeHeadersForEmail(
            $user['EMAIL'], $base_url,
            MailSiteFactory::unsubscribeMailtoAddress(C\p('MAIL_SENDER')));
        $extra_headers["Reply-To"] =
            MailSiteFactory::botAddress(C\MAIL_REPLY_TO);
        $extra_headers["Content-Type"] = $content_type;
        $smtp_client->send($subject, C\p('MAIL_SENDER'), $user['EMAIL'],
            $message,
            $extra_headers);
    }
    /**
     * Draws the first screen of the "email me a sign-in code" flow: a
     * small form, shown on the sign-in page, where a member types their
     * username and solves the proof-of-work check so we will mail them a
     * one-time code. Sets up that check the same way the recover-password
     * form does.
     *
     * @return array $data fields the sign-in view uses to draw the
     *      sign-in code request form
     */
    public function signinCode()
    {
        $data = [];
        $data['REFRESH'] = "signin";
        $data['SIGNIN_CODE'] = "request";
        $data['USER'] = "";
        $time = time();
        $_SESSION["request_time"] = $time;
        $_SESSION["level"] = self::HASH_CAPTCHA_LEVEL;
        $_SESSION["random_string"] = md5($time . C\p('AUTH_KEY'));
        $data['INCLUDE_SCRIPTS'] = ["sha1", "hash_captcha"];
        return $data;
    }
    /**
     * Handles the sign-in code request form once the fields and
     * proof-of-work check have passed. When the named account exists and
     * is active, it mints a fresh one-time code, stores only the code's
     * hash, and emails the code (and a one-click button) to the address
     * on file. Whatever the lookup found, the visitor sees the same
     * "if that account exists, a code was sent" message and the
     * code-entry form, so the page never reveals which usernames are
     * real. Per-IP and per-email throttling keeps the mailbox from being
     * hammered.
     *
     * @return array $data with a SCRIPT field carrying the generic
     *      message and the fields needed to draw the code-entry form
     */
    public function processSigninData()
    {
        $data = [];
        $this->getCleanFields($data);
        $data['REFRESH'] = "signin";
        $data['SIGNIN_CODE'] = "verify";
        $data['SCRIPT'] = "";
        $user_model = $this->model("user");
        $visitor_model = $this->model("visitor");
        $signin_model = $this->model("signin");
        $user = empty($data["USER"]) ? false :
            $user_model->getUser($data["USER"]);
        $is_active_account = !empty($user['USER_ID']) &&
            !empty($user['EMAIL']) && !empty($user['FIRST_NAME']) &&
            !empty($user['LAST_NAME']) && !empty($user['STATUS']) &&
            $user['STATUS'] == C\ACTIVE_STATUS;
        if ($is_active_account) {
            $ip_address = L\remoteAddress();
            $email_key = L\crawlHash($user['EMAIL']);
            $signin_page = "signin_code_time_out";
            $now = time();
            $ip_visitor = $visitor_model->getVisitor($ip_address,
                $signin_page);
            $email_visitor = $visitor_model->getVisitor($email_key,
                $signin_page);
            $throttled = (isset($ip_visitor['END_TIME']) &&
                $ip_visitor['END_TIME'] > $now) ||
                (isset($email_visitor['END_TIME']) &&
                $email_visitor['END_TIME'] > $now);
            if (!$throttled) {
                $code = $this->generateSigninCode();
                $signin_model->setSigninCode($user['USER_ID'],
                    L\crawlCrypt($code), $now + C\SIGNIN_CODE_TIMEOUT);
                $this->sendSigninCodeMail($user, $code);
                $visitor_model->updateVisitor($ip_address, $signin_page,
                    C\ONE_MINUTE, C\ONE_WEEK, 1);
                $visitor_model->updateVisitor($email_key, $signin_page,
                    C\ONE_MINUTE, C\ONE_WEEK, 1);
                $_SESSION['SIGNIN_CODE_USER_ID'] = $user['USER_ID'];
            }
        }
        $data['SCRIPT'] = "doMessage('<h1 class=\"green\" >" .
            tl('register_controller_email_code_sent') . "</h1>');";
        return $data;
    }
    /**
     * Checks a one-time sign-in code and, when it is right, signs the
     * member straight in without a password. The member to sign in is
     * named either by the signed token in the emailed button or, when
     * the person typed the code into the form, by the pending id held in
     * their session. A code only works while it has not expired and has
     * not been guessed at too many times; each wrong guess is counted
     * and the code is thrown away once the ceiling is reached. A correct
     * code is spent immediately so it can never be used twice.
     *
     * @return array $data with a SCRIPT field message when sign-in does
     *      not happen; on success it redirects into the admin area and
     *      does not return
     */
    public function signinCodeComplete()
    {
        $data = [];
        $data['REFRESH'] = "signin";
        $data['SIGNIN_CODE'] = "verify";
        $data['SCRIPT'] = "";
        $user_model = $this->model("user");
        $signin_model = $this->model("signin");
        $fail_message = "doMessage('<h1 class=\"red\" >" .
            tl('register_controller_email_code_invalid') . "</h1>');";
        $user_id = false;
        if (!empty($_REQUEST['token'])) {
            $user_id = $this->parseSigninToken(
                $this->clean($_REQUEST['token'], "string"));
        } else if (!empty($_SESSION['SIGNIN_CODE_USER_ID'])) {
            $user_id = $_SESSION['SIGNIN_CODE_USER_ID'];
        }
        $code = isset($_REQUEST['code']) ?
            strtoupper($this->clean($_REQUEST['code'], "string")) : "";
        if (empty($user_id) || $code === "") {
            $data['SCRIPT'] = $fail_message;
            return $data;
        }
        $record = $signin_model->getSigninCode($user_id);
        $now = time();
        if (empty($record) || $record['EXPIRES'] < $now ||
            $record['TRIES'] >= C\SIGNIN_CODE_MAX_TRIES) {
            $signin_model->deleteSigninCode($user_id);
            $data['SCRIPT'] = $fail_message;
            return $data;
        }
        if (!hash_equals($record['CODE_HASH'],
            L\crawlCrypt($code, $record['CODE_HASH']))) {
            $signin_model->incrementSigninCodeTries($user_id);
            if ($record['TRIES'] + 1 >= C\SIGNIN_CODE_MAX_TRIES) {
                $signin_model->deleteSigninCode($user_id);
            }
            $data['SCRIPT'] = $fail_message;
            return $data;
        }
        /* The code was right: spend it so it cannot be used again, then
           establish the signed-in session and hand off to the admin
           area, exactly as a normal password sign-in would. */
        $signin_model->deleteSigninCode($user_id);
        unset($_SESSION['SIGNIN_CODE_USER_ID']);
        $_SESSION['USER_ID'] = $user_id;
        if (!empty($_COOKIE[C\p('SESSION_NAME')])) {
            $user_model->setSessionUser($_COOKIE[C\p('SESSION_NAME')], $user_id,
                time() + C\p('AUTOLOGOUT'));
        }
        $csrf_token = $this->generateCSRFToken($user_id);
        $location = B\controllerUrl("admin", true) .
            http_build_query([C\p('CSRF_TOKEN') => $csrf_token]);
        $this->web_site->header("Location:" . $location);
        \seekquarry\atto\webExit();
    }
    /**
     * Builds the signed token that names a member inside an emailed
     * sign-in link. It joins the member id and an expiry time, then signs
     * that with the site's secret key so the button in the mail cannot
     * be pointed at a different account or used after it goes stale.
     *
     * @param int $user_id the id of the member the link signs in
     * @return string the id, the expiry time, and their signature joined
     *      by dots, safe to drop straight into a URL
     */
    private function makeSigninToken($user_id)
    {
        $expires = time() + C\SIGNIN_CODE_TIMEOUT;
        $payload = strval(intval($user_id)) . "." . strval($expires);
        return $payload . "." . L\crawlAuthHash(
            self::SIGNIN_SIGN_PREFIX . $payload);
    }
    /**
     * Reads a sign-in token from an emailed link and, when its signature
     * checks out and it has not expired, returns the member id it names.
     * Returns false for anything that does not parse, whose signature
     * does not match, or whose expiry has passed, so a tampered,
     * made-up, or stale link is simply rejected.
     *
     * @param string $token the token taken from the sign-in link
     * @return int|bool the member's id when the token is valid and still
     *      current, or false when it is not
     */
    private function parseSigninToken($token)
    {
        $parts = explode(".", (string)$token);
        if (count($parts) !== 3) {
            return false;
        }
        list($user_id, $expires, $signature) = $parts;
        if (!ctype_digit($user_id) || !ctype_digit($expires)) {
            return false;
        }
        $payload = $user_id . "." . $expires;
        if (!hash_equals(L\crawlAuthHash(
            self::SIGNIN_SIGN_PREFIX . $payload), $signature)) {
            return false;
        }
        if (intval($expires) < time()) {
            return false;
        }
        return intval($user_id);
    }
    /**
     * Makes a fresh one-time sign-in code: a short string of characters
     * the member will type or click to sign in. The characters are drawn
     * with a cryptographically strong random source from an alphabet
     * that leaves out look-alikes such as O, 0, I, 1, and L so the code
     * is easy to read off an email and type back without mistakes.
     *
     * @return string the newly generated sign-in code
     */
    private function generateSigninCode()
    {
        $alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
        $last = strlen($alphabet) - 1;
        $code = "";
        for ($i = 0; $i < C\SIGNIN_CODE_LENGTH; $i++) {
            $code .= $alphabet[random_int(0, $last)];
        }
        return $code;
    }
    /**
     * Emails a member their one-time sign-in code two ways at once: the
     * code itself, to read off and type into the form, and a button that
     * carries the same code so a single tap signs them in. Both reach the
     * same place and either one can be used. The message goes out as both
     * plain text and light HTML so every mail program can show it.
     *
     * @param array $user the member the code was issued to, supplying
     *      their name and email address
     * @param string $code the plain sign-in code to deliver
     */
    public function sendSigninCodeMail($user, $code)
    {
        $smtp_client = MailSiteFactory::outboundSmtpClient();
        $base_url = C\baseUrl();
        $subject = tl('register_controller_email_code_subject');
        $signin_url = $base_url .
            "?c=register&a=signinCodeComplete&token=" .
            $this->makeSigninToken($user['USER_ID']) .
            "&code=" . urlencode($code);
        /* Plain-text version, kept so text-only mail apps and the spam
           filters that expect a text part still get the whole message. */
        $text_message = tl('register_controller_admin_email_salutation',
            $user['FIRST_NAME'], $user['LAST_NAME']) . "\n" .
            tl('register_controller_email_code_body') . "\n\n" .
            $code . "\n\n" .
            tl('register_controller_email_code_link') . "\n" .
            $signin_url;
        /* Light HTML version: the same code shown large, then the same
           link as a button. Names are escaped so nothing the member
           typed can inject markup into the message. */
        $first_name = htmlspecialchars($user['FIRST_NAME']);
        $last_name = htmlspecialchars($user['LAST_NAME']);
        $code_text = htmlspecialchars($code);
        $signin_link = htmlspecialchars($signin_url);
        $button_style = "display:inline-block;padding:0.6em 1.2em;" .
            "background:#1a3c6e;color:#ffffff;text-decoration:none;" .
            "border-radius:4px";
        $code_style = "font-size:1.6em;font-weight:bold;" .
            "letter-spacing:0.25em";
        $html_message = "<p>" .
            tl('register_controller_admin_email_salutation',
            $first_name, $last_name) . "</p>\n" .
            "<p>" . tl('register_controller_email_code_body') . "</p>\n" .
            "<p style=\"" . $code_style . "\">" . $code_text . "</p>\n" .
            "<p>" . tl('register_controller_email_code_link') . "</p>\n" .
            "<p><a href=\"" . $signin_link . "\" style=\"" . $button_style .
            "\">" . tl('register_controller_email_code_button') .
            "</a></p>\n";
        list($content_type, $message) =
            MimeMessage::alternativeBody($text_message, $html_message);
        $extra_headers = [];
        $extra_headers["Reply-To"] =
            MailSiteFactory::botAddress(C\MAIL_REPLY_TO);
        $extra_headers["Content-Type"] = $content_type;
        $smtp_client->send($subject, C\p('MAIL_SENDER'), $user['EMAIL'],
            $message,
            $extra_headers);
    }
    /**
     * Used to verify the email sent to a user try to set up an account.
     * If the email is legit the account is activated
     *
     * @return array $data will contain a SCRIPT field with the
     *     Javascript doMessage call saying whether verification was
     *     successful or not
     */
    public function emailVerification()
    {
        $data = [];
        $data['REFRESH'] = "signin";
        $data['SCRIPT'] = "";
        $user_model = $this->model("user");
        $token = isset($_REQUEST["token"]) ?
            $this->clean($_REQUEST["token"], "string") : "";
        $provided_hash = isset($_REQUEST["hash"]) ?
            $this->clean($_REQUEST["hash"], "string") : "";
        $user_id = $this->parseActivationToken($token);
        if ($user_id === false || $provided_hash === "") {
            $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                tl('register_controller_email_verification_error')."</h1>');";
        } else {
            $user = $user_model->getUserById($user_id);
            if (empty($user['USER_ID']) || empty($user['HASH'])) {
                $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_email_verification_error').
                    "</h1>');";
            } else if ($user['STATUS'] == C\ACTIVE_STATUS) {
                $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_already_activated')."</h1>');";
            } else {
                $hash = L\crawlCrypt($user["HASH"], $provided_hash);
                if (hash_equals($hash, $provided_hash)) {
                    $user_model->updateUserStatus($user["USER_ID"],
                        C\ACTIVE_STATUS);
                    $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                        tl('register_controller_account_activated')."</h1>');";
                } else {
                    $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                        tl('register_controller_email_verification_error').
                        "</h1>');";
                    $this->model("visitor")->updateVisitor(
                        L\remoteAddress(), "captcha_time_out");
                }
            }
        }
        $_SESSION['REMOTE_ADDR'] = L\remoteAddress();
        return $data;
    }
    /**
     * Sets up the form variables need to present the initial recover account
     * form. If this form is submitted with missing fields, this method
     * would also be called to set up an appropriate MISSING field
     *
     * @return array $data field correspond to values needed for account
     *     recovery form
     */
    public function recoverPassword()
    {
        $data = $this->setupQuestionViewData();
        $data['REFRESH'] = "recover";
        return $data;
    }
    /**
     * Called once the initial recover form (username and captcha) is filled
     * in correctly. For email recovery it mails the member a one-time
     * recover link. For question recovery it returns the airgapped
     * one-screen form: the member's own question, an answer box, and the
     * new-password fields, with no email sent.
     *
     * @return array $data either a SCRIPT field announcing the email was
     *     sent, or the fields the recover view needs to draw the
     *     question-and-password screen
     */
    public function processRecoverData()
    {
        $data = [];
        $this->getCleanFields($data);
        $data['SCRIPT'] = "";
        $user_model = $this->model("user");
        $data["REFRESH"] = "signin";
        $user = $user_model->getUser($data['USER']);
        if (!$user || C\p('RECOVERY_MODE') == C\NO_RECOVERY) {
            $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                tl('register_controller_account_recover_fail')."</h1>');";
            $this->model("visitor")->updateVisitor(
                L\remoteAddress(), "captcha_time_out");
            return $data;
        }
        if (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY) {
            return $this->setupQuestionRecover($user, $data);
        }
        $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
            tl('register_controller_account_recover_email')."</h1>');";
        $smtp_client = MailSiteFactory::outboundSmtpClient();
        $base_url = C\baseUrl();
        $subject = tl('register_controller_recover_request');
        $message = tl('register_controller_admin_email_salutation',
            $user['FIRST_NAME'], $user['LAST_NAME'])."\n";
        $message .= tl('register_controller_recover_body')."\n";
        $time = time();
        $message .= $base_url .
            "?c=register&a=recoverComplete&user=" .
            $user['USER_NAME'] .
            "&hash=".urlencode(L\crawlCrypt(
                $user['HASH'] . $time . $user['USER_NAME'].C\p('AUTH_KEY'))) .
            "&time=" . $time ;
        $smtp_client->send($subject, C\p('MAIL_SENDER'), $user['EMAIL'],
            $message);
        return $data;
    }
    /**
     * Builds the form data for the airgapped, question-based recover screen:
     * the member's own recovery question shown back to them, an answer box,
     * and the new-password fields, all on one screen. A signed finish token
     * ties the eventual submit back to this captcha-cleared step. When the
     * account has no recovery question on file, a generic failure is shown
     * so the screen reveals nothing extra.
     *
     * @param array $user the USERS row of the member recovering
     * @param array $data form data being assembled for the recover view
     * @return array $data with the question, token, and password fields set
     */
    private function setupQuestionRecover($user, $data)
    {
        if (empty($user['RECOVERY_QUESTION'])) {
            $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                tl('register_controller_account_recover_fail')."</h1>');";
            $this->model("visitor")->updateVisitor(
                L\remoteAddress(), "captcha_time_out");
            return $data;
        }
        $time = time();
        $data["user"] = $user['USER_NAME'];
        $data["time"] = $time;
        $data["QUESTION"] = $this->clean($user['RECOVERY_QUESTION'],
            "string");
        $data["PASSWORD"] = "";
        $data["REPASSWORD"] = "";
        $data["RECOVER_COMPLETE"] = true;
        $data["REFRESH"] = "recover";
        $data['finish_hash'] = urlencode(L\crawlCrypt($user['HASH'] .
            $time . $user['CREATION_TIME'] . C\p('AUTH_KEY')));
        return $data;
    }
    /**
     * This activity either verifies the recover email and sets up the
     * appropriate  data for a change password form or it verifies the
     * change password form data and changes the password. If verifications
     * fail, error messages are set up
     *
     * @return array form data to be used by recover or signin views
     */
    public function recoverComplete()
    {
        $data = [];
        $data['REFRESH'] = "signin";
        $user_model = $this->model("user");
        $visitor_model = $this->model("visitor");
        $fields = ["user", "hash", "time"];
        if (isset($_REQUEST['finish_hash'])) {
            $fields = ["user", "finish_hash", "time", "password",
                "repassword"];
            if (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY) {
                $fields[] = "answer";
            }
        }
        $recover_fail = "doMessage('<h1 class=\"red\" >".
            tl('register_controller_account_recover_fail')."</h1>');";
        foreach ($fields as $field) {
            if (isset($_REQUEST[$field])) {
                $data[$field] = $this->clean($_REQUEST[$field], "string");
            } else {
                $data['SCRIPT'] = $recover_fail;
                return $data;
            }
        }
        $user = $user_model->getUser($data["user"]);
        if (empty($user["USER_ID"])) {
            $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                tl('register_controller_account_recover_fail')."</h1>');";
            return $data;
        }
        $user_session = $user_model->getUserSession($user["USER_ID"]);
        if (isset($data['finish_hash'])) {
            $finish_hash = urlencode(L\crawlCrypt($user['HASH'].$data["time"].
                $user['CREATION_TIME'] . C\p('AUTH_KEY'),
                urldecode($data['finish_hash'])));
            if ($finish_hash != $data['finish_hash'] ||
                (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY &&
                !$user_model->checkRecoveryAnswer($user,
                $_REQUEST["answer"]))) {
                $visitor_model->updateVisitor(
                    L\remoteAddress(), "captcha_time_out");
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_account_recover_fail')."</h1>');";
                return $data;
            }
            if ($data["password"] == $data["repassword"]) {
                if (!empty(L\passwordPolicyViolations($data["password"]))) {
                    $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                        tl('register_controller_password_requirements').
                        "</h1>');";
                    return $data;
                }
                if (isset($user_session['LAST_RECOVERY_TIME']) &&
                    $user_session['LAST_RECOVERY_TIME'] > $data["time"]) {
                    $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                        tl('register_controller_recovered_already')."</h1>');";
                    return $data;
                } else if (time() - $data["time"] > C\ONE_DAY) {
                    $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                        tl('register_controller_recovery_expired')."</h1>');";
                    return $data;
                } else {
                    $user["PASSWORD"] = $data["password"];
                    $user_model->updateUser($user);
                    $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                        tl('register_controller_password_changed')."</h1>');";
                    $user_session['LAST_RECOVERY_TIME'] = time();
                    $user_model->setUserSession($user["USER_ID"],
                        $user_session);
                    return $data;
                }
            } else {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_passwords_dont_match')."</h1>');";
            }
        } else {
            $hash = L\crawlCrypt(
                $user['HASH'].$data["time"].$user['USER_NAME'].C\p('AUTH_KEY'),
                $data['hash']);
            if ($hash != $data['hash']) {
                $visitor_model->updateVisitor(
                    L\remoteAddress(), "captcha_time_out");
                $data['SCRIPT'] = $recover_fail;
                return $data;
            } else if (isset($user_session['LAST_RECOVERY_TIME']) &&
                    $user_session['LAST_RECOVERY_TIME'] > $data["time"]) {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_recovered_already')."</h1>');";
                return $data;
            } else if (time() - $data["time"] > C\ONE_DAY) {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_recovery_expired')."</h1>');";
                return $data;
            }
        }
        if (C\p('RECOVERY_MODE') == C\NO_RECOVERY) {
            $data['SCRIPT'] = $recover_fail;
            return $data;
        }
        $data['PASSWORD'] = "";
        $data['REPASSWORD'] = "";
        $data["REFRESH"] = "recover";
        $data["RECOVER_COMPLETE"] = true;
        $data['finish_hash'] = urlencode(L\crawlCrypt($user['HASH'] .
            $data["time"]. $user['CREATION_TIME'] . C\p('AUTH_KEY')));
        return $data;
    }
    /**
     * Sets up the form variables need to present the resend registration
     * form. If this form is submitted with missing fields, this method
     * would also be called to set up an appropriate MISSING field
     *
     * @return array $data field correspond to values needed for account
     *     creation form
     */
    public function resendRegistration()
    {
        $data = $this->setupQuestionViewData();
        $data['REFRESH'] = "resendEmail";
        return $data;
    }
    /**
     * Re-sends the account-activation email for someone who registered
     * but never activated. To avoid revealing whether any given account
     * exists or has already been activated, every outcome shows the same
     * neutral message; the activation mail is actually re-sent only when
     * the named account exists, is still inactive, and the per-IP /
     * per-address resend rate limit has not been reached.
     *
     * @return array form data, including a SCRIPT field with the neutral
     *     doMessage shown to the visitor, used by the signin view
     */
    public function resendComplete()
    {
        $data = [];
        $data['REFRESH'] = "signin";
        $data['SCRIPT'] = "";
        $user_model = $this->model("user");
        $visitor_model = $this->model("visitor");
        $generic_message = "doMessage('<h1 class=\"red\" >".
            tl('register_controller_email_resend_generic')."</h1>');";
        if (isset($_REQUEST["user"])) {
            $data["user"] = $this->clean($_REQUEST["user"], "string");
        }
        $user = empty($data["user"]) ? false :
            $user_model->getUser($data["user"]);
        $is_inactive_account = !empty($user['USER_ID']) &&
            !empty($user['STATUS']) && !empty($user['EMAIL']) &&
            !empty($user['FIRST_NAME']) && !empty($user['LAST_NAME']) &&
            $user['STATUS'] != C\ACTIVE_STATUS;
        if ($is_inactive_account) {
            $ip_address = L\remoteAddress();
            $email_key = L\crawlHash($user['EMAIL']);
            $resend_page = "resend_time_out";
            $now = time();
            $ip_visitor = $visitor_model->getVisitor($ip_address,
                $resend_page);
            $email_visitor = $visitor_model->getVisitor($email_key,
                $resend_page);
            $throttled = (isset($ip_visitor['END_TIME']) &&
                $ip_visitor['END_TIME'] > $now) ||
                (isset($email_visitor['END_TIME']) &&
                $email_visitor['END_TIME'] > $now);
            if (!$throttled) {
                $this->sendActivationMail($user, $data);
                $visitor_model->updateVisitor($ip_address, $resend_page,
                    C\ONE_MINUTE, C\ONE_WEEK, 1);
                $visitor_model->updateVisitor($email_key, $resend_page,
                    C\ONE_MINUTE, C\ONE_WEEK, 1);
                $user_model->setUserSession($user['USER_ID'], $_SESSION);
            }
        }
        $data['SCRIPT'] = $generic_message;
        return $data;
    }
    /**
     * Used to handle data from the suggest-a-url to crawl form
     * (suggest_view.php). Basically, it saves any data submitted to
     * a file which can then be imported in manageCrawls
     *
     * @return array $data contains fields with the current value for
     *     the url (if set but not submitted) as well as for a captcha
     */
    public function suggestUrl()
    {
        $data["REFRESH"] = "suggest";
        $visitor_model = $this->model("visitor");
        $crawl_model = $this->model("crawl");
        $clear = false;
        $data['INCLUDE_SCRIPTS'] = ["sha1", "hash_captcha"];
        if (!isset($_SESSION['BUILD_TIME']) || !isset($_REQUEST['build_time'])||
            $_SESSION['BUILD_TIME'] != $_REQUEST['build_time'] ||
            $this->clean($_REQUEST['build_time'], "int") <= 0) {
            $time = time();
            $_SESSION["request_time"] = $time;
            $_SESSION["level"] = self::HASH_CAPTCHA_LEVEL;
            $_SESSION["random_string"] = md5( $time . C\p('AUTH_KEY'));
            $clear = true;
            if (isset($_REQUEST['url'])) {
                unset($_REQUEST['url']);
            }
            if (isset($_REQUEST['arg'])) {
                unset($_REQUEST['arg']);
            }
            $data['build_time'] = time();
            $_SESSION['BUILD_TIME'] = $data['build_time'];
        } else {
            $data['build_time'] = $_SESSION['BUILD_TIME'];
        }
        $data['url'] = "";
        if (isset($_REQUEST['url'])) {
            $data['url'] = $this->clean($_REQUEST['url'], "string");
        }
        $missing = [];
        $save = isset($_REQUEST['arg']) && $_REQUEST['arg'];
        $data['MISSING'] = $missing;
        $fail = false;
        if ($save && isset($_REQUEST['url'])) {
            $url = $this->clean($_REQUEST['url'], "string");
            $url_parts = @parse_url($url);
            if (!isset($url_parts['scheme'])) {
                $url = "http://".$url;
            }
            $suggest_host = UrlParser::getHost($url);
            $scheme = UrlParser::getScheme($url);
            if (strlen($suggest_host) < 12 || !$suggest_host ||
                !in_array($scheme, ["http", "https"])) {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_invalid_url')."</h1>');";
                $fail = true;
            } else  if ($missing != []) {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_error_fields')."</h1>');";
                $fail = true;
            }
            if ($fail) {
                return $data;
            }
            if (!$this->validateHashCode()) {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_failed_hashcode')."</h1>');";
                $visitor_model->updateVisitor(
                    L\remoteAddress(), "captcha_time_out");
                return $data;
            }
            /* Handle cases where captcha was okay */
            if (C\DIRECT_ADD_SUGGEST) {
                $machine_urls = $this->model("machine")->getQueueServerUrls();
                $status = $crawl_model->crawlStatus($machine_urls);
                if (empty($status['CRAWL_TIME'])) {
                    $seed_info = $crawl_model->getSeedInfo();
                    $seed_info['seed_sites']['url'][] = "#\n#" .
                        date('r')."\n#";
                    $seed_info['seed_sites']['url'][] = $url;
                    $crawl_model->setSeedInfo($seed_info);
                } else {
                    $timestamp = $status['CRAWL_TIME'];
                    $seed_info = $crawl_model->getCrawlSeedInfo(
                        $timestamp, $machine_urls);
                    $seed_info['seed_sites']['url'][] = "#\n#" .
                        date('r')."\n#";
                    $seed_info['seed_sites']['url'][] = $url;
                    $crawl_model->setCrawlSeedInfo($timestamp,
                        $seed_info, $machine_urls);
                    $crawl_model->injectUrlsCurrentCrawl(
                        $timestamp, [$url], $machine_urls);
                }
            } else if (!$crawl_model->appendSuggestSites($url)) {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                tl('register_controller_suggest_full')."</h1>');";
                return $data;
            }
            $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                tl('register_controller_url_submitted')."</h1>');";
            $visitor_model->updateVisitor(
                L\remoteAddress(), "suggest_day_exceeded",
                C\ONE_DAY, C\ONE_DAY, C\MAX_SUGGEST_URLS_ONE_DAY);
            $data['build_time'] = time();
            $_SESSION['BUILD_TIME'] = $data['build_time'];
            $data['url'] ="";
        }
        return $data;
    }
    /**
     * Sets up the captcha fields in a $data associative array so the
     * register or recover view can draw them.
     *
     * @return array $data associate array with field to help the register and
     *     recover view draw themselves
     */
    public function setupQuestionViewData()
    {
        $data = [];
        $data['INCLUDE_SCRIPTS'] = ["register_validator"];
        $fields = $this->register_fields;
        foreach ($fields as $field) {
            $data[strtoupper($field)] = "";
        }
        $data['INCLUDE_SCRIPTS'] = array_unique(
            array_merge($data['INCLUDE_SCRIPTS'],
            ["sha1", "hash_captcha"]));
        $time = time();
        $_SESSION["request_time"] = $time;
        $_SESSION["level"] = self::HASH_CAPTCHA_LEVEL;
        $_SESSION["random_string"] = md5( $time . C\p('AUTH_KEY') );
        return $data;
    }
    /**
     * Used to select which activity a controller will do. If the $activity
     * is $activity_success, then this method checks the prereqs for
     * $activity_success. If they are not met then the view $data array is
     * updated with an error message and $activity_fail is set to be the
     * next activity. If the prereq is met then the $activity is left as
     * $activity_success. If $activity was not initially equal to
     * $activity_success then this method does nothing.
     *
     * @param string &$activity current tentative activity
     * @param string $activity_success activity to test for and to test prereqs
     *     for.
     * @param string $activity_fail if prereqs not met which activity to switch
     *     to
     * @param array &$data data to help render the view this controller draws
     */
    public function preactivityPrerequisiteCheck(&$activity,
        $activity_success, $activity_fail, &$data)
    {
        $profile_model = $this->model("profile");
        $profile = $profile_model->getProfile(C\WORK_DIRECTORY);
        if ($activity == $activity_success) {
            $this->dataIntegrityCheck($data);
            if (!$data["SUCCESS"]) {
                $activity = $activity_fail;
                return;
            }
            if (!$this->validateHashCode()) {
                $data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_failed_hashcode').
                    "</h1>');";
                $this->model("visitor")->updateVisitor(
                    L\remoteAddress(), "captcha_time_out");
                $activity = $activity_fail;
            }
        }
    }
    /**
     * Checks the data submitted on a create-account or recover-account
     * form and, when something is wrong, adds a message for the view and
     * marks the attempt as failed. It flags missing or malformed fields,
     * a username that is already taken, and, when registering, an email
     * address that has been suppressed (has turned off all of this
     * site's mail) and so can no longer be used to register.
     *
     * @param array &$data carries the submitted fields; this sets
     *      SUCCESS to false and appends an error SCRIPT when a check
     *      fails
     */
    public function dataIntegrityCheck(&$data)
    {
        if (!isset($data['SCRIPT'])) {
            $data['SCRIPT'] = "";
        }
        $data['SUCCESS'] = true;
        $this->getCleanFields($data);
        if ($data['MISSING'] != []) {
            $data['SUCCESS'] = false;
            $message = "doMessage('<h1 class=\"red\" >".
                tl('register_controller_error_fields')."</h1>');";
            if ($data['MISSING'] == ["email"]) {
                $message = "doMessage('<h1 class=\"red\" >".
                    tl('register_controller_check_email')."</h1>');";
            }
            $data['SCRIPT'] .= $message;
        } else if (isset($data["check_user"]) &&
            $this->model("user")->getUserId($data['USER'])) {
            $data['SUCCESS'] = false;
            $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
            tl('register_controller_user_already_exists')."</h1>');";
        } else if (isset($data["check_user"]) && !empty($data['EMAIL']) &&
            $this->model("mailSuppression")->isSuppressed(
            $data['EMAIL'])) {
            $data['SUCCESS'] = false;
            $data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
                tl('register_controller_email_suppressed')."</h1>');";
        }
    }
    /**
     * Used to clean the inputs for form variables
     * for creating/recovering an account. It also puts
     * in blank values for missing fields into a "MISSING"
     * array
     *
     * @param array &$data an array of data to be sent to the view
     *     After this method is done it will have cleaned versions
     *     of the $_REQUEST variables from create or recover account
     *     forms as well as a "MISSING" field which is an array of
     *     those items which did not have values on the create/recover
     *     account form
     */
    public function getCleanFields(&$data)
    {
        $fields = $this->register_fields;
        if (isset($data["check_fields"])) {
            $fields = $data["check_fields"];
        }
        if (!isset($data["SCRIPT"])) {
            $data["SCRIPT"] = "";
        }
        $missing = [];
        $regex_email=
            '/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+'.
            '(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/';
        foreach ($fields as $field) {
            if (!isset($_REQUEST[$field]) || !trim($_REQUEST[$field])) {
                $error = true;
                $missing[] = $field;
                $data[strtoupper($field)] = "";
            } else if ($field == "email" &&
                !preg_match($regex_email,
                $this->clean($_REQUEST['email'], "string" ))) {
                $error = true;
                $missing[] = "email";
                $data[strtoupper($field)] = "";
            } else if ($field == "user" && stripos($this->forbidden_usernames,
                $_REQUEST[$field]) !== false && $_REQUEST['a'] ==
                'createAccount') {
                $error = true;
                $missing[] = "user";
                $data[strtoupper($field)] = "";
            } else {
                $data[strtoupper($field)] = $this->clean($_REQUEST[$field],
                    "string");
                if (!in_array($field, ['password','repassword'])) {
                    $data[strtoupper($field)] = trim($data[strtoupper($field)]);
                }
            }
        }
        if (isset($_REQUEST['password'])
            && isset($_REQUEST['repassword']) &&
            (strlen($_REQUEST['password']) > C\LONG_NAME_LEN
            || !empty(L\passwordPolicyViolations($_REQUEST['password']))
            || $this->clean($_REQUEST['password'], "string" ) !=
            $this->clean($_REQUEST['repassword'], "string" ))) {
            $error = true;
            $missing[] = "password";
            $missing[] = "repassword";
            $data["PASSWORD"] = "";
            $data["REPASSWORD"] = "";
        }
        $data['MISSING'] = $missing;
    }
     /**
     * Calculates the sha1 of a string consist of a randomString,request_time
     * send by a server and the nonce send by a client.It checks
     * whether the sha1 produces expected number of a leading zeroes
     *
     * @return bool true if the sha1 produces expected number
     * of a leading zeroes.
     */
    public function validateHashCode()
    {
        $nonce = $_REQUEST['nonce_for_string'] ?? "";
        $random_string = $_SESSION["random_string"] ?? "";
        $request_time = $_SESSION["request_time"] ?? "";
        $level = $_SESSION['level'] ?? self::HASH_CAPTCHA_LEVEL;
        $meets = $this->meetsProofOfWork($random_string, $request_time,
            $nonce, $level);
        $time = time();
        $_SESSION["request_time"] = $time;
        $_SESSION["random_string"] =  md5( $time . C\p('AUTH_KEY') );
        if ((time() - $_SESSION["request_time"] < self::HASH_TIMESTAMP_TIMEOUT)
           && $meets) {
            return true;
        }
        return false;
    }
}
