Skip to content

Commit

Permalink
MVP
Browse files Browse the repository at this point in the history
  • Loading branch information
ticktackk committed Apr 15, 2020
1 parent 356413e commit c081db1
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 0 deletions.
205 changes: 205 additions & 0 deletions Captcha/hCaptcha.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

namespace TickTackk\hCaptchaIntegration\Captcha;

use XF\App as BaseApp;
use XF\Captcha\AbstractCaptcha;
use XF\Template\Templater;
use GuzzleHttp\Exception\RequestException as GuzzleHttpRequestException;
use ArrayObject;
use XF\Http\Request as HttpRequest;
use XF\SubContainer\Http as HttpSubContainer;
use GuzzleHttp\Client as GuzzleHttpClient;

/**
* Class hCaptcha
*
* @package TickTackk\hCaptchaIntegration\Captcha
*/
class hCaptcha extends AbstractCaptcha
{
/**
* hCAPTCHA site key
*
* @var null|string
*/
protected $siteKey = null;

/**
* hCAPTCHA secret key
*
* @var null|string
*/
protected $secretKey = null;

/**
* Enable hCAPTCHA invisible mode
*
* @var bool
*/
protected $invisibleMode = false;

/**
* hCaptcha constructor.
*
* @param BaseApp $app
*/
public function __construct(BaseApp $app)
{
parent::__construct($app);

$extraKeys = $this->options()->extraCaptchaKeys;

if (!empty($extraKeys['tckHCaptchaSiteKey']) && !empty($extraKeys['tckHCaptchaSecretKey']))
{
$this->siteKey = $extraKeys['tckHCaptchaSiteKey'];
$this->secretKey = $extraKeys['tckHCaptchaSecretKey'];
}

if (!empty($extraKeys['tckHCaptchaInvisible']))
{
$this->invisibleMode = $extraKeys['tckHCaptchaInvisible'];
}
}

/**
* @return string|null
*/
protected function getSiteKey() :? string
{
return $this->siteKey;
}

/**
* @return string|null
*/
protected function getSecretKey() :? string
{
return $this->secretKey;
}

/**
* @return bool
*/
protected function isInvisibleMode() : bool
{
return $this->invisibleMode;
}

/**
* @return bool
*/
protected function isForcedVisible() : bool
{
return $this->forceVisible;
}

/**
* @param Templater $templater
*
* @return string
*/
public function renderInternal(Templater $templater) : string
{
$siteKey = $this->getSiteKey();
if (!$siteKey)
{
return '';
}

return $templater->renderTemplate('public:tckHCaptchaIntegration_captcha_hcaptcha', [
'siteKey' => $siteKey,
'invisible' => $this->isInvisibleMode() && !$this->isForcedVisible()
]);
}

public function isValid() : bool
{
$siteKey = $this->getSiteKey();
$secretKey = $this->getSecretKey();

if (!$siteKey || !$secretKey)
{
return true; // if not configured, always pass
}

$request = $this->request();
$captchaResponse = $request->filter('h-captcha-response', 'str');
if (!$captchaResponse)
{
return false;
}

try
{
$addOns = $this->app->container('addon.cache');
$addOnVersion = $addOns['TickTackk\hCaptchaIntegration'] ?? 0 >= 1000011;

$response = $this->httpClient()->post('https://hcaptcha.com/siteverify', [
'form_params' => [
'secret' => $secretKey,
'response' => $captchaResponse,
'remoteip' => $request->getIp()
],
'headers' => [
'XF-TCK-ADDON-VER' => $addOnVersion
]
])->getBody()->getContents();
$response = \GuzzleHttp\json_decode($response, true);

if (isset($response['success']) && isset($response['hostname']) && $response['hostname'] == $request->getHost())
{
return $response['success'];
}

return false;
}
catch(GuzzleHttpRequestException $e)
{
// this is an exception with the underlying request, so let it go through
\XF::logException($e, false, 'hCAPTCHA connection error: ');

return true;
}
}

/**
* @return BaseApp
*/
protected function app() : BaseApp
{
return $this->app;
}

/**
* @return ArrayObject
*/
protected function options() : ArrayObject
{
return $this->app()->options();
}

/**
* @return HttpRequest
*/
protected function request() : HttpRequest
{
return $this->app()->request();
}

/**
* @return HttpSubContainer
*/
protected function http() : HttpSubContainer
{
return $this->app()->http();
}

/**
* @return GuzzleHttpClient
*/
protected function httpClient() : GuzzleHttpClient
{
return $this->http()->client();
}
}
144 changes: 144 additions & 0 deletions _files/js/ticktackk/hcaptchaintegration/captcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
var TickTackk = window.TickTackk || {};
TickTackk.hCaptchaIntegration = TickTackk.hCaptchaIntegration || {};

