tcaptcha.js
1 var TCaptcha = { 2 node: null, 3 4 frameNode: null, 5 imgCntNode: null, 6 bgNode: null, 7 fgNode: null, 8 msgNode: null, 9 sliderNode: null, 10 respNode: null, 11 reloadNode: null, 12 helpNode: null, 13 challengeNode: null, 14 15 ticketCaptchaNode: null, 16 17 challenge: null, 18 19 reloadTs: null, 20 reloadTimeout: null, 21 expireTimeout: null, 22 frameTimeout: null, 23 24 pcdBypassable: false, 25 26 errorCb: null, 27 28 path: '/captcha', 29 30 ticketKey: '4chan-tc-ticket', 31 32 domain: '4chan.org', 33 34 failCd: 60, 35 36 tabindex: null, 37 38 hCaptchaSiteKey: '49d294fa-f15c-41fc-80ba-c2544c52ec2a', 39 40 init: function(el, board, thread_id, tabindex) { 41 if (this.node) { 42 this.destroy(); 43 } 44 45 if (tabindex) { 46 this.tabindex = tabindex; 47 } 48 49 this.node = el; 50 51 el.style.position = 'relative'; 52 el.style.width = '300px'; 53 54 this.frameNode = null; 55 this.imgCntNode = this.buildImgCntNode(); 56 this.bgNode = this.buildImgNode('bg'); 57 this.fgNode = this.buildImgNode('fg'); 58 this.sliderNode = this.buildSliderNode(); 59 60 this.respNode = this.buildRespField(); 61 this.reloadNode = this.buildReloadNode(board, thread_id); 62 this.helpNode = this.buildHelpNode(); 63 this.msgNode = this.buildMsgNode(); 64 this.challengeNode = this.buildChallengeNode(); 65 66 el.appendChild(this.reloadNode); 67 el.appendChild(this.respNode); 68 el.appendChild(this.helpNode); 69 70 this.imgCntNode.appendChild(this.bgNode); 71 this.imgCntNode.appendChild(this.fgNode); 72 el.appendChild(this.imgCntNode); 73 74 el.appendChild(this.sliderNode); 75 el.appendChild(this.msgNode); 76 el.appendChild(this.challengeNode); 77 78 window.addEventListener('message', this.onFrameMessage); 79 }, 80 81 destroy: function() { 82 let self = TCaptcha; 83 84 if (!self.node) { 85 return; 86 } 87 88 window.removeEventListener('message', self.onFrameMessage); 89 90 clearTimeout(self.frameTimeout); 91 clearTimeout(self.reloadTimeout); 92 clearTimeout(self.expireTimeout); 93 94 self.node.textContent = ''; 95 96 self.node = null; 97 self.frameNode = null; 98 self.imgCntNode = null; 99 self.bgNode = null; 100 self.fgNode = null; 101 self.msgNode = null; 102 self.sliderNode = null; 103 self.respNode = null; 104 self.reloadNode = null; 105 self.helpNode = null; 106 self.challengeNode = null; 107 108 self.ticketCaptchaNode = null; 109 110 self.challenge = null; 111 112 self.pcdBypassable = false; 113 114 self.errorCb = null; 115 116 self.reloadTs = null; 117 118 self.onReloadCdDone = null; 119 }, 120 121 setErrorCb: function(func) { 122 TCaptcha.errorCb = func; 123 }, 124 125 toggleImgCntNode: function(flag) { 126 TCaptcha.imgCntNode.style.display = flag ? 'block' : 'none'; 127 }, 128 129 getTicket: function() { 130 return localStorage.getItem(TCaptcha.ticketKey); 131 }, 132 133 setTicket: function(val) { 134 if (val) { 135 localStorage.setItem(TCaptcha.ticketKey, val); 136 } 137 else if (val === false) { 138 localStorage.removeItem(TCaptcha.ticketKey); 139 } 140 }, 141 142 buildFrameNode: function() { 143 let el = document.createElement('iframe'); 144 el.id = 't-frame'; 145 el.style.border = '0'; 146 el.style.width = '100%'; 147 el.style.height = '80px'; 148 el.style.marginTop = '2px'; 149 el.style.position = 'relative'; 150 el.style.display = 'block'; 151 el.onerror = TCaptcha.onFrameError; 152 return el; 153 }, 154 155 buildImgCntNode: function() { 156 let el = document.createElement('div'); 157 el.id = 't-cnt'; 158 el.style.height = '80px'; 159 el.style.marginTop = '2px'; 160 el.style.position = 'relative'; 161 return el; 162 }, 163 164 buildImgNode: function(id) { 165 let el = document.createElement('div'); 166 el.id = 't-' + id; 167 el.style.width = '100%'; 168 el.style.height = '100%'; 169 el.style.position = 'absolute'; 170 el.style.backgroundRepeat = 'no-repeat'; 171 el.style.backgroundPosition = 'top left'; 172 el.style.pointerEvents = 'none'; 173 return el; 174 }, 175 176 buildMsgNode: function() { 177 let el = document.createElement('div'); 178 el.id = 't-msg'; 179 el.style.width = '100%'; 180 el.style.height = 'calc(100% - 20px)'; 181 el.style.position = 'absolute'; 182 el.style.top = '20px'; 183 el.style.textAlign = 'center'; 184 el.style.fontSize = '12px'; 185 el.style.filter = 'inherit'; 186 el.style.display = 'none'; 187 el.style.alignContent = 'center'; 188 return el; 189 }, 190 191 buildRespField: function() { 192 let el = document.createElement('input'); 193 el.id = 't-resp'; 194 el.name = 't-response'; 195 el.placeholder = 'Type the CAPTCHA here'; 196 el.setAttribute('autocomplete', 'off'); 197 el.type = 'text'; 198 el.style.width = '160px'; 199 el.style.boxSizing = 'border-box'; 200 el.style.textTransform = 'uppercase'; 201 el.style.fontSize = '11px'; 202 el.style.height = '18px'; 203 el.style.margin = '0'; 204 el.style.padding = '0 2px'; 205 el.style.fontFamily = 'monospace'; 206 el.style.verticalAlign = 'middle'; 207 if (this.tabindex) { 208 el.setAttribute('tabindex', this.tabindex + 2); 209 } 210 return el; 211 }, 212 213 buildSliderNode: function() { 214 let el = document.createElement('input'); 215 el.id = 't-slider'; 216 el.setAttribute('autocomplete', 'off'); 217 el.type = 'range'; 218 el.style.width = '100%'; 219 el.style.boxSizing = 'border-box'; 220 el.style.visibility = 'hidden'; 221 el.style.margin = '0'; 222 el.style.transition = 'box-shadow 15s linear'; 223 el.style.boxShadow = '0 0 6px 4px #1d8dc4'; 224 el.style.position = 'relative'; 225 el.value = 0; 226 el.min = 0; 227 el.max = 100; 228 el.addEventListener('input', this.onSliderInput, false); 229 if (this.tabindex) { 230 el.setAttribute('tabindex', this.tabindex + 1); 231 } 232 return el; 233 }, 234 235 buildChallengeNode: function() { 236 let el = document.createElement('input'); 237 el.name = 't-challenge'; 238 el.type = 'hidden'; 239 return el; 240 }, 241 242 buildReloadNode: function(board, thread_id) { 243 let el = document.createElement('button'); 244 el.id = 't-load'; 245 el.type = 'button'; 246 el.style.fontSize = '11px'; 247 el.style.padding = '0'; 248 el.style.width = '90px'; 249 el.style.boxSizing = 'border-box'; 250 el.style.margin = '0 6px 0 0'; 251 el.style.verticalAlign = 'middle'; 252 el.style.height = '18px'; 253 el.textContent = 'Get Captcha'; 254 el.setAttribute('data-board', board); 255 el.setAttribute('data-tid', thread_id); 256 el.addEventListener('click', this.onReloadClick, false); 257 if (this.tabindex) { 258 el.setAttribute('tabindex', this.tabindex); 259 } 260 return el; 261 }, 262 263 buildHelpNode: function() { 264 let el = document.createElement('button'); 265 el.id = 't-help'; 266 el.type = 'button'; 267 el.style.fontSize = '11px'; 268 el.style.padding = '0'; 269 el.style.width = '20px'; 270 el.style.boxSizing = 'border-box'; 271 el.style.margin = '0 0 0 6px'; 272 el.style.verticalAlign = 'middle'; 273 el.style.height = '18px'; 274 el.textContent = '?'; 275 el.setAttribute('data-tip', 'Help'); 276 el.tabIndex = -1; 277 el.addEventListener('click', this.onHelpClick, false); 278 return el; 279 }, 280 281 onHelpClick: function() { 282 let str = `- Only type letters and numbers displayed in the image. 283 - If needed, use the slider to align the image to make it readable. 284 - Make sure to not block any cookies set by 4chan.`; 285 alert(str); 286 }, 287 288 onTicketCaptchaError: function() { 289 TCaptcha.toggleMsgOverlay(true, "Couldn't load the captcha.<br><br>Please check your browser's content blocker."); 290 }, 291 292 onTicketCaptchaDone: function(resp) { 293 TCaptcha.reloadNode.setAttribute('data-ticket-resp', resp); 294 TCaptcha.destroyTicketCaptcha(); 295 TCaptcha.onReloadClick(); 296 }, 297 298 loadTicketCaptcha: function() { 299 window.pcd_c_loaded = TCaptcha.buildTicketCaptcha; 300 window.pcd_c_done = TCaptcha.onTicketCaptchaDone; 301 TCaptcha.toggleMsgOverlay(true, 'Loading…'); 302 let s = document.createElement('script'); 303 s.src = 'https://js.hcaptcha.com/1/api.js?onload=pcd_c_loaded&render=explicit&recaptchacompat=off'; 304 s.onerror = TCaptcha.onTicketCaptchaError; 305 document.head.appendChild(s); 306 }, 307 308 buildTicketCaptcha: function() { 309 let self = TCaptcha; 310 311 self.toggleMsgOverlay(false); 312 313 if (!window.hcaptcha) { 314 self.loadTicketCaptcha(); 315 return; 316 } 317 318 let el = document.createElement('div'); 319 el.id = 't-tc-cnt'; 320 self.imgCntNode.appendChild(el); 321 322 let wid = window.hcaptcha.render('t-tc-cnt', { 323 sitekey: self.hCaptchaSiteKey, 324 callback: 'pcd_c_done' 325 }); 326 327 el.setAttribute('data-wid', wid); 328 329 self.ticketCaptchaNode = el; 330 }, 331 332 destroyTicketCaptcha: function() { 333 let self = TCaptcha; 334 335 if (!window.hcaptcha || !self.ticketCaptchaNode) { 336 return; 337 } 338 339 let wid = self.ticketCaptchaNode.getAttribute('data-wid'); 340 window.hcaptcha.reset(wid); 341 self.imgCntNode.removeChild(self.ticketCaptchaNode); 342 self.ticketCaptchaNode = null; 343 }, 344 345 onReloadClick: function() { 346 let btn = TCaptcha.reloadNode; 347 let board = btn.getAttribute('data-board'); 348 let thread_id = btn.getAttribute('data-tid'); 349 let ticket_resp = btn.getAttribute('data-ticket-resp'); 350 btn.removeAttribute('data-ticket-resp'); 351 TCaptcha.toggleReloadBtn(false, 'Loading'); 352 TCaptcha.load(board, thread_id, ticket_resp); 353 }, 354 355 onFrameMessage: function(e) { 356 if (e.origin !== `https://sys.${TCaptcha.domain}`) { 357 return; 358 } 359 360 if (e.data && e.data.twister) { 361 TCaptcha.destroyFrame(); 362 TCaptcha.buildFromJson(e.data.twister); 363 } 364 }, 365 366 onFrameError: function(e) { 367 TCaptcha.unlockReloadBtn(); 368 369 console.log(e); 370 371 if (TCaptcha.errorCb) { 372 TCaptcha.errorCb.call(null, 373 "Couldn't load the captcha frame. Check your content blocker settings." 374 ); 375 } 376 }, 377 378 load: function(board, thread_id, ticket_resp) { 379 let self = TCaptcha; 380 381 clearTimeout(self.frameTimeout); 382 clearTimeout(self.reloadTimeout); 383 clearTimeout(self.expireTimeout); 384 385 let params = ['framed=1']; 386 387 if (board) { 388 params.push('board=' + board); 389 } 390 391 if (thread_id > 0) { 392 params.push('thread_id=' + thread_id); 393 } 394 395 if (ticket_resp) { 396 params.push('ticket_resp=' + encodeURIComponent(ticket_resp)); 397 } 398 399 let ticket = self.getTicket(); 400 401 if (ticket) { 402 params.push('ticket=' + ticket); 403 } 404 405 if (params.length > 0) { 406 params = '?' + params.join('&'); 407 } 408 409 let src = 'https://sys.' + self.domain + self.path + params; 410 411 self.frameNode = self.buildFrameNode(); 412 self.toggleImgCntNode(false); 413 self.node.insertBefore(self.frameNode, self.imgCntNode); 414 self.frameTimeout = setTimeout(self.onFrameTimeout, 60000, src); 415 self.frameNode.src = src; 416 }, 417 418 onFrameTimeout: function(src) { 419 let self = TCaptcha; 420 421 self.destroyFrame(); 422 423 console.log('Captcha frame timeout'); 424 425 if (self.errorCb) { 426 self.errorCb.call(null, `Couldn't get the captcha. 427 Make sure your browser doesn't block content on 4chan then click 428 <a href="${src.replace('framed=1', 'opened=1')}" target="_blank">here</a>.`); 429 } 430 }, 431 432 destroyFrame: function() { 433 let self = TCaptcha; 434 435 clearTimeout(self.frameTimeout); 436 self.frameTimeout = null; 437 if (self.frameNode) { 438 self.frameNode.remove(); 439 self.frameNode = null; 440 } 441 self.toggleImgCntNode(true); 442 self.unlockReloadBtn(); 443 }, 444 445 unlockReloadBtn: function() { 446 TCaptcha.reloadTs = null; 447 TCaptcha.toggleReloadBtn(true, 'Get Captcha'); 448 }, 449 450 toggleReloadBtn: function(flag, label) { 451 let self = TCaptcha; 452 453 if (self.reloadNode) { 454 self.reloadNode.disabled = !flag; 455 456 if (label !== undefined) { 457 self.reloadNode.textContent = label; 458 } 459 } 460 }, 461 462 onCaptchaFailed: function() { 463 let self = TCaptcha; 464 465 let cd = self.failCd * 1000; 466 467 if (self.reloadTs && self.reloadTs < cd) { 468 self.setReloadCd(cd, true); 469 } 470 }, 471 472 setReloadCd: function(cd, visible, onDone) { 473 let self = TCaptcha; 474 475 if (!self.node) { 476 return; 477 } 478 479 clearTimeout(self.reloadTimeout); 480 481 self.onReloadCdDone = onDone; 482 483 self.pcdBypassable = visible === -1; 484 485 if (cd) { 486 self.toggleReloadBtn(false); 487 if (visible) { 488 self.reloadTs = Date.now() + cd; 489 self.onReloadCdTick(); 490 } 491 else { 492 self.reloadTimeout = setTimeout(self.stopReloadCd, cd); 493 } 494 } 495 else { 496 self.stopReloadCd(); 497 } 498 }, 499 500 stopReloadCd: function() { 501 let self = TCaptcha; 502 self.unlockReloadBtn(); 503 if (self.onReloadCdDone) { 504 self.onReloadCdDone.call(self); 505 } 506 }, 507 508 onReloadCdTick: function() { 509 let self = TCaptcha; 510 511 if (!self.reloadNode || !self.reloadTs) { 512 return; 513 } 514 515 let cd = self.reloadTs - Date.now(); 516 517 if (self.pcdBypassable) { 518 if (document.cookie.indexOf('_ev1=') !== -1) { 519 cd = 0; 520 } 521 } 522 523 if (cd > 0) { 524 self.reloadNode.textContent = Math.ceil(cd / 1000); 525 self.reloadTimeout = setTimeout(self.onReloadCdTick, Math.min(cd, 1000)); 526 } 527 else { 528 self.stopReloadCd(); 529 } 530 }, 531 532 clearChallenge: function() { 533 let self = TCaptcha; 534 535 if (self.node) { 536 self.challengeNode.value = ''; 537 self.respNode.value = ''; 538 self.fgNode.style.backgroundImage = ''; 539 self.bgNode.style.backgroundImage = ''; 540 self.toggleSlider(false); 541 self.toggleMsgOverlay(false); 542 } 543 }, 544 545 toggleSlider: function(flag) { 546 TCaptcha.sliderNode.style.visibility = flag ? '' : 'hidden'; 547 TCaptcha.sliderNode.style.boxShadow = flag ? '' : '0 0 4px 2px #1d8dc4'; 548 }, 549 550 toggleMsgOverlay: function(flag, txt) { 551 if (txt !== undefined) { 552 TCaptcha.msgNode.innerHTML = `<div>${txt}</div>`; 553 } 554 TCaptcha.msgNode.style.display = flag ? 'grid' : 'none'; 555 }, 556 557 onSliderInput: function() { 558 var m = -Math.floor((+this.value) / 100 * this.twisterDelta); 559 TCaptcha.bgNode.style.backgroundPositionX = m + 'px'; 560 }, 561 562 onTicketPcdTick: function() { 563 let self = TCaptcha; 564 565 let el = document.getElementById('t-pcd'); 566 567 if (!el) { 568 return; 569 } 570 571 let pcd = +el.getAttribute('data-pcd'); 572 573 pcd = pcd - (0 | (Date.now() / 1000)); 574 575 if (pcd <= 0) { 576 self.onTicketPcdEnd(); 577 return; 578 } 579 580 el.textContent = pcd; 581 582 setTimeout(self.updateTicketPcd, 1000); 583 }, 584 585 clearTicketOverlay: function() { 586 TCaptcha.toggleMsgOverlay(false); 587 }, 588 589 buildFromJson: function(data) { 590 let self = TCaptcha; 591 592 if (!self.node) { 593 return; 594 } 595 596 self.unlockReloadBtn(); 597 self.toggleSlider(false); 598 self.toggleMsgOverlay(false); 599 600 self.setTicket(data.ticket); 601 602 if (TCaptcha.errorCb) { 603 TCaptcha.errorCb.call(null, ''); 604 } 605 606 if (data.cd) { 607 self.setReloadCd(data.cd * 1000, !data.challenge); 608 } 609 610 if (data.mpcd) { 611 self.clearChallenge(); 612 self.destroyTicketCaptcha(); 613 self.buildTicketCaptcha(); 614 return; 615 } 616 617 if (data.pcd) { 618 self.buildTicket(data); 619 return; 620 } 621 622 if (data.error) { 623 console.log(data.error); 624 625 if (TCaptcha.errorCb) { 626 TCaptcha.errorCb.call(null, data.error); 627 } 628 629 return; 630 } 631 632 self.imgCntNode.style.width = data.img_width + 'px'; 633 self.imgCntNode.style.height = data.img_height + 'px'; 634 635 self.challengeNode.value = data.challenge; 636 637 self.expireTimeout = setTimeout(self.clearChallenge, data.ttl * 1000 - 3000); 638 639 if (data.bg_width) { 640 self.buildTwister(data); 641 } 642 else if (data.img) { 643 self.buildStatic(data); 644 } 645 else { 646 self.buildNoop(data); 647 } 648 }, 649 650 buildTwister: function(data) { 651 let self = TCaptcha; 652 653 self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; 654 self.bgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.bg + ')'; 655 656 self.bgNode.style.backgroundPositionX = '0px'; 657 658 self.toggleSlider(true); 659 self.sliderNode.value = 0; 660 self.sliderNode.twisterDelta = data.bg_width - data.img_width; 661 self.sliderNode.focus(); 662 }, 663 664 buildStatic: function(data) { 665 let self = TCaptcha; 666 self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; 667 self.bgNode.style.backgroundImage = ''; 668 }, 669 670 buildTicket: function(data) { 671 let self = TCaptcha; 672 self.toggleMsgOverlay(true, data.pcd_msg || 'Please wait a while.'); 673 self.fgNode.style.backgroundImage = ''; 674 self.bgNode.style.backgroundImage = ''; 675 self.setReloadCd(data.pcd * 1000, data.bpcd ? -1 : true, self.clearTicketOverlay); 676 }, 677 678 buildNoop: function(data) { 679 let self = TCaptcha; 680 self.toggleMsgOverlay(true, 'Verification not required.'); 681 self.fgNode.style.backgroundImage = ''; 682 self.bgNode.style.backgroundImage = ''; 683 } 684 };