/**
* 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 Sarika Padmashali (tweaks cpollett)
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
/*
* Localized strings handed to this script by the page that loads it. Only
* the create-account form sets these; the shared password-requirements
* code below does not use them, so other forms can load this script too.
*/
var tl = document.tl;
/*
* How long the brief top-of-screen success note stays up, in milliseconds.
*/
var success_message_time = 5000;
/*
* Returns the list of password-policy rules the given value breaks, using
* a rules object read from a password-requirements hint. The list is empty
* when the value satisfies every rule. This mirrors the server-side
* passwordPolicyViolations so the browser and the server agree on what an
* acceptable password is.
*/
function passwordPolicyViolations(value, rules)
{
var violations = [];
if (value.length < rules.min) {
violations.push("too_short");
}
if (value.length > rules.max) {
violations.push("too_long");
}
if (rules.lower && !(/[a-z]/.test(value))) {
violations.push("lowercase");
}
if (rules.upper && !(/[A-Z]/.test(value))) {
violations.push("uppercase");
}
if (rules.digit && !(/[0-9]/.test(value))) {
violations.push("digit");
}
if (rules.symbol && !(/[^A-Za-z0-9]/.test(value))) {
violations.push("symbol");
}
for (var index = 0; index < rules.forbidden.length; index++) {
if (value.indexOf(rules.forbidden.charAt(index)) >= 0) {
violations.push("forbidden");
break;
}
}
return violations;
}
/*
* Reads the rule set out of a password-requirements hint's data attributes
* into a plain object the checker above understands. The forbidden
* characters travel as space-separated character codes so no quote ever
* has to sit inside the markup.
*/
function passwordRulesFromContainer(container)
{
var codes = container.getAttribute("data-pw-forbidden");
var forbidden = "";
if (codes != "") {
var forbidden_codes = codes.split(" ");
for (var index = 0; index < forbidden_codes.length; index++) {
forbidden +=
String.fromCharCode(parseInt(forbidden_codes[index], 10));
}
}
return {
field: container.getAttribute("data-pw-field"),
min: parseInt(container.getAttribute("data-pw-min"), 10),
max: parseInt(container.getAttribute("data-pw-max"), 10),
lower: container.getAttribute("data-pw-lower") == "1",
upper: container.getAttribute("data-pw-upper") == "1",
digit: container.getAttribute("data-pw-digit") == "1",
symbol: container.getAttribute("data-pw-symbol") == "1",
forbidden: forbidden,
gate: container.getAttribute("data-pw-gate") == "1"
};
}
/*
* Turns a list of broken-rule tokens into a short phrase naming just the
* requirements still missing, taking the wording from the hint's data
* attributes so it stays in the page's language and minimal.
*/
function passwordMissingText(violations, container)
{
var attribute_for = {
too_short: "data-pw-phrase-short",
lowercase: "data-pw-phrase-lower",
uppercase: "data-pw-phrase-upper",
digit: "data-pw-phrase-digit",
symbol: "data-pw-phrase-symbol",
forbidden: "data-pw-phrase-forbidden"
};
var phrases = [];
for (var index = 0; index < violations.length; index++) {
var name = attribute_for[violations[index]];
if (name) {
var phrase = container.getAttribute(name);
if (phrase) {
phrases.push(phrase);
}
}
}
return phrases.join(", ");
}
/*
* Shows the live feedback for one password box. While a rule is still
* broken it lists, in green, just what is missing (with "Too short" for the
* length); once every rule is met it instead turns the box's own border
* green, clears that text, and flashes a brief note at the top of the page;
* an empty box shows nothing. Because this replaces the hint's sentence,
* that plain sentence is seen only when JavaScript is off. Returns whether
* the password is acceptable.
*/
function showPasswordFeedback(container)
{
var rules = passwordRulesFromContainer(container);
var field = elt(rules.field);
var message = container.getElementsByClassName(
"password-requirements-text")[0];
if (!field || !message) {
return true;
}
if (field.value == "") {
message.innerHTML = "";
setClass(rules.field, false, "field-ok");
field.password_ok = false;
return false;
}
var violations = passwordPolicyViolations(field.value, rules);
if (violations.length == 0) {
message.innerHTML = "";
setClass(rules.field, true, "field-ok");
if (!field.password_ok) {
field.password_ok = true;
doMessage("<h1 class=\"green\">" +
container.getAttribute("data-pw-ok") + "</h1>",
success_message_time);
}
return true;
}
setClass(rules.field, false, "field-ok");
field.password_ok = false;
var missing = passwordMissingText(violations, container);
missing = missing.charAt(0).toUpperCase() + missing.slice(1);
message.innerHTML = "<span class=\"green\">" + missing + "</span>";
return false;
}
/*
* Wires one password-requirements hint to its password box: shows the live
* feedback as the person types and, when the hint gates its form, blocks
* submitting an unacceptable password.
*/
function wirePasswordRequirements(container)
{
var rules = passwordRulesFromContainer(container);
var field = elt(rules.field);
if (!field) {
return;
}
showPasswordFeedback(container);
listen(field, "keyup", function () {
showPasswordFeedback(container);
});
listen(field, "blur", function () {
setClass(rules.field, false, "field-ok");
});
if (rules.gate && field.form) {
listen(field.form, "submit", function (event) {
if (passwordPolicyViolations(field.value, rules).length > 0) {
showPasswordFeedback(container);
field.focus();
event.preventDefault();
}
});
}
}
/*
* Wires up every password-requirements hint on the page. This runs on all
* the password forms, not just create-account.
*/
function setupPasswordRequirements()
{
var containers = document.getElementsByClassName(
"password-requirements");
for (var index = 0; index < containers.length; index++) {
wirePasswordRequirements(containers[index]);
}
}
/*
* Whether the create-account e-mail box holds a validly formatted address.
* Returns a yes/no answer only; the form marks a bad address by outlining
* the box in red rather than by printing words.
*/
function emailValid()
{
var mail_format = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
return elt("email").value.match(mail_format) != null;
}
/*
* Outlines the e-mail box in red once the person leaves it having typed
* something that is not a valid address, and clears that outline once the
* box is empty or valid again.
*/
function showEmailValidity()
{
setClass("email",
elt("email").value != "" && !emailValid(), "field-error");
}
/*
* Checks the create-account retyped password matches the first one. While
* they differ it shows a green note saying so; once they match it instead
* turns the retype box's border green, drops the note, and flashes a brief
* note at the top of the page.
*/
function passwordMatch()
{
var retyped_pass = elt("retype-password");
var password = elt("pass-word");
var password_match = elt("pass-match");
if (password.value == "") {
password_match.innerHTML = "";
setClass("retype-password", false, "field-ok");
retyped_pass.passwords_matched = false;
return true;
}
if (retyped_pass.value == password.value) {
password_match.innerHTML = "";
setClass("retype-password", true, "field-ok");
if (!retyped_pass.passwords_matched) {
retyped_pass.passwords_matched = true;
doMessage("<h1 class=\"green\">" +
tl['register_validator_js_retype_password_matched'] +
"</h1>", success_message_time);
}
return true;
} else {
setClass("retype-password", false, "field-ok");
retyped_pass.passwords_matched = false;
password_match.innerHTML = "<span class=\"green\">" +
tl['register_validator_js_retype_password_not_matched'] +
"</span>";
return false;
}
}
/*
* Shows the required-asterisk next to a box when it is empty and hides it
* otherwise. Returns whether the box has something in it.
*/
function showRequired(field_id)
{
var field = elt(field_id);
if (!field) {
return true;
}
if (field.value == "") {
setDisplay(field_id + "-star", true, "inline");
return false;
}
setDisplay(field_id + "-star", false);
return true;
}
/*
* Hides the required-asterisk next to a box once it has something in it,
* so an asterisk shown on a failed submit clears itself as the person
* fills the box in.
*/
function hideRequiredIfFilled(field_id)
{
var field = elt(field_id);
if (field && field.value != "") {
setDisplay(field_id + "-star", false);
}
}
/*
* Run when the create-account form is submitted: this is the one client
* check the submit button performs before anything is sent to the server.
* It shows an asterisk beside every required box left empty, a red outline
* on a badly typed e-mail, the retype note when the two passwords differ,
* and the password-rule feedback when the password breaks a rule. Returns
* whether the form is good to send.
*/
function checkRegistrationForm()
{
var required = ["firstname", "lastname", "username", "email",
"pass-word", "retype-password"];
var good = true;
for (var index = 0; index < required.length; index++) {
if (!showRequired(required[index])) {
good = false;
}
}
if (elt("email").value != "" && !emailValid()) {
showEmailValidity();
good = false;
}
if (!passwordMatch()) {
good = false;
}
var container = document.getElementsByClassName(
"password-requirements")[0];
if (container && !showPasswordFeedback(container)) {
good = false;
}
return good;
}
/*
* Wires the create-account-only fields. Each required box clears its
* asterisk as soon as it has something typed in it (and the e-mail box
* drops its red outline while being edited); the retype box updates its
* match note live; and submitting with anything missing shows the
* asterisks and holds the form back. The other password forms do not have
* these boxes, so this does nothing on them.
*/
function setupRegistrationForm()
{
if (!elt("firstname")) {
return;
}
var required = ["firstname", "lastname", "username", "email",
"pass-word", "retype-password"];
for (var index = 0; index < required.length; index++) {
wireRequiredField(required[index]);
}
listen(elt("email"), "blur", showEmailValidity);
listen(elt("retype-password"), "keyup", passwordMatch);
listen(elt("retype-password"), "blur", function () {
setClass("retype-password", false, "field-ok");
});
listen(elt("firstname").form, "submit", function (event) {
if (!checkRegistrationForm()) {
event.preventDefault();
}
});
}
/*
* Wires one create-account box so its asterisk clears as the box is filled
* in; the e-mail box also drops its red outline while being edited.
*/
function wireRequiredField(field_id)
{
listen(elt(field_id), "keyup", function () {
hideRequiredIfFilled(field_id);
if (field_id == "email") {
setClass("email", false, "field-error");
}
});
}
/*
* On page load, turn on the shared password-requirements feedback for every
* password box, then wire the create-account-only fields if they are
* present.
*/
listen(window, "load", function () {
setupPasswordRequirements();
setupRegistrationForm();
});