!function($, window, document, _undefined)
{
"use strict";

TickTackk.hCaptchaIntegration.hCaptcha = XF.Element.newHandler({

options: {
sitekey: null,
invisible: null
},

$hCaptchaTarget: null,

hCaptchaId: null,
invisibleValidated: false,
reloading: false,

init: function()
{
if (!this.options.sitekey)
{
return;
}

var $form = this.$target.closest('form');

if (this.options.invisible)
{
var $hCaptchaTarget = $('<div />'),
$formRow = this.$target.closest('.formRow');

$formRow.hide();
$formRow.after($hCaptchaTarget);
this.$hCaptchaTarget = $hCaptchaTarget;

$form.on('ajax-submit:before', XF.proxy(this, 'beforeSubmit'));
}
else
{
this.$hCaptchaTarget = this.$target;
}

$form.on('ajax-submit:error ajax-submit:always', XF.proxy(this, 'reload'));

if (window.hcaptcha)
{
this.create();
}
else
{
TickTackk.hCaptchaIntegration.Callbacks.push(XF.proxy(this, 'create'));

$.ajax({
url: 'https://hcaptcha.com/1/api.js?onload=TickTackkhCaptchaIntegrationCallback&render=explicit',
dataType: 'script',
cache: true,
global: false
});
}
},

create: function()
{
if (!window.hcaptcha)
{
return;
}

var options = {
sitekey: this.options.sitekey
};
if (this.options.invisible)
{
options.size = 'invisible';
options.callback = XF.proxy(this, 'complete');

}
this.hCaptchaId = hcaptcha.render(this.$hCaptchaTarget[0], options);
},

/**
* @param {Event} e
* @param {Object} config
*/
beforeSubmit: function(e, config)
{
if (!this.invisibleValidated)
{
e.preventDefault();
config.preventSubmit = true;

hcaptcha.execute();
}
},

complete: function()
{
this.invisibleValidated = true;
this.$target.closest('form').submit();
},

reload: function()
{
if (!window.hcaptcha || this.hCaptchaId === null || this.reloading)
{
return;
}

this.reloading = true;

var self = this;
setTimeout(function()
{
hcaptcha.reset(self.hCaptchaId);
self.reloading = false;
self.invisibleValidated = false;
}, 50);
}
});

TickTackk.hCaptchaIntegration.Callbacks = [];

/**
*
* This is the callback for hCatcha
*
* @constructor
*/
window.TickTackkhCaptchaIntegrationCallback = function()
{
var cb = TickTackk.hCaptchaIntegration.Callbacks;

for (var i = 0; i < cb.length; i++)
{
cb[i]();
}
};

XF.Element.register('tck-hcaptcha-integration-h-captcha', 'TickTackk.hCaptchaIntegration.hCaptcha');
}
(jQuery, window, document);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You would need to get your own API keys from <a href="https://dashboard.hcaptcha.com/" target="_blank">hCaptcha dashboard</a> and enter them below.
1 change: 1 addition & 0 deletions _output/phrases/tckHCaptchaIntegration_use_hcaptcha.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use hCAPTCHA
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use invisible hCAPTCHA
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"template": "option_template_captcha",
"description": "Add hCaptcha in available captcha list",
"execution_order": 10,
"enabled": true,
"action": "str_replace",
"find": "<!--[XF:captcha_after_recaptcha]-->",
"replace": "$0\n\n<xf:option value=\"TickTackk\\hCaptchaIntegration:hCaptcha\" data-hide=\"true\">\n\t<xf:label>{{ phrase('tckHCaptchaIntegration_use_hcaptcha') }}</xf:label>\n\t<xf:hint>{{ phrase('tckHCaptchaIntegration_hcaptcha_config_hint') }}</xf:hint>\n\t<xf:dependent>\n\t\t<div>{{ phrase('site_key:') }}</div>\n\t\t<xf:textbox name=\"{$extraKeysInput}[tckHCaptchaSiteKey]\" value=\"{$xf.options.extraCaptchaKeys.tckHCaptchaSiteKey}\" />\n\t</xf:dependent>\n\t<xf:dependent>\n\t\t<div>{{ phrase('secret_key:') }}</div>\n\t\t<xf:textbox name=\"{$extraKeysInput}[tckHCaptchaSecretKey]\" value=\"{$xf.options.extraCaptchaKeys.tckHCaptchaSecretKey}\" />\n\t</xf:dependent>\n\t<xf:dependent>\n\t\t<xf:checkbox>\n\t\t\t<xf:option name=\"{$extraKeysInput}[tckHCaptchaInvisible]\" selected=\"{$xf.options.extraCaptchaKeys.tckHCaptchaInvisible}\">{{ phrase('tckHCaptchaIntegration_use_invisible_hcaptcha') }}</xf:option>\n\t\t</xf:checkbox>\n\t</xf:dependent>\n</xf:option>"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<xf:js src="ticktackk/hcaptchaintegration/captcha.js" addon="TickTackk/hCaptchaIntegration" min="1" />

<div data-xf-init="tck-hcaptcha-integration-h-captcha"
data-sitekey="{$siteKey}"
data-theme="{{ property('styleType') }}"
data-invisible="{$invisible}">
</div>
6 changes: 6 additions & 0 deletions build.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"additional_files": [
"js/ticktackk/hcaptchaintegration/"
],
"minify": [
"js/ticktackk/hcaptchaintegration/captcha.js"
],
"exec": [
"composer install --working-dir=_build/upload/src/addons/{addon_id}/ --no-dev --optimize-autoloader"
],
Expand Down

0 comments on commit c081db1

Please sign in to comment.