diff --git a/plugins/hcaptcha/LICENSE b/plugins/hcaptcha/LICENSE new file mode 100644 index 0000000000..4aed64b3af --- /dev/null +++ b/plugins/hcaptcha/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 RainLoop Team + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/hcaptcha/README b/plugins/hcaptcha/README new file mode 100644 index 0000000000..1c27b710d3 --- /dev/null +++ b/plugins/hcaptcha/README @@ -0,0 +1,3 @@ +A CAPTCHA is a program that can generate and grade tests that humans can pass but current computer programs cannot. +For example, humans can read distorted text as the one shown below, but current computer programs can't. +More info at https://hcaptcha.com diff --git a/plugins/hcaptcha/VERSION b/plugins/hcaptcha/VERSION new file mode 100644 index 0000000000..d3827e75a5 --- /dev/null +++ b/plugins/hcaptcha/VERSION @@ -0,0 +1 @@ +1.0 diff --git a/plugins/hcaptcha/index.php b/plugins/hcaptcha/index.php new file mode 100644 index 0000000000..dc710aea69 --- /dev/null +++ b/plugins/hcaptcha/index.php @@ -0,0 +1,151 @@ +UseLangs(true); + + $this->addJs('js/hcaptcha.js'); + + $this->addHook('ajax.action-pre-call', 'AjaxActionPreCall'); + $this->addHook('filter.ajax-response', 'FilterAjaxResponse'); + } + + /** + * @return array + */ + public function configMapping() + { + return array( + \RainLoop\Plugins\Property::NewInstance('site_key')->SetLabel('Site key') + ->SetAllowedInJs(true) + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('secret_key')->SetLabel('Secret key') + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('theme')->SetLabel('Theme') + ->SetAllowedInJs(true) + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(array('light', 'dark')), + \RainLoop\Plugins\Property::NewInstance('error_limit')->SetLabel('Limit') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(array(0, 1, 2, 3, 4, 5)) + ->SetDescription('') + ); + } + + /** + * @return string + */ + private function getCaptchaCacherKey() + { + return 'CaptchaNew/Login/'.\RainLoop\Utils::GetConnectionToken(); + } + + /** + * @return int + */ + private function getLimit() + { + $iConfigLimit = $this->Config()->Get('plugin', 'error_limit', 0); + if (0 < $iConfigLimit) + { + $oCacher = $this->Manager()->Actions()->Cacher(); + $sLimit = $oCacher && $oCacher->IsInited() ? $oCacher->Get($this->getCaptchaCacherKey()) : '0'; + + if (0 < \strlen($sLimit) && \is_numeric($sLimit)) + { + $iConfigLimit -= (int) $sLimit; + } + } + + return $iConfigLimit; + } + + /** + * @return void + */ + public function FilterAppDataPluginSection($bAdmin, $bAuth, &$aData) + { + if (!$bAdmin && !$bAuth && \is_array($aData)) + { + $aData['show_captcha_on_login'] = 1 > $this->getLimit(); + } + } + + /** + * @param string $sAction + */ + public function AjaxActionPreCall($sAction) + { + if ('Login' === $sAction && 0 >= $this->getLimit()) + { + $bResult = false; + + $sResult = $this->Manager()->Actions()->Http()->SendPostRequest( + 'https://hcaptcha.com/siteverify', + array( + 'secret' => $this->Config()->Get('plugin', 'secret_key', ''), + 'response' => $this->Manager()->Actions()->GetActionParam('h-captcha-response', '') + ) + ); + + if ($sResult) + { + $aResp = @\json_decode($sResult, true); + if (\is_array($aResp) && isset($aResp['success']) && $aResp['success']) + { + $bResult = true; + } + } + + if (!$bResult) + { + $this->Manager()->Actions()->Logger()->Write('HcaptchaResponse:'.$sResult); + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CaptchaError); + } + } + } + + /** + * @param string $sAction + * @param array $aResponseItem + */ + public function FilterAjaxResponse($sAction, &$aResponseItem) + { + if ('Login' === $sAction && $aResponseItem && isset($aResponseItem['Result'])) + { + $oCacher = $this->Manager()->Actions()->Cacher(); + $iConfigLimit = (int) $this->Config()->Get('plugin', 'error_limit', 0); + + $sKey = $this->getCaptchaCacherKey(); + + if (0 < $iConfigLimit && $oCacher && $oCacher->IsInited()) + { + if (false === $aResponseItem['Result']) + { + $iLimit = 0; + $sLimut = $oCacher->Get($sKey); + if (0 < \strlen($sLimut) && \is_numeric($sLimut)) + { + $iLimit = (int) $sLimut; + } + + $oCacher->Set($sKey, ++$iLimit); + + if ($iConfigLimit <= $iLimit) + { + $aResponseItem['Captcha'] = true; + } + } + else + { + $oCacher->Delete($sKey); + } + } + } + } +} diff --git a/plugins/hcaptcha/js/hcaptcha.js b/plugins/hcaptcha/js/hcaptcha.js new file mode 100644 index 0000000000..42b1a759d6 --- /dev/null +++ b/plugins/hcaptcha/js/hcaptcha.js @@ -0,0 +1,95 @@ +(function ($, window) { + + $(function () { + + var + nId = null, + bStarted = false + ; + + function ShowHcaptcha() + { + if (window.hcaptcha && window.rl) + { + if (null === nId) + { + var + oEl = null, + oLink = $('.plugin-mark-Login-BottomControlGroup') + ; + + if (oLink && oLink[0]) + { + oEl = $('
'); + + $(oLink[0]).after(oEl); + + nId = window.hcaptcha.render(oEl[0], { + 'sitekey': window.rl.pluginSettingsGet('hcaptcha', 'site_key'), + 'theme': window.rl.pluginSettingsGet('hcaptcha', 'theme') + }); + } + } + } + } + + window.__globalShowHcaptcha = ShowHcaptcha; + + function StartHcaptcha() + { + if (!window.hcaptcha && window.rl) + { + $.getScript('https://hcaptcha.com/1/api.js?onload=__globalShowHcaptcha&render=explicit'); + } + else + { + ShowHcaptcha(); + } + } + + if (window.rl) + { + window.rl.addHook('user-login-submit', function (fSubmitResult) { + if (null !== nId && !window.hcaptcha.getResponse(nId)) + { + fSubmitResult(105); + } + }); + + window.rl.addHook('view-model-on-show', function (sName, oViewModel) { + if (!bStarted && oViewModel && + ('View:RainLoop:Login' === sName || 'View/App/Login' === sName || 'LoginViewModel' === sName || 'LoginAppView' === sName) && + window.rl.pluginSettingsGet('hcaptcha', 'show_captcha_on_login')) + { + bStarted = true; + StartHcaptcha(); + } + }); + + window.rl.addHook('ajax-default-request', function (sAction, oParameters) { + if ('Login' === sAction && oParameters && null !== nId && window.hcaptcha) + { + oParameters['h-captcha-response'] = window.hcaptcha.getResponse(nId); + } + }); + + window.rl.addHook('ajax-default-response', function (sAction, oData, sType) { + if ('Login' === sAction) + { + if (!oData || 'success' !== sType || !oData['Result']) + { + if (null !== nId && window.hcaptcha) + { + window.hcaptcha.reset(nId); + } + else if (oData && oData['Captcha']) + { + StartHcaptcha(); + } + } + } + }); + } + }); + +}($, window));