release: WRNexusJS 0.4.0
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/* global Audio, CSS, CustomEvent, Element, HTMLElement, MutationObserver, URL, clearInterval, clearTimeout, document, fetch, window */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
@@ -5,6 +6,11 @@
|
||||
var existingRuntime = window[RUNTIME_KEY];
|
||||
|
||||
if (existingRuntime && typeof existingRuntime.scan === "function") {
|
||||
window.__wrnexusRuntimes = window.__wrnexusRuntimes || {};
|
||||
window.__wrnexusRuntimes.captcha = {
|
||||
mount: existingRuntime.scan,
|
||||
unmount: existingRuntime.unmount || function () {},
|
||||
};
|
||||
existingRuntime.scan(document);
|
||||
return;
|
||||
}
|
||||
@@ -26,7 +32,9 @@
|
||||
|
||||
function normalizeSize(value, compact) {
|
||||
if (compact) return "compact";
|
||||
var normalized = String(value || "normal").trim().toLowerCase();
|
||||
var normalized = String(value || "normal")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (normalized === "compact" || normalized === "small" || normalized === "sm") return "compact";
|
||||
if (normalized === "big" || normalized === "large" || normalized === "lg") return "big";
|
||||
return "normal";
|
||||
@@ -35,7 +43,9 @@
|
||||
function commaList(value) {
|
||||
return String(value || "")
|
||||
.split(",")
|
||||
.map(function (item) { return item.trim().toLowerCase(); })
|
||||
.map(function (item) {
|
||||
return item.trim().toLowerCase();
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -89,7 +99,8 @@
|
||||
requiredMessage: data.captchaRequiredMessage || "Please complete the security check.",
|
||||
incorrectMessage: data.captchaIncorrectMessage || "That answer was not correct. Try again.",
|
||||
expiredMessage: data.captchaExpiredMessage || "This challenge expired. Load a new one.",
|
||||
networkMessage: data.captchaNetworkMessage || "The verification service is unavailable. Try again.",
|
||||
networkMessage:
|
||||
data.captchaNetworkMessage || "The verification service is unavailable. Try again.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,15 +112,18 @@
|
||||
type: challenge && challenge.type ? challenge.type : state.config.type,
|
||||
action: state.config.action,
|
||||
disturbance: state.config.disturbance,
|
||||
imageStyle: challenge && challenge.metadata && challenge.metadata.imageStyle
|
||||
? challenge.metadata.imageStyle
|
||||
: state.config.imageStyle,
|
||||
requestedImageStyle: challenge && challenge.metadata && challenge.metadata.requestedImageStyle
|
||||
? challenge.metadata.requestedImageStyle
|
||||
: state.config.imageStyle,
|
||||
imageStylePool: challenge && challenge.metadata && challenge.metadata.imageStylePool
|
||||
? challenge.metadata.imageStylePool
|
||||
: state.config.allowedStyles,
|
||||
imageStyle:
|
||||
challenge && challenge.metadata && challenge.metadata.imageStyle
|
||||
? challenge.metadata.imageStyle
|
||||
: state.config.imageStyle,
|
||||
requestedImageStyle:
|
||||
challenge && challenge.metadata && challenge.metadata.requestedImageStyle
|
||||
? challenge.metadata.requestedImageStyle
|
||||
: state.config.imageStyle,
|
||||
imageStylePool:
|
||||
challenge && challenge.metadata && challenge.metadata.imageStylePool
|
||||
? challenge.metadata.imageStylePool
|
||||
: state.config.allowedStyles,
|
||||
size: state.config.size,
|
||||
status: state.status,
|
||||
challengeId: challenge && challenge.id ? challenge.id : "",
|
||||
@@ -190,7 +204,14 @@
|
||||
var answer = state.root.querySelector("[data-captcha-answer]");
|
||||
|
||||
show(loading, status === "loading");
|
||||
show(challenge, status !== "loading" && status !== "idle" && state.config.provider !== "turnstile" && state.config.provider !== "recaptcha" && state.config.provider !== "hcaptcha");
|
||||
show(
|
||||
challenge,
|
||||
status !== "loading" &&
|
||||
status !== "idle" &&
|
||||
state.config.provider !== "turnstile" &&
|
||||
state.config.provider !== "recaptcha" &&
|
||||
state.config.provider !== "hcaptcha",
|
||||
);
|
||||
show(badge, status === "verified");
|
||||
|
||||
if (state.config.showStatus) {
|
||||
@@ -201,16 +222,27 @@
|
||||
show(error, false);
|
||||
}
|
||||
|
||||
if (message && status === "verified") text(state.root, "[data-captcha-success-message]", message);
|
||||
if (message && status === "verified")
|
||||
text(state.root, "[data-captcha-success-message]", message);
|
||||
if (message && status !== "verified") text(state.root, "[data-captcha-error-message]", message);
|
||||
|
||||
if (answer) {
|
||||
answer.disabled = state.config.disabled || status === "verified" || status === "expired" || status === "loading" || status === "verifying";
|
||||
answer.disabled =
|
||||
state.config.disabled ||
|
||||
status === "verified" ||
|
||||
status === "expired" ||
|
||||
status === "loading" ||
|
||||
status === "verifying";
|
||||
answer.setAttribute("aria-invalid", status === "incorrect" ? "true" : "false");
|
||||
}
|
||||
|
||||
if (verifyButton) {
|
||||
verifyButton.disabled = state.config.disabled || status === "loading" || status === "verifying" || status === "verified" || status === "expired";
|
||||
verifyButton.disabled =
|
||||
state.config.disabled ||
|
||||
status === "loading" ||
|
||||
status === "verifying" ||
|
||||
status === "verified" ||
|
||||
status === "expired";
|
||||
}
|
||||
|
||||
show(verifyIcon, status !== "verifying");
|
||||
@@ -291,7 +323,12 @@
|
||||
honeypot.value = "";
|
||||
honeypot.name = "";
|
||||
}
|
||||
if (providerMount && state.config.provider !== "turnstile" && state.config.provider !== "recaptcha" && state.config.provider !== "hcaptcha") {
|
||||
if (
|
||||
providerMount &&
|
||||
state.config.provider !== "turnstile" &&
|
||||
state.config.provider !== "recaptcha" &&
|
||||
state.config.provider !== "hcaptcha"
|
||||
) {
|
||||
providerMount.replaceChildren();
|
||||
show(providerMount, false);
|
||||
}
|
||||
@@ -304,26 +341,52 @@
|
||||
var refresh = state.root.querySelector("[data-captcha-refresh]");
|
||||
var verify = state.root.querySelector("[data-captcha-verify]");
|
||||
var footer = state.root.querySelector("[data-captcha-footer]");
|
||||
var isExternal = state.config.provider === "turnstile" || state.config.provider === "recaptcha" || state.config.provider === "hcaptcha";
|
||||
var isExternal =
|
||||
state.config.provider === "turnstile" ||
|
||||
state.config.provider === "recaptcha" ||
|
||||
state.config.provider === "hcaptcha";
|
||||
var notRobot = isNotRobot(state);
|
||||
var locked = state.config.disabled || state.status === "loading" || state.status === "verifying" || state.notRobotPending;
|
||||
var locked =
|
||||
state.config.disabled ||
|
||||
state.status === "loading" ||
|
||||
state.status === "verifying" ||
|
||||
state.notRobotPending;
|
||||
|
||||
show(footer, !notRobot);
|
||||
show(audio, !notRobot && state.config.showAudio && state.config.showListen && Boolean(challenge && challenge.audioUrl) && state.status !== "verified");
|
||||
show(audioAlternative, !notRobot && state.config.showAudio && Boolean(challenge && challenge.type === "image") && state.status !== "verified");
|
||||
show(
|
||||
audio,
|
||||
!notRobot &&
|
||||
state.config.showAudio &&
|
||||
state.config.showListen &&
|
||||
Boolean(challenge && challenge.audioUrl) &&
|
||||
state.status !== "verified",
|
||||
);
|
||||
show(
|
||||
audioAlternative,
|
||||
!notRobot &&
|
||||
state.config.showAudio &&
|
||||
Boolean(challenge && challenge.type === "image") &&
|
||||
state.status !== "verified",
|
||||
);
|
||||
show(refresh, !notRobot && state.config.showRefresh && state.status !== "verified");
|
||||
show(verify, !notRobot && state.config.showVerify && !isExternal);
|
||||
|
||||
if (audio) audio.disabled = locked;
|
||||
if (audioAlternative) audioAlternative.disabled = locked;
|
||||
if (refresh) refresh.disabled = locked;
|
||||
if (verify) verify.disabled = locked || state.status === "verified" || state.status === "expired";
|
||||
if (verify)
|
||||
verify.disabled = locked || state.status === "verified" || state.status === "expired";
|
||||
}
|
||||
|
||||
function renderItems(state, challenge) {
|
||||
var container = state.root.querySelector("[data-captcha-items]");
|
||||
var template = state.root.querySelector("[data-captcha-item-template]");
|
||||
if (!container || !template || !Array.isArray(challenge.items) || challenge.items.length === 0) {
|
||||
if (
|
||||
!container ||
|
||||
!template ||
|
||||
!Array.isArray(challenge.items) ||
|
||||
challenge.items.length === 0
|
||||
) {
|
||||
show(container, false);
|
||||
return;
|
||||
}
|
||||
@@ -359,10 +422,14 @@
|
||||
if (selected) {
|
||||
state.selections.splice(index, 1);
|
||||
} else {
|
||||
var maximum = Number(state.challenge && state.challenge.maxSelections ? state.challenge.maxSelections : 0);
|
||||
var maximum = Number(
|
||||
state.challenge && state.challenge.maxSelections ? state.challenge.maxSelections : 0,
|
||||
);
|
||||
if (maximum > 0 && state.selections.length >= maximum) {
|
||||
var removed = state.selections.shift();
|
||||
var previousButton = state.root.querySelector('[data-captcha-item-id="' + CSS.escape(removed) + '"]');
|
||||
var previousButton = state.root.querySelector(
|
||||
'[data-captcha-item-id="' + CSS.escape(removed) + '"]',
|
||||
);
|
||||
if (previousButton) {
|
||||
previousButton.setAttribute("aria-pressed", "false");
|
||||
show(previousButton.querySelector("[data-captcha-item-check]"), false);
|
||||
@@ -376,7 +443,9 @@
|
||||
show(button.querySelector("[data-captcha-item-check]"), nowSelected);
|
||||
emit(state, "input", { selections: state.selections.slice() });
|
||||
|
||||
var minimum = Number(state.challenge && state.challenge.minSelections ? state.challenge.minSelections : 0);
|
||||
var minimum = Number(
|
||||
state.challenge && state.challenge.minSelections ? state.challenge.minSelections : 0,
|
||||
);
|
||||
if (state.config.autoVerify && minimum > 0 && state.selections.length >= minimum) verify(state);
|
||||
}
|
||||
|
||||
@@ -389,10 +458,15 @@
|
||||
|
||||
var notRobot = challenge.type === "not-robot";
|
||||
state.root.dataset.captchaType = challenge.type || state.config.type;
|
||||
state.root.dataset.captchaResolvedImageStyle = challenge.metadata && challenge.metadata.imageStyle
|
||||
? String(challenge.metadata.imageStyle)
|
||||
: state.config.imageStyle;
|
||||
text(state.root, "[data-captcha-prompt]", challenge.prompt || "Complete the security challenge.");
|
||||
state.root.dataset.captchaResolvedImageStyle =
|
||||
challenge.metadata && challenge.metadata.imageStyle
|
||||
? String(challenge.metadata.imageStyle)
|
||||
: state.config.imageStyle;
|
||||
text(
|
||||
state.root,
|
||||
"[data-captcha-prompt]",
|
||||
challenge.prompt || "Complete the security challenge.",
|
||||
);
|
||||
|
||||
var promptRow = state.root.querySelector("[data-captcha-prompt-row]");
|
||||
var notRobotPanel = state.root.querySelector("[data-captcha-not-robot]");
|
||||
@@ -435,7 +509,12 @@
|
||||
emit(state, "challenge", challenge);
|
||||
emit(state, "ready");
|
||||
|
||||
if (state.config.autoVerify && !notRobot && challenge.inputMode === "none" && !challenge.items) {
|
||||
if (
|
||||
state.config.autoVerify &&
|
||||
!notRobot &&
|
||||
challenge.inputMode === "none" &&
|
||||
!challenge.items
|
||||
) {
|
||||
window.setTimeout(function () {
|
||||
verify(state);
|
||||
}, 0);
|
||||
@@ -445,7 +524,11 @@
|
||||
async function createChallenge(state, requestedPresentation) {
|
||||
if (state.config.disabled) return;
|
||||
|
||||
if (state.config.provider === "turnstile" || state.config.provider === "recaptcha" || state.config.provider === "hcaptcha") {
|
||||
if (
|
||||
state.config.provider === "turnstile" ||
|
||||
state.config.provider === "recaptcha" ||
|
||||
state.config.provider === "hcaptcha"
|
||||
) {
|
||||
await mountExternal(state);
|
||||
return;
|
||||
}
|
||||
@@ -495,7 +578,13 @@
|
||||
|
||||
async function verify(state) {
|
||||
state.notRobotPending = false;
|
||||
if (state.config.disabled || state.status === "loading" || state.status === "verifying" || state.status === "expired" || state.status === "verified") {
|
||||
if (
|
||||
state.config.disabled ||
|
||||
state.status === "loading" ||
|
||||
state.status === "verifying" ||
|
||||
state.status === "expired" ||
|
||||
state.status === "verified"
|
||||
) {
|
||||
updateNotRobotState(state);
|
||||
updateControls(state);
|
||||
return;
|
||||
@@ -636,7 +725,13 @@
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
script.addEventListener("error", function () { reject(new Error("CAPTCHA provider script failed")); }, { once: true });
|
||||
script.addEventListener(
|
||||
"error",
|
||||
function () {
|
||||
reject(new Error("CAPTCHA provider script failed"));
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
|
||||
scriptPromises.set(definition.url, promise);
|
||||
@@ -674,7 +769,12 @@
|
||||
state.externalWidgetId = api.render(mount, {
|
||||
sitekey: state.config.siteKey,
|
||||
theme: "auto",
|
||||
size: state.config.size === "compact" ? "compact" : state.config.provider === "turnstile" ? "flexible" : "normal",
|
||||
size:
|
||||
state.config.size === "compact"
|
||||
? "compact"
|
||||
: state.config.provider === "turnstile"
|
||||
? "flexible"
|
||||
: "normal",
|
||||
action: state.config.action,
|
||||
callback: function (token) {
|
||||
setResponseToken(state, token);
|
||||
@@ -715,36 +815,55 @@
|
||||
audio.volume = 1;
|
||||
audio.src = new URL(state.challenge.audioUrl, window.location.href).href;
|
||||
|
||||
audio.addEventListener("ended", function () {
|
||||
if (state.audioPlayer === audio) state.audioPlayer = null;
|
||||
emit(state, "audioEnd");
|
||||
emit(state, "audio-end");
|
||||
}, { once: true });
|
||||
audio.addEventListener(
|
||||
"ended",
|
||||
function () {
|
||||
if (state.audioPlayer === audio) state.audioPlayer = null;
|
||||
emit(state, "audioEnd");
|
||||
emit(state, "audio-end");
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
audio.addEventListener("error", function () {
|
||||
if (state.audioPlayer === audio) state.audioPlayer = null;
|
||||
var mediaError = audio.error;
|
||||
setStatus(state, "network-error", "The audio challenge could not be played. Load a new challenge and try again.");
|
||||
emit(state, "error", {
|
||||
code: "audio-playback-failed",
|
||||
audioUrl: audio.src,
|
||||
mediaErrorCode: mediaError ? mediaError.code : null,
|
||||
});
|
||||
}, { once: true });
|
||||
audio.addEventListener(
|
||||
"error",
|
||||
function () {
|
||||
if (state.audioPlayer === audio) state.audioPlayer = null;
|
||||
var mediaError = audio.error;
|
||||
setStatus(
|
||||
state,
|
||||
"network-error",
|
||||
"The audio challenge could not be played. Load a new challenge and try again.",
|
||||
);
|
||||
emit(state, "error", {
|
||||
code: "audio-playback-failed",
|
||||
audioUrl: audio.src,
|
||||
mediaErrorCode: mediaError ? mediaError.code : null,
|
||||
});
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
audio.load();
|
||||
audio.play().then(function () {
|
||||
emit(state, "audioStart");
|
||||
emit(state, "audio-start");
|
||||
}).catch(function (error) {
|
||||
if (state.audioPlayer === audio) state.audioPlayer = null;
|
||||
setStatus(state, "network-error", "The audio challenge could not be played. Check the audio endpoint and try again.");
|
||||
emit(state, "error", {
|
||||
code: "audio-playback-rejected",
|
||||
audioUrl: audio.src,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
audio
|
||||
.play()
|
||||
.then(function () {
|
||||
emit(state, "audioStart");
|
||||
emit(state, "audio-start");
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (state.audioPlayer === audio) state.audioPlayer = null;
|
||||
setStatus(
|
||||
state,
|
||||
"network-error",
|
||||
"The audio challenge could not be played. Check the audio endpoint and try again.",
|
||||
);
|
||||
emit(state, "error", {
|
||||
code: "audio-playback-rejected",
|
||||
audioUrl: audio.src,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onFormSubmit(state, event) {
|
||||
@@ -753,7 +872,10 @@
|
||||
event.stopImmediatePropagation();
|
||||
setStatus(state, "incorrect", state.config.requiredMessage);
|
||||
emit(state, "failure", { code: "missing-input" });
|
||||
var target = state.root.querySelector("[data-captcha-answer]") || state.root.querySelector("[data-captcha-not-robot-button]") || state.root.querySelector("[data-captcha-verify]");
|
||||
var target =
|
||||
state.root.querySelector("[data-captcha-answer]") ||
|
||||
state.root.querySelector("[data-captcha-not-robot-button]") ||
|
||||
state.root.querySelector("[data-captcha-verify]");
|
||||
if (target && typeof target.focus === "function") target.focus();
|
||||
}
|
||||
|
||||
@@ -781,6 +903,8 @@
|
||||
externalWidgetId: null,
|
||||
audioPlayer: null,
|
||||
form: root.closest("form"),
|
||||
formSubmitListener: null,
|
||||
formSuccessListener: null,
|
||||
};
|
||||
states.set(root, state);
|
||||
root.dataset.captchaSize = state.config.size;
|
||||
@@ -808,17 +932,28 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (verifyButton) verifyButton.addEventListener("click", function () { verify(state); });
|
||||
if (notRobotButton) notRobotButton.addEventListener("click", function () { verifyNotRobot(state); });
|
||||
if (refreshButton) refreshButton.addEventListener("click", function () {
|
||||
emit(state, "refresh");
|
||||
createChallenge(state, state.config.presentation);
|
||||
});
|
||||
if (audioButton) audioButton.addEventListener("click", function () { playAudio(state); });
|
||||
if (audioAlternative) audioAlternative.addEventListener("click", function () {
|
||||
emit(state, "refresh", { presentation: "audio" });
|
||||
createChallenge(state, "audio");
|
||||
});
|
||||
if (verifyButton)
|
||||
verifyButton.addEventListener("click", function () {
|
||||
verify(state);
|
||||
});
|
||||
if (notRobotButton)
|
||||
notRobotButton.addEventListener("click", function () {
|
||||
verifyNotRobot(state);
|
||||
});
|
||||
if (refreshButton)
|
||||
refreshButton.addEventListener("click", function () {
|
||||
emit(state, "refresh");
|
||||
createChallenge(state, state.config.presentation);
|
||||
});
|
||||
if (audioButton)
|
||||
audioButton.addEventListener("click", function () {
|
||||
playAudio(state);
|
||||
});
|
||||
if (audioAlternative)
|
||||
audioAlternative.addEventListener("click", function () {
|
||||
emit(state, "refresh", { presentation: "audio" });
|
||||
createChallenge(state, "audio");
|
||||
});
|
||||
|
||||
root.addEventListener("captcha-reset", function () {
|
||||
emit(state, "refresh");
|
||||
@@ -826,12 +961,14 @@
|
||||
});
|
||||
|
||||
if (state.form) {
|
||||
state.form.addEventListener("submit", function (event) {
|
||||
state.formSubmitListener = function (event) {
|
||||
onFormSubmit(state, event);
|
||||
}, true);
|
||||
state.form.addEventListener("wire:success", function () {
|
||||
};
|
||||
state.formSuccessListener = function () {
|
||||
createChallenge(state, state.config.presentation);
|
||||
});
|
||||
};
|
||||
state.form.addEventListener("submit", state.formSubmitListener, true);
|
||||
state.form.addEventListener("wire:success", state.formSuccessListener);
|
||||
}
|
||||
|
||||
updateNotRobotState(state);
|
||||
@@ -846,8 +983,53 @@
|
||||
host.querySelectorAll("[data-wrn-captcha]").forEach(initialize);
|
||||
}
|
||||
|
||||
function cleanupRoot(root) {
|
||||
var state = states.get(root);
|
||||
if (!state) return;
|
||||
if (state.timer) clearInterval(state.timer);
|
||||
if (state.notRobotTimer) clearTimeout(state.notRobotTimer);
|
||||
if (state.audioPlayer) {
|
||||
try {
|
||||
state.audioPlayer.pause();
|
||||
state.audioPlayer.src = "";
|
||||
} catch {
|
||||
// Best-effort cleanup for browser audio implementations.
|
||||
}
|
||||
}
|
||||
if (state.form && state.formSubmitListener) {
|
||||
state.form.removeEventListener("submit", state.formSubmitListener, true);
|
||||
}
|
||||
if (state.form && state.formSuccessListener) {
|
||||
state.form.removeEventListener("wire:success", state.formSuccessListener);
|
||||
}
|
||||
try {
|
||||
if (state.externalApi && state.externalWidgetId != null) {
|
||||
if (typeof state.externalApi.remove === "function")
|
||||
state.externalApi.remove(state.externalWidgetId);
|
||||
else if (typeof state.externalApi.reset === "function")
|
||||
state.externalApi.reset(state.externalWidgetId);
|
||||
}
|
||||
} catch {
|
||||
// Third-party providers may reject cleanup after navigation.
|
||||
}
|
||||
states.delete(root);
|
||||
}
|
||||
|
||||
function unmount(scope) {
|
||||
var host = scope || document;
|
||||
var roots = [];
|
||||
if (host instanceof Element && host.matches("[data-wrn-captcha]")) roots.push(host);
|
||||
if (host.querySelectorAll)
|
||||
host.querySelectorAll("[data-wrn-captcha]").forEach(function (root) {
|
||||
roots.push(root);
|
||||
});
|
||||
roots.forEach(cleanupRoot);
|
||||
}
|
||||
|
||||
var runtime = {
|
||||
scan: scan,
|
||||
mount: scan,
|
||||
unmount: unmount,
|
||||
reset: function (element) {
|
||||
var root = typeof element === "string" ? document.querySelector(element) : element;
|
||||
var state = root ? states.get(root) : null;
|
||||
@@ -861,9 +1043,17 @@
|
||||
};
|
||||
|
||||
window[RUNTIME_KEY] = runtime;
|
||||
window.__wrnexusRuntimes = window.__wrnexusRuntimes || {};
|
||||
window.__wrnexusRuntimes.captcha = { mount: scan, unmount: unmount };
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () { scan(document); }, { once: true });
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
function () {
|
||||
scan(document);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
} else {
|
||||
scan(document);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user