<?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;
}
}