diff --git a/administrator/components/com_admin/sql/updates/mysql/5.1.0-2024-02-08.sql b/administrator/components/com_admin/sql/updates/mysql/5.1.0-2024-02-08.sql new file mode 100644 index 000000000000..12f1961de85f --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/5.1.0-2024-02-08.sql @@ -0,0 +1,2 @@ +INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `custom_data`, `ordering`, `state`) VALUES +(0, 'plg_captcha_math', 'plugin', 'math', 'captcha', 0, 0, 1, 0, 1, '', '{}', '', 1, 0); diff --git a/administrator/components/com_admin/sql/updates/postgresql/5.1.0-2024-02-08.sql b/administrator/components/com_admin/sql/updates/postgresql/5.1.0-2024-02-08.sql new file mode 100644 index 000000000000..39fe17af4eb2 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/5.1.0-2024-02-08.sql @@ -0,0 +1,2 @@ +INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "locked", "manifest_cache", "params", "custom_data", "ordering", "state") VALUES +(0, 'plg_captcha_math', 'plugin', 'math', 'captcha', 0, 0, 1, 0, 1, '', '{}', '', 1, 0); diff --git a/administrator/language/en-GB/plg_captcha_math.ini b/administrator/language/en-GB/plg_captcha_math.ini new file mode 100644 index 000000000000..5b24bc7c51a1 --- /dev/null +++ b/administrator/language/en-GB/plg_captcha_math.ini @@ -0,0 +1,11 @@ +; Joomla! Project +; (C) 2024 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_CAPTCHA_MATH="Captcha - Math" +PLG_CAPTCHA_MATH_XML_DESCRIPTION="A simple Captcha plugin that uses Math." + +PLG_CAPTCHA_MATH_EMPTY_STORE="Captcha Math solution not found, please make sure you did not submit the same form twice." +PLG_CAPTCHA_MATH_ENTER_SOLUTION="Enter solution (all or remaining digits) for" +PLG_CAPTCHA_MATH_WRONG_SOLUTION="Incorrect math solution for the Captcha." diff --git a/administrator/language/en-GB/plg_captcha_math.sys.ini b/administrator/language/en-GB/plg_captcha_math.sys.ini new file mode 100644 index 000000000000..acb32bd18aac --- /dev/null +++ b/administrator/language/en-GB/plg_captcha_math.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2024 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_CAPTCHA_MATH="Captcha - Math" +PLG_CAPTCHA_MATH_XML_DESCRIPTION="A simple Captcha plugin that uses Math." diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index c34c49dd41e0..30af27adb3d3 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -264,6 +264,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_behaviour_compat', 'plugin', 'compat', 'behaviour', 0, 1, 1, 0, 1, '', '{"classes_aliases":"1","es5_assets":"1"}', '', 1, 0), (0, 'plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 2, 0), (0, 'plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 3, 0), +(0, 'plg_captcha_math', 'plugin', 'math', 'captcha', 0, 0, 1, 0, 1, '', '{}', '', 1, 0), (0, 'plg_content_confirmconsent', 'plugin', 'confirmconsent', 'content', 0, 0, 1, 0, 1, '', '{}', '', 1, 0), (0, 'plg_content_contact', 'plugin', 'contact', 'content', 0, 1, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_content_emailcloak', 'plugin', 'emailcloak', 'content', 0, 1, 1, 0, 1, '', '{"mode":"1"}', '', 3, 0), diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 435afc0ee211..4a0a9cad89d5 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -270,6 +270,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_behaviour_compat', 'plugin', 'compat', 'behaviour', 0, 1, 1, 0, 1, '', '{"classes_aliases":"1","es5_assets":"1"}', '', 1, 0), (0, 'plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 2, 0), (0, 'plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, 1, '', '{}', '', 3, 0), +(0, 'plg_captcha_math', 'plugin', 'math', 'captcha', 0, 0, 1, 0, 1, '', '{}', '', 1, 0), (0, 'plg_content_confirmconsent', 'plugin', 'confirmconsent', 'content', 0, 0, 1, 0, 1, '', '{}', '', 1, 0), (0, 'plg_content_contact', 'plugin', 'contact', 'content', 0, 1, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_content_emailcloak', 'plugin', 'emailcloak', 'content', 0, 1, 1, 0, 1, '', '{"mode":"1"}', '', 3, 0), diff --git a/layouts/plugins/captcha/math/mathcaptcha.php b/layouts/plugins/captcha/math/mathcaptcha.php new file mode 100644 index 000000000000..b01735f6b416 --- /dev/null +++ b/layouts/plugins/captcha/math/mathcaptcha.php @@ -0,0 +1,52 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Document\Document; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\FileLayout; + +extract($displayData); + +/** + * Layout variables + * ----------------- + * @var string $name Name of the input field. + * @var array $attributes List of input attributes + * @var string $formula Formula + * @var integer $inputIdx Active Input index + * @var Document $document Document instance + * @var FileLayout $this Context + */ + +$id = $attributes['id'] ?? ''; +$class = str_replace(' required', '', ($attributes['class'] ?? '')); + +$letters = 'abcdefghijklmnopqrstuvwxyz'; +$classes = [ + substr(str_shuffle($letters), 0, 2) . bin2hex(random_bytes(rand(2, 7))), + substr(str_shuffle($letters), 0, 2) . bin2hex(random_bytes(rand(2, 7))), + substr(str_shuffle($letters), 0, 2) . bin2hex(random_bytes(rand(2, 7))), +]; + +$styles = ''; +foreach ($classes as $i => $c) { + $styles .= '.' . $c . '{display:' . ($i !== $inputIdx ? 'none' : 'block') . '}'; +} + +$document->getWebAssetManager()->addInlineStyle($styles, ['name' => 'inline.plg_captcha_math']) +?> +
+ escape($formula); ?> + + + +
diff --git a/libraries/src/Captcha/Captcha.php b/libraries/src/Captcha/Captcha.php index a5b673de074a..7ea09df01629 100644 --- a/libraries/src/Captcha/Captcha.php +++ b/libraries/src/Captcha/Captcha.php @@ -193,7 +193,7 @@ public function display($name, $id, $class = '') /** * Checks if the answer is correct. * - * @param string $code The answer. + * @param mixed $code The answer. * * @return bool Whether the provided answer was correct * @@ -203,6 +203,9 @@ public function display($name, $id, $class = '') public function checkAnswer($code) { if ($this->provider) { + if (!is_scalar($code)) { + $code = json_encode($code); + } return $this->provider->checkAnswer($code); } diff --git a/plugins/captcha/math/math.xml b/plugins/captcha/math/math.xml new file mode 100644 index 000000000000..e06a4121ab44 --- /dev/null +++ b/plugins/captcha/math/math.xml @@ -0,0 +1,22 @@ + + + PLG_CAPTCHA_MATH + 5.1.0 + 2024-02 + Joomla! Project + admin@joomla.org + www.joomla.org + (C) 2024 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + PLG_CAPTCHA_MATH_XML_DESCRIPTION + Joomla\Plugin\Captcha\Math + + services + src + + + language/en-GB/plg_captcha_math.ini + language/en-GB/plg_captcha_math.sys.ini + + + diff --git a/plugins/captcha/math/services/provider.php b/plugins/captcha/math/services/provider.php new file mode 100644 index 000000000000..63f57d3fe295 --- /dev/null +++ b/plugins/captcha/math/services/provider.php @@ -0,0 +1,46 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +\defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\Captcha\Math\Extension\Math; + +return new class () implements ServiceProviderInterface { + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function register(Container $container) + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new Math( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('captcha', 'math') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/plugins/captcha/math/src/Extension/Math.php b/plugins/captcha/math/src/Extension/Math.php new file mode 100644 index 000000000000..94bcfb5ae8c6 --- /dev/null +++ b/plugins/captcha/math/src/Extension/Math.php @@ -0,0 +1,57 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\Captcha\Math\Extension; + +use Joomla\CMS\Event\Captcha\CaptchaSetupEvent; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\SubscriberInterface; +use Joomla\Plugin\Captcha\Math\Provider\MathCaptchaProvider; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Math captcha Plugin + * + * @since __DEPLOY_VERSION__ + */ +final class Math extends CMSPlugin implements SubscriberInterface +{ + /** + * Returns an array of events this plugin will listen to. + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onCaptchaSetup' => 'onCaptchaSetup', + ]; + } + + /** + * Register Captcha instance + * + * @param CaptchaSetupEvent $event + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function onCaptchaSetup(CaptchaSetupEvent $event) + { + $this->loadLanguage(); + $event->getCaptchaRegistry()->add(new MathCaptchaProvider($this->getApplication())); + } +} diff --git a/plugins/captcha/math/src/Provider/MathCaptchaProvider.php b/plugins/captcha/math/src/Provider/MathCaptchaProvider.php new file mode 100644 index 000000000000..24d451325021 --- /dev/null +++ b/plugins/captcha/math/src/Provider/MathCaptchaProvider.php @@ -0,0 +1,218 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\Captcha\Math\Provider; + +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Captcha\CaptchaProviderInterface; +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Math captcha Provider + * + * @since __DEPLOY_VERSION__ + */ +final class MathCaptchaProvider implements CaptchaProviderInterface +{ + /** + * @var CMSApplicationInterface + * + * @since __DEPLOY_VERSION__ + */ + protected $app; + + /** + * Math formula + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected string $formula = ''; + + /** + * Index of active input + * + * @var int + * + * @since __DEPLOY_VERSION__ + */ + protected int $inputIdx = 0; + + /** + * Session key, to store result + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected string $sessionKey = 'plg_captcha_math'; + + /** + * Class constructor + * + * @param CMSApplicationInterface $app + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(CMSApplicationInterface $app) + { + $this->app = $app; + } + + /** + * Return Captcha name, CMD string. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return 'math'; + } + + /** + * Render the captcha input + * + * @param string $name Input name given in the form + * @param array $attributes The class of the field and other attributes, from the form. + * + * @return string The HTML to be embedded in the form + * + * @throws \RuntimeException + * + * @since __DEPLOY_VERSION__ + */ + public function display(string $name = '', array $attributes = []): string + { + // Prepare the numbers and store the result. + // They are the same for all captcha on the page, because browser can submit only 1 form at a time. + if (!$this->formula) { + $this->createQuiz(); + } + + return LayoutHelper::render( + 'plugins.captcha.math.mathcaptcha', + [ + 'name' => $name, + 'attributes' => $attributes, + 'formula' => $this->formula, + 'inputIdx' => $this->inputIdx, + 'document' => $this->app->getDocument(), + ], + null, + ['component' => 'none'] + ); + } + + /** + * Prepare the quiz + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function createQuiz() + { + // 2 or 3 + if (rand(1, 2) === 2) { + $numbers = [rand(110, 980), rand(1, 9)]; + $solution = array_sum($numbers); + } else { + $numbers = [rand(10, 90), rand(1, 9)]; + $solution = array_sum($numbers); + } + + // Full or half + if (rand(1, 2) === 2) { + $numbers[] = (int) substr($solution, 0, 1); + $solution = (int) substr($solution, 1); + $this->formula = sprintf('%d + %d = %d', ...$numbers); + } else { + $this->formula = sprintf('%d + %d =', ...$numbers); + } + + $this->inputIdx = rand(0, 2); + + $this->app->getSession()->set($this->sessionKey . '.pwd', $solution); + $this->app->getSession()->set($this->sessionKey . '.secret', $this->inputIdx); + } + + /** + * Validate the input data + * + * @param ?string $code Answer provided by user + * + * @return bool If the answer is correct, false otherwise + * + * @throws \RuntimeException + * + * @since __DEPLOY_VERSION__ + */ + public function checkAnswer(string $code = null): bool + { + $code = $code ? json_decode($code, true) : false; + if (!$code) { + return false; + } + + // Get a real solution from session, and compare with answer + $solution = (int) $this->app->getSession()->get($this->sessionKey . '.pwd'); + $inputIdx = (int) $this->app->getSession()->get($this->sessionKey . '.secret'); + + if (!$solution || $inputIdx < 0 || $inputIdx > 2) { + throw new \RuntimeException(Text::_('PLG_CAPTCHA_MATH_EMPTY_STORE')); + } + + // Clean stored value to prevent repetitive form submission + $this->app->getSession()->set($this->sessionKey, null); + + // Check for correct response + $isOk = !empty($code[$inputIdx]) && $solution === (int) $code[$inputIdx]; + unset($code[$inputIdx]); + + foreach ($code as $r) { + if (!$isOk) { + break; + } + $isOk = $r === ''; + } + + return $isOk; + } + + /** + * Method to react on the setup of a captcha field. Gives the possibility + * to change the field and/or the XML element for the field. + * + * @param FormField $field Captcha field instance + * @param \SimpleXMLElement $element XML form definition + * + * @return void + * + * @throws \RuntimeException + * + * @since __DEPLOY_VERSION__ + */ + public function setupField(FormField $field, \SimpleXMLElement $element): void + { + // Custom message + if (!$element['message']) { + $element['message'] = 'PLG_CAPTCHA_MATH_WRONG_SOLUTION'; + } + } +}