Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added self-hosted GDPR compliant captcha module #109

Open
wants to merge 55 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d6fe6d5
start
fballiano Feb 17, 2025
a7aadac
start
fballiano Feb 17, 2025
0ff78d3
start
fballiano Feb 17, 2025
6b81dd7
work
fballiano Feb 17, 2025
51ee3b3
Merge branch main
fballiano Feb 17, 2025
93d2411
hidelogo
fballiano Feb 17, 2025
5379772
it works!!
fballiano Feb 17, 2025
3c6af3c
rector
fballiano Feb 17, 2025
5caa2e2
copyright checker should not check xml files
fballiano Feb 17, 2025
f577e1a
it does not work on backend login
fballiano Feb 17, 2025
99ac604
local copy of altcha
fballiano Feb 17, 2025
ef17e64
local copy of altcha
fballiano Feb 17, 2025
6788e5e
1.2.0
fballiano Feb 20, 2025
1f5195f
suggestion
fballiano Feb 22, 2025
b5c9c44
enabled floating interface
fballiano Feb 22, 2025
b00a0b2
whiteline
fballiano Feb 22, 2025
88fceab
updates
fballiano Feb 22, 2025
d98af62
updates
fballiano Feb 22, 2025
7d70e7e
Merge branch 'main' into captcha
fballiano Feb 24, 2025
93b55ff
using events to solve the single catcha per page limitation
fballiano Feb 24, 2025
3fd3f28
added expiration
fballiano Feb 24, 2025
e3da37b
work
fballiano Feb 24, 2025
e2ec66e
added replay attack prevention
fballiano Feb 24, 2025
109ec41
PHPCS
fballiano Feb 24, 2025
c7c76af
enabled
fballiano Feb 24, 2025
d7b4d55
merged main branch
fballiano Feb 25, 2025
a89b480
Merge branch 'main' into captcha
fballiano Feb 26, 2025
0b398d9
fix backend login
fballiano Feb 26, 2025
33eaa34
composer update
fballiano Feb 28, 2025
5aef971
Merge branch main
fballiano Mar 3, 2025
09e4663
cutoff date lowered to 1 day
fballiano Mar 4, 2025
b936f20
Renamed maho_captcha to captcha
fballiano Mar 4, 2025
d35b88b
Renamed maho_captcha to captcha
fballiano Mar 4, 2025
f887cae
Renamed 2.0.0 since the old mage captcha_setup was already 1.7
fballiano Mar 4, 2025
d35bd65
Created an upgrade script too
fballiano Mar 4, 2025
5159a2a
Renamed maho_captcha to captcha
fballiano Mar 4, 2025
3a9c1ec
Merge branch 'main' into captcha
fballiano Mar 5, 2025
f217d73
Merge branch 'main' into captcha
fballiano Mar 5, 2025
35c0975
Merge branch 'main' into captcha
fballiano Mar 5, 2025
fdee7d2
work
fballiano Mar 6, 2025
60a569d
Caching verification results, in cache verify gets called multiple ti…
fballiano Mar 7, 2025
4f4a164
External JS
justinbeaty Mar 8, 2025
e610ee1
phpcs
fballiano Mar 8, 2025
57b7dbf
composer update
fballiano Mar 9, 2025
eb30d9e
Renamed mahorecaptcha to mahocaptcha
fballiano Mar 9, 2025
1ed8100
probably it was some first test
fballiano Mar 9, 2025
59991e7
last line
fballiano Mar 9, 2025
c2e3eb7
reimplemented auto validation and lazy load
fballiano Mar 9, 2025
a9d1ee7
re-separated frontend and backend stuff
fballiano Mar 9, 2025
69754ff
defer loading on maho-captcha
fballiano Mar 9, 2025
d987e88
not necessary anymore
fballiano Mar 9, 2025
f5aa4f9
return should return a string
fballiano Mar 9, 2025
b149231
not necessary anymore
fballiano Mar 9, 2025
4327712
not used
fballiano Mar 9, 2025
8d3ccbd
Does this work?
justinbeaty Mar 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/copyright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:

# Only check files with specific extensions
extension="${file##*.}"
if ! [[ "${extension,,}" =~ ^(css|js|php|phtml|template|xml)$ ]]; then
if ! [[ "${extension,,}" =~ ^(css|js|php|phtml)$ ]]; then
continue
fi

