Skip to content

Commit e49f269

Browse files
authored
Merge pull request #75 from monarc-project/feature/captcha
Captcha
2 parents 6da3378 + 7248f81 commit e49f269

10 files changed

+234
-4
lines changed

composer.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@
6060
"symfony/console": "^5.0",
6161
"guzzlehttp/guzzle": "^6.5",
6262
"phpoffice/phpword": "^0.18.1",
63-
"laminas/laminas-mvc-middleware": "^2.2"
63+
"laminas/laminas-mvc-middleware": "^2.2",
64+
"laminas/laminas-captcha": "^2.18"
6465
},
6566
"require-dev": {
66-
"roave/security-advisories": "dev-master"
67+
"roave/security-advisories": "dev-latest"
6768
},
6869
"autoload": {
6970
"psr-4": {

config/module.config.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@
4545
return [
4646
'router' => [
4747
'routes' => [
48+
'captcha' => [
49+
'type' => 'segment',
50+
'options' => [
51+
'route' => '/api/captcha',
52+
'defaults' => [
53+
'controller' => PipeSpec::class,
54+
'middleware' => new PipeSpec(Controller\ApiCaptchaController::class),
55+
],
56+
],
57+
],
4858
'monarc_api_admin_users_roles' => [
4959
'type' => 'segment',
5060
'options' => [
@@ -1475,6 +1485,7 @@
14751485
DeprecatedTable\RecordTable::class => AutowireFactory::class,
14761486
DeprecatedTable\QuestionTable::class => AutowireFactory::class,
14771487
DeprecatedTable\QuestionChoiceTable::class => AutowireFactory::class,
1488+
Table\ActionHistoryTable::class => ClientEntityManagerFactory::class,
14781489
Table\AnrTable::class => ClientEntityManagerFactory::class,
14791490
Table\AnrInstanceMetadataFieldTable::class => ClientEntityManagerFactory::class,
14801491
Table\AmvTable::class => ClientEntityManagerFactory::class,
@@ -1613,7 +1624,8 @@
16131624
AdapterAuthentication::class => static function (ContainerInterface $container) {
16141625
return new AdapterAuthentication(
16151626
$container->get(Table\UserTable::class),
1616-
$container->get(ConfigService::class)
1627+
$container->get(ConfigService::class),
1628+
$container->get(Service\ActionHistoryService::class),
16171629
);
16181630
},
16191631
ConnectedUserService::class => static function (ContainerInterface $container) {
@@ -1794,6 +1806,9 @@
17941806
'routes' => [],
17951807
],
17961808
],
1809+
'permissions' => [
1810+
'captcha',
1811+
],
17971812
'roles' => [
17981813
// Super Admin : Management of users (and guides, models, referentials, etc.)
17991814
Entity\UserRole::SUPER_ADMIN_FO => [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @link https://github.com/monarc-project for the canonical source repository
4+
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
5+
* @license MONARC is licensed under GNU Affero General Public License version 3
6+
*/
7+
8+
use Phinx\Migration\AbstractMigration;
9+
10+
class CreateActionsHistoryTable extends AbstractMigration
11+
{
12+
public function change()
13+
{
14+
$this->execute(
15+
'CREATE TABLE IF NOT EXISTS `actions_history` (
16+
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
17+
`user_id` int(11) unsigned DEFAULT NULL,
18+
`action` varchar(100) NOT NULL,
19+
`data` TEXT,
20+
`status` smallint(3) unsigned NOT NULL DEFAULT 0,
21+
`creator` varchar(255) NOT NULL,
22+
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
23+
PRIMARY KEY (`id`),
24+
CONSTRAINT `actions_history_user_id_id_fk1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL);'
25+
);
26+
}
27+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @link https://github.com/monarc-project for the canonical source repository
4+
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
5+
* @license MONARC is licensed under GNU Affero General Public License version 3
6+
*/
7+
8+
namespace Monarc\FrontOffice\Controller;
9+
10+
use Monarc\Core\Controller\Handler\AbstractRestfulControllerRequestHandler;
11+
use Monarc\Core\Controller\Handler\ControllerRequestResponseHandlerTrait;
12+
use Monarc\FrontOffice\Service\CaptchaService;
13+
14+
class ApiCaptchaController extends AbstractRestfulControllerRequestHandler
15+
{
16+
use ControllerRequestResponseHandlerTrait;
17+
18+
public function __construct(private CaptchaService $captchaService)
19+
{
20+
}
21+
22+
public function getList()
23+
{
24+
if ($this->captchaService->isActivated()) {
25+
return $this->getPreparedJsonResponse(
26+
array_merge(['isCaptchaActivated' => true], $this->captchaService->generate())
27+
);
28+
}
29+
30+
return $this->getPreparedJsonResponse(['isCaptchaActivated' => false]);
31+
}
32+
33+
/**
34+
* @param array $data
35+
*/
36+
public function create($data)
37+
{
38+
return $this->getSuccessfulJsonResponse(
39+
['isCaptchaValid' => $this->captchaService->isValid($data['captchaId'], $data['captchaInput'])]
40+
);
41+
}
42+
}

src/Controller/ApiConfigController.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ public function __construct(private ConfigService $configService)
1919

2020
public function getList()
2121
{
22+
$isExportDefaultWithEval = $this->configService->getConfigOption('export', [])['defaultWithEval'] ?? false;
23+
2224
return new JsonModel(array_merge(
2325
$this->configService->getLanguage(),
2426
$this->configService->getAppVersion(),
2527
$this->configService->getCheckVersion(),
2628
$this->configService->getAppCheckingURL(),
2729
$this->configService->getMospApiUrl(),
28-
$this->configService->getTerms()
30+
$this->configService->getTerms(),
31+
['isExportDefaultWithEval' => $isExportDefaultWithEval],
2932
));
3033
}
3134
}

src/Entity/ActionHistory.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @link https://github.com/monarc-project for the canonical source repository
4+
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
5+
* @license MONARC is licensed under GNU Affero General Public License version 3
6+
*/
7+
8+
namespace Monarc\FrontOffice\Entity;
9+
10+
use Doctrine\ORM\Mapping as ORM;
11+
use Monarc\Core\Entity\ActionHistorySuperClass;
12+
13+
/**
14+
* @ORM\Table(name="actions_history", indexes={
15+
* @ORM\Index(name="action", columns={"action"}),
16+
* })
17+
* @ORM\Entity
18+
*/
19+
class ActionHistory extends ActionHistorySuperClass
20+
{
21+
}

src/Service/ActionHistoryService.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @link https://github.com/monarc-project for the canonical source repository
4+
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
5+
* @license MONARC is licensed under GNU Affero General Public License version 3
6+
*/
7+
8+
namespace Monarc\FrontOffice\Service;
9+
10+
use Monarc\Core\Service\ActionHistoryService as CoreActionHistoryService;
11+
use Monarc\FrontOffice\Table\ActionHistoryTable;
12+
13+
class ActionHistoryService extends CoreActionHistoryService
14+
{
15+
public function __construct(ActionHistoryTable $actionHistoryTable)
16+
{
17+
parent::__construct($actionHistoryTable);
18+
}
19+
}

src/Service/AnrObjectService.php

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public function createList(Entity\Anr $anr, array $data): array
161161
$object = $this->create($anr, $objectData, false);
162162
$createdObjectsUuids[] = $object->getUuid();
163163
}
164+
$this->monarcObjectTable->flush();
164165

165166
return $createdObjectsUuids;
166167
}

src/Service/CaptchaService.php

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @link https://github.com/monarc-project for the canonical source repository
4+
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
5+
* @license MONARC is licensed under GNU Affero General Public License version 3
6+
*/
7+
8+
namespace Monarc\FrontOffice\Service;
9+
10+
use Laminas\Captcha\Image as CaptchaImage;
11+
use Laminas\Session\Container;
12+
use Monarc\Core\Entity\ActionHistorySuperClass;
13+
use Monarc\Core\Service\ConfigService;
14+
15+
final class CaptchaService
16+
{
17+
private const DEFAULT_FAILED_LOGIN_ATTEMPTS = 3;
18+
19+
private array $params;
20+
21+
public function __construct(private ActionHistoryService $actionHistoryService, ConfigService $config)
22+
{
23+
$this->params = $config->getCaptchaConfig();
24+
}
25+
26+
public function isActivated(): bool
27+
{
28+
$isEnabled = (bool)($this->params['enabled'] ?? false);
29+
if (!$isEnabled) {
30+
return false;
31+
}
32+
33+
$failedLoginAttemptsLimit = (int)($this->params['failedLoginAttempts'] ?? self::DEFAULT_FAILED_LOGIN_ATTEMPTS);
34+
if ($failedLoginAttemptsLimit > 0) {
35+
/* Login attempt number validation from the logs. */
36+
$lastLoginsHistory = $this->actionHistoryService->getActionsHistoryByAction(
37+
ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT,
38+
$failedLoginAttemptsLimit
39+
);
40+
if (\count($lastLoginsHistory) < $failedLoginAttemptsLimit) {
41+
return false;
42+
}
43+
foreach ($lastLoginsHistory as $lastLoginHistory) {
44+
if ($lastLoginHistory->getStatus() === ActionHistorySuperClass::STATUS_SUCCESS) {
45+
return false;
46+
}
47+
}
48+
}
49+
50+
return true;
51+
}
52+
53+
public function generate(): array
54+
{
55+
$captcha = new CaptchaImage($this->params['params']);
56+
57+
$captchaId = $captcha->generate();
58+
59+
/* Store the captcha ID for validation. */
60+
$session = new Container('captcha');
61+
$session->offsetSet('captchaId', $captchaId);
62+
63+
return [
64+
'captchaId' => $captchaId,
65+
'captchaUrl' => $captcha->getImgUrl() . $captcha->getId() . $captcha->getSuffix(),
66+
];
67+
}
68+
69+
public function isValid(string $captchaId, string $inputText): bool
70+
{
71+
$captcha = new CaptchaImage($this->params['params']);
72+
73+
$session = new Container('captcha');
74+
$storedCaptchaId = $session->offsetGet('captchaId');
75+
if ($captchaId !== $storedCaptchaId) {
76+
return false;
77+
}
78+
79+
return $captcha->isValid(['input' => $inputText, 'id' => $storedCaptchaId]);
80+
}
81+
}

src/Table/ActionHistoryTable.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* @link https://github.com/monarc-project for the canonical source repository
4+
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
5+
* @license MONARC is licensed under GNU Affero General Public License version 3
6+
*/
7+
8+
namespace Monarc\FrontOffice\Table;
9+
10+
use Doctrine\ORM\EntityManager;
11+
use Monarc\Core\Table\ActionHistoryTable as CoreActionHistoryTable;
12+
use Monarc\FrontOffice\Entity\ActionHistory;
13+
14+
class ActionHistoryTable extends CoreActionHistoryTable
15+
{
16+
public function __construct(EntityManager $entityManager, string $entityName = ActionHistory::class)
17+
{
18+
parent::__construct($entityManager, $entityName);
19+
}
20+
}

0 commit comments

Comments
 (0)