<?php
/**
* @package Joomla.Plugin
* @subpackage Captcha.recaptcha
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\Captcha\ReCaptcha\Extension;
use Joomla\CMS\Application\CMSWebApplicationInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\DispatcherInterface;
use Joomla\Utilities\IpHelper;
use ReCaptcha\ReCaptcha as Captcha;
use ReCaptcha\RequestMethod;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Recaptcha Plugin
* Based on the official recaptcha library( https://packagist.org/packages/google/recaptcha )
*
* @since 2.5
*/
final class ReCaptcha extends CMSPlugin
{
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 3.1
*/
protected $autoloadLanguage = true;
/**
* The http request method
*
* @var RequestMethod
* @since 4.3.0
*/
private $requestMethod;
/**
* Constructor.
*
* @param DispatcherInterface $dispatcher The dispatcher
* @param array $config An optional associative array of configuration settings
* @param RequestMethod $requestMethod The http request method
*
* @since 4.3.0
*/
public function __construct(DispatcherInterface $dispatcher, array $config, RequestMethod $requestMethod)
{
parent::__construct($dispatcher, $config);
$this->requestMethod = $requestMethod;
}
/**
* Reports the privacy related capabilities for this plugin to site administrators.
*
* @return array
*
* @since 3.9.0
*/
public function onPrivacyCollectAdminCapabilities()
{
return [
$this->getApplication()->getLanguage()->_('PLG_CAPTCHA_RECAPTCHA') => [
$this->getApplication()->getLanguage()->_('PLG_RECAPTCHA_PRIVACY_CAPABILITY_IP_ADDRESS'),
],
];
}
/**
* Initializes the captcha
*
* @param string $id The id of the field.
*
* @return Boolean True on success, false otherwise
*
* @since 2.5
* @throws \RuntimeException
*/
public function onInit($id = 'dynamic_recaptcha_1')
{
$app = $this->getApplication();
if (!$app instanceof CMSWebApplicationInterface) {
return false;
}
$pubkey = $this->params->get('public_key', '');
if ($pubkey === '') {
throw new \RuntimeException($app->getLanguage()->_('PLG_RECAPTCHA_ERROR_NO_PUBLIC_KEY'));
}
$apiSrc = 'https://www.google.com/recaptcha/api.js?onload=JoomlainitReCaptcha2&render=explicit&hl='
. $app->getLanguage()->getTag();
// Load assets, the callback should be first
$app->getDocument()->getWebAssetManager()
->registerAndUseScript('plg_captcha_recaptcha', 'plg_captcha_recaptcha/recaptcha.min.js', [], ['defer' => true])
->registerAndUseScript('plg_captcha_recaptcha.api', $apiSrc, [], ['defer' => true], ['plg_captcha_recaptcha']);
return true;
}
/**
* Gets the challenge HTML
*
* @param string $name The name of the field. Not Used.
* @param string $id The id of the field.
* @param string $class The class of the field.
*
* @return string The HTML to be embedded in the form.
*
* @since 2.5
*/
public function onDisplay($name = null, $id = 'dynamic_recaptcha_1', $class = '')
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$ele = $dom->createElement('div');
$ele->setAttribute('id', $id);
$ele->setAttribute('class', ((trim($class) == '') ? 'g-recaptcha' : ($class . ' g-recaptcha')));
$ele->setAttribute('data-sitekey', $this->params->get('public_key', ''));
$ele->setAttribute('data-theme', $this->params->get('theme2', 'light'));
$ele->setAttribute('data-size', $this->params->get('size', 'normal'));
$ele->setAttribute('data-tabindex', $this->params->get('tabindex', '0'));
$ele->setAttribute('data-callback', $this->params->get('callback', ''));
$ele->setAttribute('data-expired-callback', $this->params->get('expired_callback', ''));
$ele->setAttribute('data-error-callback', $this->params->get('error_callback', ''));
$dom->appendChild($ele);
return $dom->saveHTML($ele);
}
/**
* Calls an HTTP POST function to verify if the user's guess was correct
*
* @param string $code Answer provided by user. Not needed for the Recaptcha implementation
*
* @return True if the answer is correct, false otherwise
*
* @since 2.5
* @throws \RuntimeException
*/
public function onCheckAnswer($code = null)
{
$input = $this->getApplication()->getInput();
$privatekey = $this->params->get('private_key');
$version = $this->params->get('version', '2.0');
$remoteip = IpHelper::getIp();
$response = null;
$spam = false;
switch ($version) {
case '2.0':
$response = $code ?: $input->get('g-recaptcha-response', '', 'string');
$spam = ($response === '');
break;
}
// Check for Private Key
if (empty($privatekey)) {
throw new \RuntimeException($this->getApplication()->getLanguage()->_('PLG_RECAPTCHA_ERROR_NO_PRIVATE_KEY'), 500);
}
// Check for IP
if (empty($remoteip)) {
throw new \RuntimeException($this->getApplication()->getLanguage()->_('PLG_RECAPTCHA_ERROR_NO_IP'), 500);
}
// Discard spam submissions
if ($spam) {
throw new \RuntimeException($this->getApplication()->getLanguage()->_('PLG_RECAPTCHA_ERROR_EMPTY_SOLUTION'), 500);
}
return $this->getResponse($privatekey, $remoteip, $response);
}
/**
* Get the reCaptcha response.
*
* @param string $privatekey The private key for authentication.
* @param string $remoteip The remote IP of the visitor.
* @param string $response The response received from Google.
*
* @return bool True if response is good | False if response is bad.
*
* @since 3.4
* @throws \RuntimeException
*/
private function getResponse(string $privatekey, string $remoteip, string $response)
{
$version = $this->params->get('version', '2.0');
switch ($version) {
case '2.0':
$apiResponse = (new Captcha($privatekey, $this->requestMethod))->verify($response, $remoteip);
if (!$apiResponse->isSuccess()) {
foreach ($apiResponse->getErrorCodes() as $error) {
throw new \RuntimeException($error, 403);
}
return false;
}
break;
}
return true;
}
}