Expand Down
129 changes: 129 additions & 0 deletions app/code/core/Maho/Captcha/Helper/Data.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

/**
* Maho
*
* @package Maho_Captcha
* @copyright Copyright (c) 2025 Maho (https://mahocommerce.com)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

use AltchaOrg\Altcha;

class Maho_Captcha_Helper_Data extends Mage_Core_Helper_Abstract
{
public const XML_PATH_ENABLED = 'admin/captcha/enabled';
public const XML_PATH_FRONTEND_SELECTORS = 'admin/captcha/selectors';

protected $_moduleName = 'Maho_Captcha';

/** @var array <string, bool> */
protected static array $_payloadVerificationCache = [];

public function isEnabled(): bool
{
return $this->isModuleEnabled() && $this->isModuleOutputEnabled() && Mage::getStoreConfigFlag(self::XML_PATH_ENABLED);
}

public function getTableName(): string
{
return Mage::getSingleton('core/resource')->getTableName('captcha/challenge');
}

public function getHmacKey(): string
{
return (string) Mage::getConfig()->getNode('global/crypt/key');
}

public function getFrontendSelectors(): string
{
$selectors = Mage::getStoreConfig(self::XML_PATH_FRONTEND_SELECTORS) ?? '';
$selectors = trim($selectors);
$selectors = str_replace(["\r\n", "\r"], "\n", $selectors);
$selectors = explode("\n", $selectors);

$selectorsToKeep = [];
foreach ($selectors as $selector) {
$selector = trim($selector);
if (strlen($selector) && !str_starts_with($selector, '//')) {
$selectorsToKeep[] = $selector;
}
}

return implode(',', $selectorsToKeep);
}

public function getChallengeUrl(): string
{
return Mage::getUrl('captcha/index/challenge');
}

public function getWidgetAttributes(): Varien_Object
{
return new Varien_Object([
'challengeurl' => $this->getChallengeUrl(),
'name' => 'maho_captcha',
'auto' => 'onload',
'hidelogo' => '',
'hidefooter' => '',
'refetchonexpire' => '',
]);
}

public function createChallenge(?array $options = null): Altcha\Challenge
{
$options = new Altcha\ChallengeOptions([
'algorithm' => Altcha\Algorithm::SHA512,
'saltLength' => 32,
'expires' => (new DateTime())->modify('+1 minute'),
'hmacKey' => $this->getHmacKey(),
...($options ?? []),
]);
return Altcha\Altcha::createChallenge($options);
}

public function verify(string $payload): bool
{
if (empty($payload)) {
return false;
}

if (isset(self::$_payloadVerificationCache[$payload])) {
return self::$_payloadVerificationCache[$payload];
}

// Check that the challenge is not stored in the database, meaning it was already solved
$coreRead = Mage::getSingleton('core/resource')->getConnection('core_read');
$select = $coreRead->select()
->from($this->getTableName(), ['challenge'])
->where('challenge = ?', $payload);
if ($coreRead->fetchOne($select)) {
return false;
}

try {
$isValid = Altcha\Altcha::verifySolution($payload, $this->getHmacKey(), true);
$this->logChallenge($payload);
} catch (Exception $e) {
$isValid = false;
Mage::logException($e);
}

self::$_payloadVerificationCache[$payload] = $isValid;
return $isValid;
}

protected function logChallenge(string $payload): void
{
try {
Mage::getSingleton('core/resource')
->getConnection('core_write')
->insert($this->getTableName(), [
'challenge' => $payload,
'created_at' => Mage::getModel('core/date')->gmtDate('Y-m-d H:i:s'),
]);
} catch (Exception $e) {
Mage::logException($e);
}
}
}
17 changes: 17 additions & 0 deletions app/code/core/Maho/Captcha/Model/Challenge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* Maho
*
* @package Maho_Captcha
* @copyright Copyright (c) 2025 Maho (https://mahocommerce.com)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Maho_Captcha_Model_Challenge extends Mage_Core_Model_Abstract
{
protected function _construct()
{
$this->_init('captcha/challenge');
}
}
113 changes: 113 additions & 0 deletions app/code/core/Maho/Captcha/Model/Observer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

/**
* Maho
*
* @package Maho_Captcha
* @copyright Copyright (c) 2025 Maho (https://mahocommerce.com)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Maho_Captcha_Model_Observer
{
public function verify(Varien_Event_Observer $observer): void
{
$helper = Mage::helper('captcha');
if (!$helper->isEnabled()) {
return;
}

/** @var Mage_Core_Controller_Front_Action $controller */
$controller = $observer->getControllerAction();
$data = $controller->getRequest()->getPost();

$token = $data['maho_captcha'] ?? '';
if ($helper->verify((string) $token)) {
return;
}

$this->failedVerification($controller);
}

