4chan/js/tcaptcha.js
2025-04-17 14:46:47 -05:00

684 lines
17 KiB
JavaScript

var TCaptcha = {
node: null,
frameNode: null,
imgCntNode: null,
bgNode: null,
fgNode: null,
msgNode: null,
sliderNode: null,
respNode: null,
reloadNode: null,
helpNode: null,
challengeNode: null,
ticketCaptchaNode: null,
challenge: null,
reloadTs: null,
reloadTimeout: null,
expireTimeout: null,
frameTimeout: null,
pcdBypassable: false,
errorCb: null,
path: '/captcha',
ticketKey: '4chan-tc-ticket',
domain: '4chan.org',
failCd: 60,
tabindex: null,
hCaptchaSiteKey: '49d294fa-f15c-41fc-80ba-c2544c52ec2a',
init: function(el, board, thread_id, tabindex) {
if (this.node) {
this.destroy();
}
if (tabindex) {
this.tabindex = tabindex;
}
this.node = el;
el.style.position = 'relative';
el.style.width = '300px';
this.frameNode = null;
this.imgCntNode = this.buildImgCntNode();
this.bgNode = this.buildImgNode('bg');
this.fgNode = this.buildImgNode('fg');
this.sliderNode = this.buildSliderNode();
this.respNode = this.buildRespField();
this.reloadNode = this.buildReloadNode(board, thread_id);
this.helpNode = this.buildHelpNode();
this.msgNode = this.buildMsgNode();
this.challengeNode = this.buildChallengeNode();
el.appendChild(this.reloadNode);
el.appendChild(this.respNode);
el.appendChild(this.helpNode);
this.imgCntNode.appendChild(this.bgNode);
this.imgCntNode.appendChild(this.fgNode);
el.appendChild(this.imgCntNode);
el.appendChild(this.sliderNode);
el.appendChild(this.msgNode);
el.appendChild(this.challengeNode);
window.addEventListener('message', this.onFrameMessage);
},
destroy: function() {
let self = TCaptcha;
if (!self.node) {
return;
}
window.removeEventListener('message', self.onFrameMessage);
clearTimeout(self.frameTimeout);
clearTimeout(self.reloadTimeout);
clearTimeout(self.expireTimeout);
self.node.textContent = '';
self.node = null;
self.frameNode = null;
self.imgCntNode = null;
self.bgNode = null;
self.fgNode = null;
self.msgNode = null;
self.sliderNode = null;
self.respNode = null;
self.reloadNode = null;
self.helpNode = null;
self.challengeNode = null;
self.ticketCaptchaNode = null;
self.challenge = null;
self.pcdBypassable = false;
self.errorCb = null;
self.reloadTs = null;
self.onReloadCdDone = null;
},
setErrorCb: function(func) {
TCaptcha.errorCb = func;
},
toggleImgCntNode: function(flag) {
TCaptcha.imgCntNode.style.display = flag ? 'block' : 'none';
},
getTicket: function() {
return localStorage.getItem(TCaptcha.ticketKey);
},
setTicket: function(val) {
if (val) {
localStorage.setItem(TCaptcha.ticketKey, val);
}
else if (val === false) {
localStorage.removeItem(TCaptcha.ticketKey);
}
},
buildFrameNode: function() {
let el = document.createElement('iframe');
el.id = 't-frame';
el.style.border = '0';
el.style.width = '100%';
el.style.height = '80px';
el.style.marginTop = '2px';
el.style.position = 'relative';
el.style.display = 'block';
el.onerror = TCaptcha.onFrameError;
return el;
},
buildImgCntNode: function() {
let el = document.createElement('div');
el.id = 't-cnt';
el.style.height = '80px';
el.style.marginTop = '2px';
el.style.position = 'relative';
return el;
},
buildImgNode: function(id) {
let el = document.createElement('div');
el.id = 't-' + id;
el.style.width = '100%';
el.style.height = '100%';
el.style.position = 'absolute';
el.style.backgroundRepeat = 'no-repeat';
el.style.backgroundPosition = 'top left';
el.style.pointerEvents = 'none';
return el;
},
buildMsgNode: function() {
let el = document.createElement('div');
el.id = 't-msg';
el.style.width = '100%';
el.style.height = 'calc(100% - 20px)';
el.style.position = 'absolute';
el.style.top = '20px';
el.style.textAlign = 'center';
el.style.fontSize = '12px';
el.style.filter = 'inherit';
el.style.display = 'none';
el.style.alignContent = 'center';
return el;
},
buildRespField: function() {
let el = document.createElement('input');
el.id = 't-resp';
el.name = 't-response';
el.placeholder = 'Type the CAPTCHA here';
el.setAttribute('autocomplete', 'off');
el.type = 'text';
el.style.width = '160px';
el.style.boxSizing = 'border-box';
el.style.textTransform = 'uppercase';
el.style.fontSize = '11px';
el.style.height = '18px';
el.style.margin = '0';
el.style.padding = '0 2px';
el.style.fontFamily = 'monospace';
el.style.verticalAlign = 'middle';
if (this.tabindex) {
el.setAttribute('tabindex', this.tabindex + 2);
}
return el;
},
buildSliderNode: function() {
let el = document.createElement('input');
el.id = 't-slider';
el.setAttribute('autocomplete', 'off');
el.type = 'range';
el.style.width = '100%';
el.style.boxSizing = 'border-box';
el.style.visibility = 'hidden';
el.style.margin = '0';
el.style.transition = 'box-shadow 15s linear';
el.style.boxShadow = '0 0 6px 4px #1d8dc4';
el.style.position = 'relative';
el.value = 0;
el.min = 0;
el.max = 100;
el.addEventListener('input', this.onSliderInput, false);
if (this.tabindex) {
el.setAttribute('tabindex', this.tabindex + 1);
}
return el;
},
buildChallengeNode: function() {
let el = document.createElement('input');
el.name = 't-challenge';
el.type = 'hidden';
return el;
},
buildReloadNode: function(board, thread_id) {
let el = document.createElement('button');
el.id = 't-load';
el.type = 'button';
el.style.fontSize = '11px';
el.style.padding = '0';
el.style.width = '90px';
el.style.boxSizing = 'border-box';
el.style.margin = '0 6px 0 0';
el.style.verticalAlign = 'middle';
el.style.height = '18px';
el.textContent = 'Get Captcha';
el.setAttribute('data-board', board);
el.setAttribute('data-tid', thread_id);
el.addEventListener('click', this.onReloadClick, false);
if (this.tabindex) {
el.setAttribute('tabindex', this.tabindex);
}
return el;
},
buildHelpNode: function() {
let el = document.createElement('button');
el.id = 't-help';
el.type = 'button';
el.style.fontSize = '11px';
el.style.padding = '0';
el.style.width = '20px';
el.style.boxSizing = 'border-box';
el.style.margin = '0 0 0 6px';
el.style.verticalAlign = 'middle';
el.style.height = '18px';
el.textContent = '?';
el.setAttribute('data-tip', 'Help');
el.tabIndex = -1;
el.addEventListener('click', this.onHelpClick, false);
return el;
},
onHelpClick: function() {
let str = `- Only type letters and numbers displayed in the image.
- If needed, use the slider to align the image to make it readable.
- Make sure to not block any cookies set by 4chan.`;
alert(str);
},
onTicketCaptchaError: function() {
TCaptcha.toggleMsgOverlay(true, "Couldn't load the captcha.<br><br>Please check your browser's content blocker.");
},
onTicketCaptchaDone: function(resp) {
TCaptcha.reloadNode.setAttribute('data-ticket-resp', resp);
TCaptcha.destroyTicketCaptcha();
TCaptcha.onReloadClick();
},
loadTicketCaptcha: function() {
window.pcd_c_loaded = TCaptcha.buildTicketCaptcha;
window.pcd_c_done = TCaptcha.onTicketCaptchaDone;
TCaptcha.toggleMsgOverlay(true, 'Loading…');
let s = document.createElement('script');
s.src = 'https://js.hcaptcha.com/1/api.js?onload=pcd_c_loaded&render=explicit&recaptchacompat=off';
s.onerror = TCaptcha.onTicketCaptchaError;
document.head.appendChild(s);
},
buildTicketCaptcha: function() {
let self = TCaptcha;
self.toggleMsgOverlay(false);
if (!window.hcaptcha) {
self.loadTicketCaptcha();
return;
}
let el = document.createElement('div');
el.id = 't-tc-cnt';
self.imgCntNode.appendChild(el);
let wid = window.hcaptcha.render('t-tc-cnt', {
sitekey: self.hCaptchaSiteKey,
callback: 'pcd_c_done'
});
el.setAttribute('data-wid', wid);
self.ticketCaptchaNode = el;
},
destroyTicketCaptcha: function() {
let self = TCaptcha;
if (!window.hcaptcha || !self.ticketCaptchaNode) {
return;
}
let wid = self.ticketCaptchaNode.getAttribute('data-wid');
window.hcaptcha.reset(wid);
self.imgCntNode.removeChild(self.ticketCaptchaNode);
self.ticketCaptchaNode = null;
},
onReloadClick: function() {
let btn = TCaptcha.reloadNode;
let board = btn.getAttribute('data-board');
let thread_id = btn.getAttribute('data-tid');
let ticket_resp = btn.getAttribute('data-ticket-resp');
btn.removeAttribute('data-ticket-resp');
TCaptcha.toggleReloadBtn(false, 'Loading');
TCaptcha.load(board, thread_id, ticket_resp);
},
onFrameMessage: function(e) {
if (e.origin !== `https://sys.${TCaptcha.domain}`) {
return;
}
if (e.data && e.data.twister) {
TCaptcha.destroyFrame();
TCaptcha.buildFromJson(e.data.twister);
}
},
onFrameError: function(e) {
TCaptcha.unlockReloadBtn();
console.log(e);
if (TCaptcha.errorCb) {
TCaptcha.errorCb.call(null,
"Couldn't load the captcha frame. Check your content blocker settings."
);
}
},
load: function(board, thread_id, ticket_resp) {
let self = TCaptcha;
clearTimeout(self.frameTimeout);
clearTimeout(self.reloadTimeout);
clearTimeout(self.expireTimeout);
let params = ['framed=1'];
if (board) {
params.push('board=' + board);
}
if (thread_id > 0) {
params.push('thread_id=' + thread_id);
}
if (ticket_resp) {
params.push('ticket_resp=' + encodeURIComponent(ticket_resp));
}
let ticket = self.getTicket();
if (ticket) {
params.push('ticket=' + ticket);
}
if (params.length > 0) {
params = '?' + params.join('&');
}
let src = 'https://sys.' + self.domain + self.path + params;
self.frameNode = self.buildFrameNode();
self.toggleImgCntNode(false);
self.node.insertBefore(self.frameNode, self.imgCntNode);
self.frameTimeout = setTimeout(self.onFrameTimeout, 60000, src);
self.frameNode.src = src;
},
onFrameTimeout: function(src) {
let self = TCaptcha;
self.destroyFrame();
console.log('Captcha frame timeout');
if (self.errorCb) {
self.errorCb.call(null, `Couldn't get the captcha.
Make sure your browser doesn't block content on 4chan then click
<a href="${src.replace('framed=1', 'opened=1')}" target="_blank">here</a>.`);
}
},
destroyFrame: function() {
let self = TCaptcha;
clearTimeout(self.frameTimeout);
self.frameTimeout = null;
if (self.frameNode) {
self.frameNode.remove();
self.frameNode = null;
}
self.toggleImgCntNode(true);
self.unlockReloadBtn();
},
unlockReloadBtn: function() {
TCaptcha.reloadTs = null;
TCaptcha.toggleReloadBtn(true, 'Get Captcha');
},
toggleReloadBtn: function(flag, label) {
let self = TCaptcha;
if (self.reloadNode) {
self.reloadNode.disabled = !flag;
if (label !== undefined) {
self.reloadNode.textContent = label;
}
}
},
onCaptchaFailed: function() {
let self = TCaptcha;
let cd = self.failCd * 1000;
if (self.reloadTs && self.reloadTs < cd) {
self.setReloadCd(cd, true);
}
},
setReloadCd: function(cd, visible, onDone) {
let self = TCaptcha;
if (!self.node) {
return;
}
clearTimeout(self.reloadTimeout);
self.onReloadCdDone = onDone;
self.pcdBypassable = visible === -1;
if (cd) {
self.toggleReloadBtn(false);
if (visible) {
self.reloadTs = Date.now() + cd;
self.onReloadCdTick();
}
else {
self.reloadTimeout = setTimeout(self.stopReloadCd, cd);
}
}
else {
self.stopReloadCd();
}
},
stopReloadCd: function() {
let self = TCaptcha;
self.unlockReloadBtn();
if (self.onReloadCdDone) {
self.onReloadCdDone.call(self);
}
},
onReloadCdTick: function() {
let self = TCaptcha;
if (!self.reloadNode || !self.reloadTs) {
return;
}
let cd = self.reloadTs - Date.now();
if (self.pcdBypassable) {
if (document.cookie.indexOf('_ev1=') !== -1) {
cd = 0;
}
}
if (cd > 0) {
self.reloadNode.textContent = Math.ceil(cd / 1000);
self.reloadTimeout = setTimeout(self.onReloadCdTick, Math.min(cd, 1000));
}
else {
self.stopReloadCd();
}
},
clearChallenge: function() {
let self = TCaptcha;
if (self.node) {
self.challengeNode.value = '';
self.respNode.value = '';
self.fgNode.style.backgroundImage = '';
self.bgNode.style.backgroundImage = '';
self.toggleSlider(false);
self.toggleMsgOverlay(false);
}
},
toggleSlider: function(flag) {
TCaptcha.sliderNode.style.visibility = flag ? '' : 'hidden';
TCaptcha.sliderNode.style.boxShadow = flag ? '' : '0 0 4px 2px #1d8dc4';
},
toggleMsgOverlay: function(flag, txt) {
if (txt !== undefined) {
TCaptcha.msgNode.innerHTML = `<div>${txt}</div>`;
}
TCaptcha.msgNode.style.display = flag ? 'grid' : 'none';
},
onSliderInput: function() {
var m = -Math.floor((+this.value) / 100 * this.twisterDelta);
TCaptcha.bgNode.style.backgroundPositionX = m + 'px';
},
onTicketPcdTick: function() {
let self = TCaptcha;
let el = document.getElementById('t-pcd');
if (!el) {
return;
}
let pcd = +el.getAttribute('data-pcd');
pcd = pcd - (0 | (Date.now() / 1000));
if (pcd <= 0) {
self.onTicketPcdEnd();
return;
}
el.textContent = pcd;
setTimeout(self.updateTicketPcd, 1000);
},
clearTicketOverlay: function() {
TCaptcha.toggleMsgOverlay(false);
},
buildFromJson: function(data) {
let self = TCaptcha;
if (!self.node) {
return;
}
self.unlockReloadBtn();
self.toggleSlider(false);
self.toggleMsgOverlay(false);
self.setTicket(data.ticket);
if (TCaptcha.errorCb) {
TCaptcha.errorCb.call(null, '');
}
if (data.cd) {
self.setReloadCd(data.cd * 1000, !data.challenge);
}
if (data.mpcd) {
self.clearChallenge();
self.destroyTicketCaptcha();
self.buildTicketCaptcha();
return;
}
if (data.pcd) {
self.buildTicket(data);
return;
}
if (data.error) {
console.log(data.error);
if (TCaptcha.errorCb) {
TCaptcha.errorCb.call(null, data.error);
}
return;
}
self.imgCntNode.style.width = data.img_width + 'px';
self.imgCntNode.style.height = data.img_height + 'px';
self.challengeNode.value = data.challenge;
self.expireTimeout = setTimeout(self.clearChallenge, data.ttl * 1000 - 3000);
if (data.bg_width) {
self.buildTwister(data);
}
else if (data.img) {
self.buildStatic(data);
}
else {
self.buildNoop(data);
}
},
buildTwister: function(data) {
let self = TCaptcha;
self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')';
self.bgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.bg + ')';
self.bgNode.style.backgroundPositionX = '0px';
self.toggleSlider(true);
self.sliderNode.value = 0;
self.sliderNode.twisterDelta = data.bg_width - data.img_width;
self.sliderNode.focus();
},
buildStatic: function(data) {
let self = TCaptcha;
self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')';
self.bgNode.style.backgroundImage = '';
},
buildTicket: function(data) {
let self = TCaptcha;
self.toggleMsgOverlay(true, data.pcd_msg || 'Please wait a while.');
self.fgNode.style.backgroundImage = '';
self.bgNode.style.backgroundImage = '';
self.setReloadCd(data.pcd * 1000, data.bpcd ? -1 : true, self.clearTicketOverlay);
},
buildNoop: function(data) {
let self = TCaptcha;
self.toggleMsgOverlay(true, 'Verification not required.');
self.fgNode.style.backgroundImage = '';
self.bgNode.style.backgroundImage = '';
}
};