/ js / tcaptcha.js
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  };