public function verifyAjax(Varien_Event_Observer $observer): void
{
$helper = Mage::helper('captcha');
if (!$helper->isEnabled()) {
return;
}

/** @var Mage_Core_Controller_Front_Action $controller */
$controller = $observer->getControllerAction();
if ($controller->getFullActionName() == 'checkout_onepage_saveBilling' &&
Mage::getSingleton('customer/session')->isLoggedIn()) {
return;
}

$data = $controller->getRequest()->getPost();
$token = $data['maho_captcha'] ?? '';
if ($helper->verify((string) $token)) {
return;
}

$this->failedVerification($controller, true);
}

public function verifyAdmin(Varien_Event_Observer $observer): void
{
$helper = Mage::helper('captcha');
if (!$helper->isEnabled()) {
return;
}

$request = Mage::app()->getRequest();
if ($request->getActionName() == 'prelogin' || !$request->isPost()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this line...

return;
}

$data = $request->getPost();
$token = $data['maho_captcha'] ?? '';

if ($helper->verify((string) $token)) {
return;
}

Mage::throwException(Mage::helper('captcha')->__('Incorrect CAPTCHA.'));
}

protected function failedVerification(Mage_Core_Controller_Front_Action $controller, bool $isAjax = false): void
{
$controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true);
$errorMessage = Mage::helper('captcha')->__('Incorrect CAPTCHA.');

if ($isAjax) {
$result = ['error' => 1, 'message' => $errorMessage];
$controller->getResponse()->setBodyJson($result);
return;
}

Mage::getSingleton('core/session')->addError($errorMessage);
$request = $controller->getRequest();
$refererUrl = $request->getServer('HTTP_REFERER');
if ($url = $request->getParam(Mage_Core_Controller_Varien_Action::PARAM_NAME_REFERER_URL)) {
$refererUrl = $url;
} elseif ($url = $request->getParam(Mage_Core_Controller_Varien_Action::PARAM_NAME_BASE64_URL)) {
$refererUrl = Mage::helper('core')->urlDecodeAndEscape($url);
} elseif ($url = $request->getParam(Mage_Core_Controller_Varien_Action::PARAM_NAME_URL_ENCODED)) {
$refererUrl = Mage::helper('core')->urlDecodeAndEscape($url);
}
$controller->getResponse()->setRedirect($refererUrl);
}

public function cleanup()
{
$resource = Mage::getSingleton('core/resource');
$connection = $resource->getConnection('core_write');
$table = $resource->getTableName('captcha/challenge');

$cutOffDate = Mage::getModel('core/date')->gmtDate('Y-m-d H:i:s', strtotime('-1 day'));
$connection->delete(
$table,
['created_at < ?' => $cutOffDate],
);
}
}
17 changes: 17 additions & 0 deletions app/code/core/Maho/Captcha/Model/Resource/Challenge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* Maho
*
* @package Maho_Captcha
* @copyright Copyright (c) 2025 Maho (https://mahocommerce.com)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Maho_Captcha_Model_Resource_Challenge extends Mage_Core_Model_Resource_Db_Abstract
{
protected function _construct()
{
$this->_init('captcha/challenge', 'challenge');
}
}
32 changes: 32 additions & 0 deletions app/code/core/Maho/Captcha/controllers/IndexController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* Maho
*
* @package Maho_Captcha
* @copyright Copyright (c) 2025 Maho (https://mahocommerce.com)
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

class Maho_Captcha_IndexController extends Mage_Core_Controller_Front_Action
{
public function challengeAction()
{
$helper = Mage::helper('captcha');
try {
if (!$helper->isEnabled()) {
Mage::throwException($helper->__('Captcha is disabled'));
}
$this->getResponse()->setBodyJson($helper->createChallenge());
} catch (Mage_Core_Exception $e) {
$error = $e->getMessage();
} catch (Exception $e) {
$error = $helper->__('Internal Error');
}
if (isset($error)) {
$this->getResponse()
->setHttpResponseCode(400)
->setBodyJson(['error' => true, 'message' => $error]);
}
}
}
Loading
Loading