diff --git a/composer.json b/composer.json
index d6c655b60..4a69398e7 100644
--- a/composer.json
+++ b/composer.json
@@ -32,6 +32,7 @@
"drupal/metatag": "^2.2.0",
"drupal/paragraphs": "^1.19.0",
"drupal/pathauto": "^1.14.0",
+ "drupal/recaptcha": "^3.4",
"drupal/redirect": "^1.12.0",
"drupal/rename_admin_paths": "^3.0@beta",
"drupal/securitytxt": "^1.4",
diff --git a/composer.lock b/composer.lock
index bf42ffd41..0fc62aae3 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d3ce2f58155f0d32b6f34a731a4b4d89",
+ "content-hash": "5018a5fd0e2f8034548d15826c72b4ec",
"packages": [
{
"name": "asm89/stack-cors",
@@ -1218,6 +1218,86 @@
"issues": "https://www.drupal.org/project/issues/admin_toolbar"
}
},
+ {
+ "name": "drupal/captcha",
+ "version": "2.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://git.drupalcode.org/project/captcha.git",
+ "reference": "2.0.9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://ftp.drupal.org/files/projects/captcha-2.0.9.zip",
+ "reference": "2.0.9",
+ "shasum": "15b2ba18fab75ea88bfa8f75fb1be09f7cd52cbb"
+ },
+ "require": {
+ "drupal/core": "^9.5 || ^10 || ^11"
+ },
+ "type": "drupal-module",
+ "extra": {
+ "drupal": {
+ "version": "2.0.9",
+ "datestamp": "1753701287",
+ "security-coverage": {
+ "status": "covered",
+ "message": "Covered by Drupal's security advisory policy"
+ }
+ },
+ "branch-alias": {
+ "dev-8.x-1.x": "1.x-dev"
+ }
+ },
+ "notification-url": "https://packages.drupal.org/8/downloads",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "anybody",
+ "homepage": "https://www.drupal.org/user/291091"
+ },
+ {
+ "name": "elachlan",
+ "homepage": "https://www.drupal.org/user/1021502"
+ },
+ {
+ "name": "grevil",
+ "homepage": "https://www.drupal.org/user/3668491"
+ },
+ {
+ "name": "japerry",
+ "homepage": "https://www.drupal.org/user/45640"
+ },
+ {
+ "name": "naveenvalecha",
+ "homepage": "https://www.drupal.org/user/2665733"
+ },
+ {
+ "name": "podarok",
+ "homepage": "https://www.drupal.org/user/116002"
+ },
+ {
+ "name": "robloach",
+ "homepage": "https://www.drupal.org/user/61114"
+ },
+ {
+ "name": "thomas.frobieter",
+ "homepage": "https://www.drupal.org/user/409335"
+ },
+ {
+ "name": "wundo",
+ "homepage": "https://www.drupal.org/user/25523"
+ }
+ ],
+ "description": "The CAPTCHA module provides this feature to virtually any user facing web form on a Drupal site.",
+ "homepage": "https://www.drupal.org/project/captcha",
+ "support": {
+ "source": "https://git.drupalcode.org/project/captcha",
+ "issues": "https://www.drupal.org/project/issues/captcha"
+ }
+ },
{
"name": "drupal/conditional_fields",
"version": "4.0.0-alpha6",
@@ -2537,6 +2617,93 @@
"documentation": "https://www.drupal.org/docs/8/modules/pathauto"
}
},
+ {
+ "name": "drupal/recaptcha",
+ "version": "3.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://git.drupalcode.org/project/recaptcha.git",
+ "reference": "8.x-3.4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://ftp.drupal.org/files/projects/recaptcha-8.x-3.4.zip",
+ "reference": "8.x-3.4",
+ "shasum": "95fa7ac5dd064ea6a1c14fc4881778bf68200598"
+ },
+ "require": {
+ "drupal/captcha": "^1.15 || ^2.0",
+ "drupal/core": "^10 || ^11",
+ "google/recaptcha": "^1.3"
+ },
+ "type": "drupal-module",
+ "extra": {
+ "drupal": {
+ "version": "8.x-3.4",
+ "datestamp": "1723563033",
+ "security-coverage": {
+ "status": "covered",
+ "message": "Covered by Drupal's security advisory policy"
+ }
+ }
+ },
+ "notification-url": "https://packages.drupal.org/8/downloads",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "hass",
+ "homepage": "https://www.drupal.org/u/hass"
+ },
+ {
+ "name": "See other contributors",
+ "homepage": "https://www.drupal.org/node/147903/committers"
+ },
+ {
+ "name": "diolan",
+ "homepage": "https://www.drupal.org/user/2336786"
+ },
+ {
+ "name": "hass",
+ "homepage": "https://www.drupal.org/user/85918"
+ },
+ {
+ "name": "id.medion",
+ "homepage": "https://www.drupal.org/user/2542592"
+ },
+ {
+ "name": "kim.pepper",
+ "homepage": "https://www.drupal.org/user/370574"
+ },
+ {
+ "name": "larowlan",
+ "homepage": "https://www.drupal.org/user/395439"
+ },
+ {
+ "name": "liam morland",
+ "homepage": "https://www.drupal.org/user/493050"
+ },
+ {
+ "name": "robloach",
+ "homepage": "https://www.drupal.org/user/61114"
+ },
+ {
+ "name": "wundo",
+ "homepage": "https://www.drupal.org/user/25523"
+ },
+ {
+ "name": "yseki",
+ "homepage": "https://www.drupal.org/user/1523064"
+ }
+ ],
+ "description": "Protect your website from spam and abuse while letting real people pass through with ease.",
+ "homepage": "https://www.drupal.org/project/recaptcha",
+ "support": {
+ "source": "https://git.drupalcode.org/project/recaptcha.git",
+ "issues": "https://www.drupal.org/project/issues/recaptcha"
+ }
+ },
{
"name": "drupal/redirect",
"version": "1.12.0",
@@ -3429,6 +3596,58 @@
},
"time": "2025-08-12T10:13:48+00:00"
},
+ {
+ "name": "google/recaptcha",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/google/recaptcha.git",
+ "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/google/recaptcha/zipball/56522c261d2e8c58ba416c90f81a4cd9f2ed89b9",
+ "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.14",
+ "php-coveralls/php-coveralls": "^2.5",
+ "phpunit/phpunit": "^10"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ReCaptcha\\": "src/ReCaptcha"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
+ "homepage": "https://www.google.com/recaptcha/",
+ "keywords": [
+ "Abuse",
+ "captcha",
+ "recaptcha",
+ "spam"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/forum/#!forum/recaptcha",
+ "issues": "https://github.com/google/recaptcha/issues",
+ "source": "https://github.com/google/recaptcha"
+ },
+ "time": "2025-06-26T22:21:57+00:00"
+ },
{
"name": "grasmash/expander",
"version": "3.0.1",
@@ -8179,7 +8398,7 @@
},
"prefer-stable": true,
"prefer-lowest": false,
- "platform": [],
- "platform-dev": [],
+ "platform": {},
+ "platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php
index 51e734a77..6d29bff66 100644
--- a/vendor/composer/InstalledVersions.php
+++ b/vendor/composer/InstalledVersions.php
@@ -32,6 +32,11 @@ class InstalledVersions
*/
private static $installed;
+ /**
+ * @var bool
+ */
+ private static $installedIsLocalDir;
+
/**
* @var bool|null
*/
@@ -309,6 +314,12 @@ public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
+
+ // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+ // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+ // so we have to assume it does not, and that may result in duplicate data being returned when listing
+ // all installed packages for example
+ self::$installedIsLocalDir = false;
}
/**
@@ -322,19 +333,27 @@ private static function getInstalled()
}
$installed = array();
+ $copiedLocalDir = false;
if (self::$canGetVendors) {
+ $selfDir = strtr(__DIR__, '\\', '/');
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ $vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
$required = require $vendorDir.'/composer/installed.php';
- $installed[] = self::$installedByVendor[$vendorDir] = $required;
- if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
- self::$installed = $installed[count($installed) - 1];
+ self::$installedByVendor[$vendorDir] = $required;
+ $installed[] = $required;
+ if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+ self::$installed = $required;
+ self::$installedIsLocalDir = true;
}
}
+ if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+ $copiedLocalDir = true;
+ }
}
}
@@ -350,7 +369,7 @@ private static function getInstalled()
}
}
- if (self::$installed !== array()) {
+ if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
index 62ecfd8d0..f27399a04 100644
--- a/vendor/composer/LICENSE
+++ b/vendor/composer/LICENSE
@@ -1,3 +1,4 @@
+
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -17,3 +18,4 @@ 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/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
index d56cf1f52..67f8f6cff 100644
--- a/vendor/composer/autoload_psr4.php
+++ b/vendor/composer/autoload_psr4.php
@@ -43,6 +43,7 @@
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Symfony\\Bridge\\PsrHttpMessage\\' => array($vendorDir . '/symfony/psr-http-message-bridge'),
'Robo\\' => array($vendorDir . '/consolidation/robo/src'),
+ 'ReCaptcha\\' => array($vendorDir . '/google/recaptcha/src/ReCaptcha'),
'QueryPath\\' => array($vendorDir . '/gravitypdf/querypath/src'),
'QueryPathTests\\' => array($vendorDir . '/gravitypdf/querypath/tests/QueryPath'),
'Psy\\' => array($vendorDir . '/psy/psysh/src'),
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
index 378ce29d1..cb53dc6ef 100644
--- a/vendor/composer/autoload_static.php
+++ b/vendor/composer/autoload_static.php
@@ -86,6 +86,7 @@ class ComposerStaticInit969219525714a33a1696abd06bf370ac
'R' =>
array (
'Robo\\' => 5,
+ 'ReCaptcha\\' => 10,
),
'Q' =>
array (
@@ -316,6 +317,10 @@ class ComposerStaticInit969219525714a33a1696abd06bf370ac
array (
0 => __DIR__ . '/..' . '/consolidation/robo/src',
),
+ 'ReCaptcha\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/google/recaptcha/src/ReCaptcha',
+ ),
'QueryPath\\' =>
array (
0 => __DIR__ . '/..' . '/gravitypdf/querypath/src',
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index fda13e17f..a25241f5d 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -1265,6 +1265,89 @@
},
"install-path": "../../web/modules/contrib/admin_toolbar"
},
+ {
+ "name": "drupal/captcha",
+ "version": "2.0.9",
+ "version_normalized": "2.0.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://git.drupalcode.org/project/captcha.git",
+ "reference": "2.0.9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://ftp.drupal.org/files/projects/captcha-2.0.9.zip",
+ "reference": "2.0.9",
+ "shasum": "15b2ba18fab75ea88bfa8f75fb1be09f7cd52cbb"
+ },
+ "require": {
+ "drupal/core": "^9.5 || ^10 || ^11"
+ },
+ "type": "drupal-module",
+ "extra": {
+ "drupal": {
+ "version": "2.0.9",
+ "datestamp": "1753701287",
+ "security-coverage": {
+ "status": "covered",
+ "message": "Covered by Drupal's security advisory policy"
+ }
+ },
+ "branch-alias": {
+ "dev-8.x-1.x": "1.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "notification-url": "https://packages.drupal.org/8/downloads",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "anybody",
+ "homepage": "https://www.drupal.org/user/291091"
+ },
+ {
+ "name": "elachlan",
+ "homepage": "https://www.drupal.org/user/1021502"
+ },
+ {
+ "name": "grevil",
+ "homepage": "https://www.drupal.org/user/3668491"
+ },
+ {
+ "name": "japerry",
+ "homepage": "https://www.drupal.org/user/45640"
+ },
+ {
+ "name": "naveenvalecha",
+ "homepage": "https://www.drupal.org/user/2665733"
+ },
+ {
+ "name": "podarok",
+ "homepage": "https://www.drupal.org/user/116002"
+ },
+ {
+ "name": "robloach",
+ "homepage": "https://www.drupal.org/user/61114"
+ },
+ {
+ "name": "thomas.frobieter",
+ "homepage": "https://www.drupal.org/user/409335"
+ },
+ {
+ "name": "wundo",
+ "homepage": "https://www.drupal.org/user/25523"
+ }
+ ],
+ "description": "The CAPTCHA module provides this feature to virtually any user facing web form on a Drupal site.",
+ "homepage": "https://www.drupal.org/project/captcha",
+ "support": {
+ "source": "https://git.drupalcode.org/project/captcha",
+ "issues": "https://www.drupal.org/project/issues/captcha"
+ },
+ "install-path": "../../web/modules/contrib/captcha"
+ },
{
"name": "drupal/conditional_fields",
"version": "4.0.0-alpha6",
@@ -2637,6 +2720,96 @@
},
"install-path": "../../web/modules/contrib/pathauto"
},
+ {
+ "name": "drupal/recaptcha",
+ "version": "3.4.0",
+ "version_normalized": "3.4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://git.drupalcode.org/project/recaptcha.git",
+ "reference": "8.x-3.4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://ftp.drupal.org/files/projects/recaptcha-8.x-3.4.zip",
+ "reference": "8.x-3.4",
+ "shasum": "95fa7ac5dd064ea6a1c14fc4881778bf68200598"
+ },
+ "require": {
+ "drupal/captcha": "^1.15 || ^2.0",
+ "drupal/core": "^10 || ^11",
+ "google/recaptcha": "^1.3"
+ },
+ "type": "drupal-module",
+ "extra": {
+ "drupal": {
+ "version": "8.x-3.4",
+ "datestamp": "1723563033",
+ "security-coverage": {
+ "status": "covered",
+ "message": "Covered by Drupal's security advisory policy"
+ }
+ }
+ },
+ "installation-source": "dist",
+ "notification-url": "https://packages.drupal.org/8/downloads",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "hass",
+ "homepage": "https://www.drupal.org/u/hass"
+ },
+ {
+ "name": "See other contributors",
+ "homepage": "https://www.drupal.org/node/147903/committers"
+ },
+ {
+ "name": "diolan",
+ "homepage": "https://www.drupal.org/user/2336786"
+ },
+ {
+ "name": "hass",
+ "homepage": "https://www.drupal.org/user/85918"
+ },
+ {
+ "name": "id.medion",
+ "homepage": "https://www.drupal.org/user/2542592"
+ },
+ {
+ "name": "kim.pepper",
+ "homepage": "https://www.drupal.org/user/370574"
+ },
+ {
+ "name": "larowlan",
+ "homepage": "https://www.drupal.org/user/395439"
+ },
+ {
+ "name": "liam morland",
+ "homepage": "https://www.drupal.org/user/493050"
+ },
+ {
+ "name": "robloach",
+ "homepage": "https://www.drupal.org/user/61114"
+ },
+ {
+ "name": "wundo",
+ "homepage": "https://www.drupal.org/user/25523"
+ },
+ {
+ "name": "yseki",
+ "homepage": "https://www.drupal.org/user/1523064"
+ }
+ ],
+ "description": "Protect your website from spam and abuse while letting real people pass through with ease.",
+ "homepage": "https://www.drupal.org/project/recaptcha",
+ "support": {
+ "source": "https://git.drupalcode.org/project/recaptcha.git",
+ "issues": "https://www.drupal.org/project/issues/recaptcha"
+ },
+ "install-path": "../../web/modules/contrib/recaptcha"
+ },
{
"name": "drupal/redirect",
"version": "1.12.0",
@@ -3565,6 +3738,61 @@
},
"install-path": "../enshrined/svg-sanitize"
},
+ {
+ "name": "google/recaptcha",
+ "version": "1.3.1",
+ "version_normalized": "1.3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/google/recaptcha.git",
+ "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/google/recaptcha/zipball/56522c261d2e8c58ba416c90f81a4cd9f2ed89b9",
+ "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.14",
+ "php-coveralls/php-coveralls": "^2.5",
+ "phpunit/phpunit": "^10"
+ },
+ "time": "2025-06-26T22:21:57+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "ReCaptcha\\": "src/ReCaptcha"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
+ "homepage": "https://www.google.com/recaptcha/",
+ "keywords": [
+ "Abuse",
+ "captcha",
+ "recaptcha",
+ "spam"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/forum/#!forum/recaptcha",
+ "issues": "https://github.com/google/recaptcha/issues",
+ "source": "https://github.com/google/recaptcha"
+ },
+ "install-path": "../google/recaptcha"
+ },
{
"name": "grasmash/expander",
"version": "3.0.1",
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index 52467da24..04281d94c 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'drupal/recommended-project',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
- 'reference' => 'a59c9db4a0a2a0db79b28cb64cb1d6e68e301592',
+ 'reference' => 'c463fdd43b63186ef0cde946405ce0e78c365632',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -178,6 +178,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
+ 'drupal/captcha' => array(
+ 'pretty_version' => '2.0.9',
+ 'version' => '2.0.9.0',
+ 'reference' => '2.0.9',
+ 'type' => 'drupal-module',
+ 'install_path' => __DIR__ . '/../../web/modules/contrib/captcha',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'drupal/conditional_fields' => array(
'pretty_version' => '4.0.0-alpha6',
'version' => '4.0.0.0-alpha6',
@@ -475,10 +484,19 @@
'aliases' => array(),
'dev_requirement' => false,
),
+ 'drupal/recaptcha' => array(
+ 'pretty_version' => '3.4.0',
+ 'version' => '3.4.0.0',
+ 'reference' => '8.x-3.4',
+ 'type' => 'drupal-module',
+ 'install_path' => __DIR__ . '/../../web/modules/contrib/recaptcha',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'drupal/recommended-project' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
- 'reference' => 'a59c9db4a0a2a0db79b28cb64cb1d6e68e301592',
+ 'reference' => 'c463fdd43b63186ef0cde946405ce0e78c365632',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -598,6 +616,15 @@
0 => '*',
),
),
+ 'google/recaptcha' => array(
+ 'pretty_version' => '1.3.1',
+ 'version' => '1.3.1.0',
+ 'reference' => '56522c261d2e8c58ba416c90f81a4cd9f2ed89b9',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../google/recaptcha',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'grasmash/expander' => array(
'pretty_version' => '3.0.1',
'version' => '3.0.1.0',
@@ -823,8 +850,8 @@
'psr/container-implementation' => array(
'dev_requirement' => false,
'provided' => array(
- 0 => '^1.0',
- 1 => '1.1|2.0',
+ 0 => '1.1|2.0',
+ 1 => '^1.0',
),
),
'psr/event-dispatcher' => array(
diff --git a/vendor/google/recaptcha/LICENSE b/vendor/google/recaptcha/LICENSE
new file mode 100644
index 000000000..d147b35b3
--- /dev/null
+++ b/vendor/google/recaptcha/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2019, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/google/recaptcha/README.md b/vendor/google/recaptcha/README.md
new file mode 100644
index 000000000..025979411
--- /dev/null
+++ b/vendor/google/recaptcha/README.md
@@ -0,0 +1,147 @@
+# reCAPTCHA PHP client library
+
+[](https://travis-ci.org/google/recaptcha)
+[](https://coveralls.io/github/google/recaptcha)
+[](https://packagist.org/packages/google/recaptcha)
+[](https://packagist.org/packages/google/recaptcha)
+
+reCAPTCHA is a free CAPTCHA service that protects websites from spam and abuse.
+This is a PHP library that wraps up the server-side verification step required
+to process responses from the reCAPTCHA service. This client supports both v2
+and v3.
+
+- reCAPTCHA: https://www.google.com/recaptcha
+- This repo: https://github.com/google/recaptcha
+- Hosted demo: https://recaptcha-demo.appspot.com/
+- Version: 1.3.1
+- License: BSD, see [LICENSE](LICENSE)
+
+## Installation
+
+### Composer (recommended)
+
+Use [Composer](https://getcomposer.org) to install this library from Packagist:
+[`google/recaptcha`](https://packagist.org/packages/google/recaptcha)
+
+Run the following command from your project directory to add the dependency:
+
+```sh
+composer require google/recaptcha "^1.3"
+```
+
+Alternatively, add the dependency directly to your `composer.json` file:
+
+```json
+"require": {
+ "google/recaptcha": "^1.3"
+}
+```
+
+### Support for earlier versions of PHP
+
+The 1.3 release moves to PHP 8 and up. For earlier versions, you will need to
+stay with the 1.2 releases.
+
+### Direct download
+
+Download the [ZIP file](https://github.com/google/recaptcha/archive/master.zip)
+and extract into your project. An autoloader script is provided in
+`src/autoload.php` which you can require into your script. For example:
+
+```php
+require_once '/path/to/recaptcha/src/autoload.php';
+$recaptcha = new \ReCaptcha\ReCaptcha($secret);
+```
+
+The classes in the project are structured according to the
+[PSR-4](https://www.php-fig.org/psr/psr-4/) standard, so you can also use your
+own autoloader or require the needed files directly in your code.
+
+## Usage
+
+First obtain the appropriate keys for the type of reCAPTCHA you wish to
+integrate for v2 at https://www.google.com/recaptcha/admin or v3 at
+https://g.co/recaptcha/v3.
+
+Then follow the [integration guide on the developer
+site](https://developers.google.com/recaptcha/intro) to add the reCAPTCHA
+functionality into your frontend.
+
+This library comes in when you need to verify the user's response. On the PHP
+side you need the response from the reCAPTCHA service and secret key from your
+credentials. Instantiate the `ReCaptcha` class with your secret key, specify any
+additional validation rules, and then call `verify()` with the reCAPTCHA
+response (usually in `$_POST['g-recaptcha-response']` or the response from
+`grecaptcha.execute()` in JS which is in `$gRecaptchaResponse` in the example)
+and user's IP address. For example:
+
+```php
+setExpectedHostname('recaptcha-demo.appspot.com')
+ ->verify($gRecaptchaResponse, $remoteIp);
+if ($resp->isSuccess()) {
+ // Verified!
+} else {
+ $errors = $resp->getErrorCodes();
+}
+```
+
+The following methods are available:
+
+- `setExpectedHostname($hostname)`: ensures the hostname matches. You must do
+ this if you have disabled "Domain/Package Name Validation" for your
+ credentials.
+- `setExpectedApkPackageName($apkPackageName)`: if you're verifying a response
+ from an Android app. Again, you must do this if you have disabled
+ "Domain/Package Name Validation" for your credentials.
+- `setExpectedAction($action)`: ensures the action matches for the v3 API.
+- `setScoreThreshold($threshold)`: set a score threshold for responses from the
+ v3 API
+- `setChallengeTimeout($timeoutSeconds)`: set a timeout between the user passing
+ the reCAPTCHA and your server processing it.
+
+Each of the `set`\*`()` methods return the `ReCaptcha` instance so you can chain
+them together. For example:
+
+```php
+setExpectedHostname('recaptcha-demo.appspot.com')
+ ->setExpectedAction('homepage')
+ ->setScoreThreshold(0.5)
+ ->verify($gRecaptchaResponse, $remoteIp);
+
+if ($resp->isSuccess()) {
+ // Verified!
+} else {
+ $errors = $resp->getErrorCodes();
+}
+```
+
+You can find the constants for the libraries error codes in the `ReCaptcha`
+class constants, e.g. `ReCaptcha::E_HOSTNAME_MISMATCH`
+
+For more details on usage and structure, see [ARCHITECTURE](ARCHITECTURE.md).
+
+### Examples
+
+You can see examples of each reCAPTCHA type in [examples/](examples/). You can
+run the examples locally by using the Composer script:
+
+```sh
+composer run-script serve-examples
+```
+
+This makes use of the in-built PHP dev server to host the examples at
+http://localhost:8080/
+
+These are also hosted on Google AppEngine Flexible environment at
+https://recaptcha-demo.appspot.com/. This is configured by
+[`app.yaml`](./app.yaml) which you can also use to [deploy to your own AppEngine
+project](https://cloud.google.com/appengine/docs/flexible/php/download).
+
+## Contributing
+
+No one ever has enough engineers, so we're very happy to accept contributions
+via Pull Requests. For details, see [CONTRIBUTING](CONTRIBUTING.md)
diff --git a/vendor/google/recaptcha/app.yaml b/vendor/google/recaptcha/app.yaml
new file mode 100644
index 000000000..b6ccaf18b
--- /dev/null
+++ b/vendor/google/recaptcha/app.yaml
@@ -0,0 +1,8 @@
+runtime: php
+env: flex
+
+skip_files:
+- tests
+
+runtime_config:
+ document_root: examples
diff --git a/vendor/google/recaptcha/composer.json b/vendor/google/recaptcha/composer.json
new file mode 100644
index 000000000..7f199fca0
--- /dev/null
+++ b/vendor/google/recaptcha/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "google/recaptcha",
+ "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
+ "type": "library",
+ "keywords": ["recaptcha", "captcha", "spam", "abuse"],
+ "homepage": "https://www.google.com/recaptcha/",
+ "license": "BSD-3-Clause",
+ "support": {
+ "forum": "https://groups.google.com/forum/#!forum/recaptcha",
+ "source": "https://github.com/google/recaptcha"
+ },
+ "require": {
+ "php": ">=8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10",
+ "friendsofphp/php-cs-fixer": "^3.14",
+ "php-coveralls/php-coveralls": "^2.5"
+ },
+ "autoload": {
+ "psr-4": {
+ "ReCaptcha\\": "src/ReCaptcha"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3.x-dev"
+ }
+ },
+ "scripts": {
+ "lint": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer -vvv fix --using-cache=no --dry-run .",
+ "lint-fix": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer -vvv fix --using-cache=no .",
+ "test": "XDEBUG_MODE=coverage vendor/bin/phpunit",
+ "serve-examples": "@php -S localhost:8080 -t examples"
+ },
+ "config": {
+ "process-timeout": 0
+ }
+}
diff --git a/vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php b/vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php
new file mode 100644
index 000000000..463a5e579
--- /dev/null
+++ b/vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php
@@ -0,0 +1,275 @@
+secret = $secret;
+ $this->requestMethod = (is_null($requestMethod)) ? new RequestMethod\Post() : $requestMethod;
+ }
+
+ /**
+ * Calls the reCAPTCHA siteverify API to verify whether the user passes
+ * CAPTCHA test and additionally runs any specified additional checks
+ *
+ * @param string $response The user response token provided by reCAPTCHA, verifying the user on your site.
+ * @param string $remoteIp The end user's IP address.
+ * @return Response Response from the service.
+ */
+ public function verify($response, $remoteIp = null)
+ {
+ // Discard empty solution submissions
+ if (empty($response)) {
+ $recaptchaResponse = new Response(false, array(self::E_MISSING_INPUT_RESPONSE));
+ return $recaptchaResponse;
+ }
+
+ $params = new RequestParameters($this->secret, $response, $remoteIp, self::VERSION);
+ $rawResponse = $this->requestMethod->submit($params);
+ $initialResponse = Response::fromJson($rawResponse);
+ $validationErrors = array();
+
+ if (isset($this->hostname) && strcasecmp($this->hostname, $initialResponse->getHostname()) !== 0) {
+ $validationErrors[] = self::E_HOSTNAME_MISMATCH;
+ }
+
+ if (isset($this->apkPackageName) && strcasecmp($this->apkPackageName, $initialResponse->getApkPackageName()) !== 0) {
+ $validationErrors[] = self::E_APK_PACKAGE_NAME_MISMATCH;
+ }
+
+ if (isset($this->action) && strcasecmp($this->action, $initialResponse->getAction()) !== 0) {
+ $validationErrors[] = self::E_ACTION_MISMATCH;
+ }
+
+ if (isset($this->threshold) && $this->threshold > $initialResponse->getScore()) {
+ $validationErrors[] = self::E_SCORE_THRESHOLD_NOT_MET;
+ }
+
+ if (isset($this->timeoutSeconds)) {
+ $challengeTs = strtotime($initialResponse->getChallengeTs());
+
+ if ($challengeTs > 0 && time() - $challengeTs > $this->timeoutSeconds) {
+ $validationErrors[] = self::E_CHALLENGE_TIMEOUT;
+ }
+ }
+
+ if (empty($validationErrors)) {
+ return $initialResponse;
+ }
+
+ return new Response(
+ false,
+ array_merge($initialResponse->getErrorCodes(), $validationErrors),
+ $initialResponse->getHostname(),
+ $initialResponse->getChallengeTs(),
+ $initialResponse->getApkPackageName(),
+ $initialResponse->getScore(),
+ $initialResponse->getAction()
+ );
+ }
+
+ /**
+ * Provide a hostname to match against in verify()
+ * This should be without a protocol or trailing slash, e.g. www.google.com
+ *
+ * @param string $hostname Expected hostname
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setExpectedHostname($hostname)
+ {
+ $this->hostname = $hostname;
+ return $this;
+ }
+
+ /**
+ * Provide an APK package name to match against in verify()
+ *
+ * @param string $apkPackageName Expected APK package name
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setExpectedApkPackageName($apkPackageName)
+ {
+ $this->apkPackageName = $apkPackageName;
+ return $this;
+ }
+
+ /**
+ * Provide an action to match against in verify()
+ * This should be set per page.
+ *
+ * @param string $action Expected action
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setExpectedAction($action)
+ {
+ $this->action = $action;
+ return $this;
+ }
+
+ /**
+ * Provide a threshold to meet or exceed in verify()
+ * Threshold should be a float between 0 and 1 which will be tested as response >= threshold.
+ *
+ * @param float $threshold Expected threshold
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setScoreThreshold($threshold)
+ {
+ $this->threshold = floatval($threshold);
+ return $this;
+ }
+
+ /**
+ * Provide a timeout in seconds to test against the challenge timestamp in verify()
+ *
+ * @param int $timeoutSeconds Maximum time (seconds) elapsed since the challenge timestamp
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setChallengeTimeout($timeoutSeconds)
+ {
+ $this->timeoutSeconds = $timeoutSeconds;
+ return $this;
+ }
+}
diff --git a/vendor/google/recaptcha/src/ReCaptcha/RequestMethod.php b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod.php
new file mode 100644
index 000000000..bd2a94902
--- /dev/null
+++ b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod.php
@@ -0,0 +1,49 @@
+curl = (is_null($curl)) ? new Curl() : $curl;
+ $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ }
+
+ /**
+ * Submit the cURL request with the specified parameters.
+ *
+ * @param RequestParameters $params Request parameters
+ * @return string Body of the reCAPTCHA response
+ */
+ public function submit(RequestParameters $params)
+ {
+ $handle = $this->curl->init($this->siteVerifyUrl);
+
+ $options = array(
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $params->toQueryString(),
+ CURLOPT_HTTPHEADER => array(
+ 'Content-Type: application/x-www-form-urlencoded'
+ ),
+ CURLINFO_HEADER_OUT => false,
+ CURLOPT_HEADER => false,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_SSL_VERIFYPEER => true
+ );
+ $this->curl->setoptArray($handle, $options);
+
+ $response = $this->curl->exec($handle);
+ $this->curl->close($handle);
+
+ if ($response !== false) {
+ return $response;
+ }
+
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
+ }
+}
diff --git a/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Post.php b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Post.php
new file mode 100644
index 000000000..a4ff716fb
--- /dev/null
+++ b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Post.php
@@ -0,0 +1,88 @@
+siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ }
+
+ /**
+ * Submit the POST request with the specified parameters.
+ *
+ * @param RequestParameters $params Request parameters
+ * @return string Body of the reCAPTCHA response
+ */
+ public function submit(RequestParameters $params)
+ {
+ $options = array(
+ 'http' => array(
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'method' => 'POST',
+ 'content' => $params->toQueryString(),
+ // Force the peer to validate (not needed in 5.6.0+, but still works)
+ 'verify_peer' => true,
+ ),
+ );
+ $context = stream_context_create($options);
+ $response = file_get_contents($this->siteVerifyUrl, false, $context);
+
+ if ($response !== false) {
+ return $response;
+ }
+
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
+ }
+}
diff --git a/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Socket.php b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Socket.php
new file mode 100644
index 000000000..236bd5f5d
--- /dev/null
+++ b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Socket.php
@@ -0,0 +1,112 @@
+handle = fsockopen($hostname, $port, $errno, $errstr, (is_null($timeout) ? ini_get("default_socket_timeout") : $timeout));
+
+ if ($this->handle != false && $errno === 0 && $errstr === '') {
+ return $this->handle;
+ }
+ return false;
+ }
+
+ /**
+ * fwrite
+ *
+ * @see http://php.net/fwrite
+ * @param string $string
+ * @param int $length
+ * @return int | bool
+ */
+ public function fwrite($string, $length = null)
+ {
+ return fwrite($this->handle, $string, (is_null($length) ? strlen($string) : $length));
+ }
+
+ /**
+ * fgets
+ *
+ * @see http://php.net/fgets
+ * @param int $length
+ * @return string
+ */
+ public function fgets($length = null)
+ {
+ return fgets($this->handle, $length);
+ }
+
+ /**
+ * feof
+ *
+ * @see http://php.net/feof
+ * @return bool
+ */
+ public function feof()
+ {
+ return feof($this->handle);
+ }
+
+ /**
+ * fclose
+ *
+ * @see http://php.net/fclose
+ * @return bool
+ */
+ public function fclose()
+ {
+ return fclose($this->handle);
+ }
+}
diff --git a/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php
new file mode 100644
index 000000000..0aa72af15
--- /dev/null
+++ b/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php
@@ -0,0 +1,110 @@
+socket = (is_null($socket)) ? new Socket() : $socket;
+ $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ }
+
+ /**
+ * Submit the POST request with the specified parameters.
+ *
+ * @param RequestParameters $params Request parameters
+ * @return string Body of the reCAPTCHA response
+ */
+ public function submit(RequestParameters $params)
+ {
+ $errno = 0;
+ $errstr = '';
+ $urlParsed = parse_url($this->siteVerifyUrl);
+
+ if (false === $this->socket->fsockopen('ssl://' . $urlParsed['host'], 443, $errno, $errstr, 30)) {
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
+ }
+
+ $content = $params->toQueryString();
+
+ $request = "POST " . $urlParsed['path'] . " HTTP/1.0\r\n";
+ $request .= "Host: " . $urlParsed['host'] . "\r\n";
+ $request .= "Content-Type: application/x-www-form-urlencoded\r\n";
+ $request .= "Content-length: " . strlen($content) . "\r\n";
+ $request .= "Connection: close\r\n\r\n";
+ $request .= $content . "\r\n\r\n";
+
+ $this->socket->fwrite($request);
+ $response = '';
+
+ while (!$this->socket->feof()) {
+ $response .= $this->socket->fgets(4096);
+ }
+
+ $this->socket->fclose();
+
+ if (0 !== strpos($response, 'HTTP/1.0 200 OK')) {
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_BAD_RESPONSE.'"]}';
+ }
+
+ $parts = preg_split("#\n\s*\n#Uis", $response);
+
+ return $parts[1];
+ }
+}
diff --git a/vendor/google/recaptcha/src/ReCaptcha/RequestParameters.php b/vendor/google/recaptcha/src/ReCaptcha/RequestParameters.php
new file mode 100644
index 000000000..e9ba45354
--- /dev/null
+++ b/vendor/google/recaptcha/src/ReCaptcha/RequestParameters.php
@@ -0,0 +1,111 @@
+secret = $secret;
+ $this->response = $response;
+ $this->remoteIp = $remoteIp;
+ $this->version = $version;
+ }
+
+ /**
+ * Array representation.
+ *
+ * @return array Array formatted parameters.
+ */
+ public function toArray()
+ {
+ $params = array('secret' => $this->secret, 'response' => $this->response);
+
+ if (!is_null($this->remoteIp)) {
+ $params['remoteip'] = $this->remoteIp;
+ }
+
+ if (!is_null($this->version)) {
+ $params['version'] = $this->version;
+ }
+
+ return $params;
+ }
+
+ /**
+ * Query string representation for HTTP request.
+ *
+ * @return string Query string formatted parameters.
+ */
+ public function toQueryString()
+ {
+ return http_build_query($this->toArray(), '', '&');
+ }
+}
diff --git a/vendor/google/recaptcha/src/ReCaptcha/Response.php b/vendor/google/recaptcha/src/ReCaptcha/Response.php
new file mode 100644
index 000000000..8a5d3aabe
--- /dev/null
+++ b/vendor/google/recaptcha/src/ReCaptcha/Response.php
@@ -0,0 +1,218 @@
+success = $success;
+ $this->hostname = $hostname;
+ $this->challengeTs = $challengeTs;
+ $this->apkPackageName = $apkPackageName;
+ $this->score = $score;
+ $this->action = $action;
+ $this->errorCodes = $errorCodes;
+ }
+
+ /**
+ * Is success?
+ *
+ * @return boolean
+ */
+ public function isSuccess()
+ {
+ return $this->success;
+ }
+
+ /**
+ * Get error codes.
+ *
+ * @return array
+ */
+ public function getErrorCodes()
+ {
+ return $this->errorCodes;
+ }
+
+ /**
+ * Get hostname.
+ *
+ * @return string
+ */
+ public function getHostname()
+ {
+ return $this->hostname;
+ }
+
+ /**
+ * Get challenge timestamp
+ *
+ * @return string
+ */
+ public function getChallengeTs()
+ {
+ return $this->challengeTs;
+ }
+
+ /**
+ * Get APK package name
+ *
+ * @return string
+ */
+ public function getApkPackageName()
+ {
+ return $this->apkPackageName;
+ }
+ /**
+ * Get score
+ *
+ * @return float
+ */
+ public function getScore()
+ {
+ return $this->score;
+ }
+
+ /**
+ * Get action
+ *
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ public function toArray()
+ {
+ return array(
+ 'success' => $this->isSuccess(),
+ 'hostname' => $this->getHostname(),
+ 'challenge_ts' => $this->getChallengeTs(),
+ 'apk_package_name' => $this->getApkPackageName(),
+ 'score' => $this->getScore(),
+ 'action' => $this->getAction(),
+ 'error-codes' => $this->getErrorCodes(),
+ );
+ }
+}
diff --git a/vendor/google/recaptcha/src/autoload.php b/vendor/google/recaptcha/src/autoload.php
new file mode 100644
index 000000000..7947a1050
--- /dev/null
+++ b/vendor/google/recaptcha/src/autoload.php
@@ -0,0 +1,69 @@
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/web/modules/contrib/captcha/README.md b/web/modules/contrib/captcha/README.md
new file mode 100755
index 000000000..c21f7be75
--- /dev/null
+++ b/web/modules/contrib/captcha/README.md
@@ -0,0 +1,93 @@
+# CAPTCHA
+
+A CAPTCHA is a challenge-response test most often placed within web forms to
+determine whether the user is human. The purpose of CAPTCHA is to block form
+submissions by spambots, which are automated scripts that post spam content
+everywhere they can. The CAPTCHA module provides this feature to virtually any
+user facing web form on a Drupal site.
+
+For a full description of the module, visit the
+[project page](https://www.drupal.org/project/captcha)
+
+Submit bug reports and feature suggestions, or track changes in the
+[issue queue](https://www.drupal.org/project/issues/captcha)
+
+
+## Table of contents
+
+- Requirements
+- Conflicts/Known issues
+- Installation
+- Configuration
+- Development
+- Maintainers
+
+
+## Requirements
+
+This module requires no modules outside of Drupal core.
+
+
+## Conflicts/Known issues
+
+CAPTCHA and page caching do not work together currently. However, the CAPTCHA
+module does support the Drupal core page caching mechanism: it just disables the
+caching of the pages where it has to put its challenges.
+
+If you use other caching mechanisms, it is possible that CAPTCHA's won't work,
+and you get error messages like 'CAPTCHA validation error: unknown CAPTCHA
+session ID'.
+
+
+## Installation
+
+Install as you would normally install a contributed Drupal module. For further
+information, see
+[Installing Drupal Modules](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules).
+
+
+## Configuration
+
+The configuration page is at `admin/config/people/captcha`,
+where you can configure the CAPTCHA module
+and enable challenges for the desired forms.
+You can also tweak the image CAPTCHA to your liking.
+
+1. Navigate to Administration > Extend and enable the module.
+1. Navigate to Administration > Configuration > People > Captcha module
+ settings to administer how and when Captcha is used.
+1. Select the challenge type you want for each of the listed forms.
+1. Select "Add a description to the CAPTCHA" to add a configurable
+ description to explain the purpose of the CAPTCHA to the visitor.
+1. For Default CAPTCHA validation, define how the response should be
+ processed by default. Note that the modules that provide the actual
+ challenges can override or ignore this.
+1. Save configuration.
+
+
+## Development
+
+You can disable captcha in your local or test environment by adding the
+following line to `settings.php`:
+```
+$settings['disable_captcha'] = TRUE;
+```
+
+
+## Maintainers
+
+- Fabiano Sant'Ana - [wundo](https://www.drupal.org/u/wundo)
+- Julian Pustkuchen - [Anybody](https://www.drupal.org/u/Anybody)
+- Jakob Perry - [japerry](https://www.drupal.org/u/japerry)
+- Rob Loach - [RobLoach](https://www.drupal.org/u/RobLoach)
+- soxofaan - [soxofaan](https://www.drupal.org/u/soxofaan)
+- Joshua Sedler - [Grevil](https://www.drupal.org/u/Grevil)
+- Thomas Frobieter - [thomas.frobieter](https://www.drupal.org/u/thomas.frobieter)
+- Lachlan Ennis - [elachlan](https://www.drupal.org/u/elachlan)
+- Naveen Valecha - [naveenvalecha](https://www.drupal.org/u/naveenvalecha)
+- Andrii Podanenko - [podarok](https://www.drupal.org/u/podarok)
+
+Supporting organizations:
+
+- Chuva Inc. - [Chuva Inc](https://www.drupal.org/chuva-inc)
+- webks GmbH - [DROWL.de](https://www.DROWL.de)
diff --git a/web/modules/contrib/captcha/captcha.api.php b/web/modules/contrib/captcha/captcha.api.php
new file mode 100644
index 000000000..d3f553407
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.api.php
@@ -0,0 +1,197 @@
+ 'textfield',
+ '#title' => t('Enter "foo"'),
+ '#required' => TRUE,
+ ];
+ // The CAPTCHA module provides an option for case sensitive and case
+ // insensitive validation of the responses. If this is not sufficient,
+ // you can provide your own validation function with the
+ // 'captcha_validate' field, illustrated by the following example:
+ $captcha['captcha_validate'] = 'hook_captcha_custom_validation';
+ return $captcha;
+ }
+ break;
+ }
+}
+
+/**
+ * Allow modules to alter a CAPTCHA.
+ *
+ * @param array $captcha
+ * The array returned by hook_captcha().
+ * @param array $info
+ * Array of information about the CAPTCHA.
+ */
+function hook_captcha_alter(&$captcha, $info) {
+ if ($info['module'] == 'mymodule') {
+ $captcha['form']['captcha_response']['#description'] = t('New description.');
+ }
+}
+
+/**
+ * Implements hook_help().
+ *
+ * You should of course implement a function foo_captcha_settings_form() which
+ * returns the form of your configuration page.
+ * === Optional: hook_help($section) ===
+ * To offer a description/explanation of your challenge, you can use the
+ * normal hook_help() system.
+ * For our simple foo CAPTCHA module this would mean:
+ */
+function hook_captcha_help($route_name, RouteMatchInterface $route_match) {
+ switch ($route_name) {
+ case 'foo_captcha.settings':
+ return '
' . t('This is a very simple challenge, which requires users to
+ enter "foo" in a textfield.') . '
';
+ }
+}
+
+/**
+ * A hook_captcha_custom_advance_validation implementation example.
+ *
+ * Custom CAPTCHA validation function. Previous example shows the basic usage
+ * for custom validation with only a $solution and $response argument, which
+ * should be sufficient for most CAPTCHA modules. More advanced CAPTCHA modules
+ * can also use extra provided arguments $element and $form_state.
+ *
+ * @param string $solution
+ * The solution for the challenge as reported by hook_captcha('generate',...).
+ * @param string $response
+ * The answer given by the user.
+ *
+ * @return true
+ * on success and FALSE on failure.
+ */
+function hook_captcha_custom_validation($solution, $response) {
+ return $response == "foo" || $response == "bar";
+}
+
+/**
+ * A hook_captcha_custom_advance_validation implementation example.
+ *
+ * Custom Advance CAPTCHA validation function. These extra arguments are the
+ * $element and $form_state arguments of the validation function of the #captcha
+ * element. See captcha_validate() in captcha.module for more info about this.
+ *
+ * @param string $solution
+ * The solution for the challenge as reported by hook_captcha('generate',...).
+ * @param string $response
+ * The answer given by the user.
+ * @param array $element
+ * The element argument.
+ * @param array $form_state
+ * The form_state argument.
+ *
+ * @return true
+ * on success and FALSE on failure.
+ */
+function hook_captcha_custom_advance_validation($solution, $response, array $element, array $form_state) {
+ return $form_state['foo']['#bar'] = 'baz';
+}
+
+/**
+ * A hook_captcha_placement_map implementation example.
+ *
+ * === Hook into CAPTCHA placement ===
+ * The CAPTCHA module attempts to place the CAPTCHA element in an appropriate
+ * spot at the bottom of the targeted form, but this automatic detection may be
+ * insufficient for complex forms.
+ * The hook_captcha_placement_map hook allows to define the placement of the
+ * CAPTCHA element as desired. The hook should return an array, mapping form IDs
+ * to placement arrays, which are associative arrays with the following fields:
+ * 'path': path (array of path items) of the form's container element in which
+ * the CAPTCHA element should be inserted.
+ * 'key': the key of the element before which the CAPTCHA element
+ * should be inserted. If the field 'key' is undefined or NULL, the CAPTCHA
+ * will just be appended in the container.
+ * 'weight': if 'key' is not NULL: should be the weight of the element defined
+ * by 'key'. If 'key' is NULL and weight is not NULL/unset: set the weight
+ * property of the CAPTCHA element to this value.
+ * For example:
+ * This will place the CAPTCHA element
+ * in the 'my_fancy_form' form inside the container $form['items']['buttons'],
+ * just before the element $form['items']['buttons']['sacebutton'].
+ * in the 'another_form' form at the toplevel of the form, with a weight 34.
+ */
+function hook_captcha_placement_map() {
+ return [
+ 'my_fancy_form' => [
+ 'path' => ['items', 'buttons'],
+ 'key' => 'savebutton',
+ ],
+ 'another_form' => [
+ 'path' => [],
+ 'weight' => 34,
+ ],
+ ];
+}
diff --git a/web/modules/contrib/captcha/captcha.config_translation.yml b/web/modules/contrib/captcha/captcha.config_translation.yml
new file mode 100644
index 000000000..eeb5c2f8b
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.config_translation.yml
@@ -0,0 +1,5 @@
+captcha.settings:
+ title: 'CAPTCHA'
+ base_route_name: captcha_settings
+ names:
+ - captcha.settings
diff --git a/web/modules/contrib/captcha/captcha.inc b/web/modules/contrib/captcha/captcha.inc
new file mode 100755
index 000000000..72bdf9f86
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.inc
@@ -0,0 +1,340 @@
+module
+ * and $captcha_type->captcha_type.
+ */
+function captcha_set_form_id_setting($form_id, $captcha_type) {
+ /** @var Drupal\captcha\Entity\CaptchaPoint $captcha_point */
+ $captcha_point = CaptchaPoint::load($form_id);
+
+ if ($captcha_point) {
+ $captcha_point->setCaptchaType($captcha_type);
+ }
+ else {
+ $captcha_point = new CaptchaPoint([
+ 'formId' => $form_id,
+ 'captchaType' => $captcha_type,
+ // @see https://www.drupal.org/project/drupal/issues/3302838
+ // On config pre-save drupal tries to query same uuid, and deprecation
+ // is generated. So we prefill the uuid for now.
+ 'uuid' => \Drupal::service('uuid')->generate(),
+ ], 'captcha_point');
+ }
+ $captcha_point->enable();
+
+ $captcha_point->save();
+}
+
+/**
+ * Get the CAPTCHA setting for a given form_id.
+ *
+ * @param string $form_id
+ * The form_id to query for.
+ * @param bool $symbolic
+ * Flag to return as (symbolic) strings instead of object.
+ *
+ * @return null|CaptchaPoint
+ * NULL if no setting is known
+ * captcha point object with fields 'module' and 'captcha_type'.
+ * If argument $symbolic is true, returns 'default' or in the
+ * form 'captcha/Math'.
+ */
+function captcha_get_form_id_setting($form_id, $symbolic = FALSE) {
+ /** @var \Drupal\captcha\Entity\CaptchaPoint $captchaPoint */
+ $captcha_point = CaptchaPoint::load($form_id);
+
+ if ($symbolic) {
+ $captcha_point = $captcha_point->getCaptchaType();
+ }
+
+ return $captcha_point;
+}
+
+/**
+ * Helper function for generating a new CAPTCHA session.
+ *
+ * @param string $form_id
+ * The form_id of the form to add a CAPTCHA to.
+ * @param int $status
+ * The initial status of the CAPTCHA session.
+ *
+ * @return string
+ * The session ID of the new CAPTCHA session.
+ */
+function _captcha_generate_captcha_session($form_id = NULL, $status = CaptchaConstants::CAPTCHA_STATUS_UNSOLVED) {
+ $user = \Drupal::currentUser();
+
+ // Initialize solution with random data.
+ $solution = hash('sha256', mt_rand());
+
+ // Insert an entry and thankfully receive the value
+ // of the autoincrement field 'csid'.
+ $captcha_sid = \Drupal::database()->insert('captcha_sessions')
+ ->fields([
+ 'uid' => $user->id(),
+ 'sid' => session_id(),
+ 'ip_address' => \Drupal::request()->getClientIp(),
+ 'timestamp' => \Drupal::time()->getRequestTime(),
+ 'form_id' => $form_id,
+ 'solution' => $solution,
+ 'status' => $status,
+ 'attempts' => 0,
+ ])
+ ->execute();
+ return $captcha_sid;
+}
+
+/**
+ * Helper function for updating the solution in the CAPTCHA session table.
+ *
+ * @param string $captcha_sid
+ * The CAPTCHA session ID to update.
+ * @param string $solution
+ * The new solution to associate with the given CAPTCHA session.
+ */
+function _captcha_update_captcha_session($captcha_sid, $solution) {
+ \Drupal::database()->update('captcha_sessions')
+ ->condition('csid', $captcha_sid)
+ ->fields([
+ 'timestamp' => \Drupal::time()->getRequestTime(),
+ 'solution' => $solution,
+ ])
+ ->execute();
+}
+
+/**
+ * Helper function for checking if CAPTCHA is required for user.
+ *
+ * Based on the CAPTCHA persistence setting, the CAPTCHA session
+ * ID and user session info.
+ */
+function _captcha_required_for_user($captcha_sid, $form_id) {
+ // Get the CAPTCHA persistence setting.
+ $captcha_persistence = \Drupal::config('captcha.settings')
+ ->get('persistence');
+
+ // First check: should we always add a CAPTCHA?
+ if ($captcha_persistence == CaptchaConstants::CAPTCHA_PERSISTENCE_SHOW_ALWAYS) {
+ return TRUE;
+ }
+
+ // Get the status of the current CAPTCHA session.
+ $captcha_session_status = \Drupal::database()
+ ->select('captcha_sessions', 'cs')
+ ->fields('cs', ['status'])
+ ->condition('csid', $captcha_sid)
+ ->execute()
+ ->fetchField();
+
+ // Second check: if the current session is already
+ // solved: omit further CAPTCHAs.
+ if ($captcha_session_status == CaptchaConstants::CAPTCHA_STATUS_SOLVED) {
+ return FALSE;
+ }
+
+ // Third check: look at the persistence level
+ // (per form instance, per form or per user).
+ if ($captcha_persistence == CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE) {
+ return TRUE;
+ }
+ else {
+ $captcha_success_form_ids = isset($_SESSION['captcha_success_form_ids']) ? (array) ($_SESSION['captcha_success_form_ids']) : [];
+ switch ($captcha_persistence) {
+ case CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL:
+ return (count($captcha_success_form_ids) == 0);
+
+ case CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE:
+ return !isset($captcha_success_form_ids[$form_id]);
+ }
+ }
+
+ // We should never get to this point, but to be sure, we return TRUE.
+ return TRUE;
+}
+
+/**
+ * Get the CAPTCHA title.
+ *
+ * @return string
+ * CAPTCHA title.
+ */
+function _captcha_get_title() {
+ $title = \Drupal::config('captcha.settings')->get('title');
+ if ($title === NULL) {
+ return '';
+ }
+ return Xss::filter($title);
+}
+
+/**
+ * Get the CAPTCHA description.
+ *
+ * @return string
+ * CAPTCHA description.
+ */
+function _captcha_get_description() {
+ $description = \Drupal::config('captcha.settings')->get('description');
+ if ($description === NULL) {
+ return '';
+ }
+ return Xss::filter($description);
+}
+
+/**
+ * Gets the error message for when a user enters an incorrect CAPTCHA answer.
+ *
+ * @return string
+ * Error message.
+ */
+function _captcha_get_error_message() {
+ $error_message = \Drupal::config('captcha.settings')->get('wrong_captcha_response_message');
+ if ($error_message) {
+ return Xss::filter($error_message);
+ }
+ return t('The answer you entered for the CAPTCHA was not correct.');
+}
+
+/**
+ * Parse or interpret the given captcha_type.
+ *
+ * @param string $captcha_type
+ * representation of the CAPTCHA type,
+ * e.g. 'default', 'captcha/Math', 'image_captcha/Image'.
+ *
+ * @return array
+ * list($captcha_module, $captcha_type).
+ */
+function _captcha_parse_captcha_type($captcha_type) {
+ if ($captcha_type == CaptchaConstants::CAPTCHA_TYPE_DEFAULT) {
+ $captcha_type = \Drupal::config('captcha.settings')
+ ->get('default_challenge', CaptchaConstants::CAPTCHA_MATH_CAPTCHA_TYPE);
+ }
+ return explode('/', $captcha_type);
+}
+
+/**
+ * Helper function to get placement information for a given form_id.
+ */
+function _captcha_get_captcha_placement($form_id, $form) {
+ // Get CAPTCHA placement map from cache. Two levels of cache:
+ // static variable in this function and storage in the variables table.
+ static $placement_map = NULL;
+
+ $write_cache = FALSE;
+
+ // Try first level cache.
+ if ($placement_map === NULL) {
+ // If first level cache missed: try second level cache.
+ if ($cache = \Drupal::cache()->get('captcha_placement_map_cache')) {
+ $placement_map = $cache->data;
+ }
+ else {
+ // If second level cache missed: initialize the placement map
+ // and let other modules hook into this with the
+ // hook_captcha_placement_map hook.
+ // By default however, probably all Drupal core forms
+ // are already correctly handled with the best effort guess
+ // based on the 'actions' element (see below).
+ $placement_map = \Drupal::moduleHandler()
+ ->invokeAll('captcha_placement_map');
+ $write_cache = TRUE;
+ }
+ }
+
+ // Query the placement map.
+ if (array_key_exists($form_id, $placement_map) && $placement_map[$form_id] !== NULL) {
+ $placement = $placement_map[$form_id];
+ }
+ // If no placement info is available in placement map:
+ // make a best effort guess.
+ else {
+ // If there is an "actions" button group, a good placement
+ // is just before that.
+ if (isset($form['actions']) && isset($form['actions']['#type']) && $form['actions']['#type'] === 'actions') {
+ $placement = [
+ 'path' => [],
+ 'key' => 'actions',
+ // #type 'actions' defaults to 100.
+ 'weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99),
+ ];
+ }
+ else {
+ // Search the form for buttons and guess placement from it.
+ $buttons = _captcha_search_buttons($form);
+ if (count($buttons)) {
+ // Pick first button.
+ // @todo make this more sofisticated? Use cases needed.
+ $placement = (isset($buttons[count($buttons) - 1])) ? $buttons[count($buttons) - 1] : $buttons[0];
+ }
+ else {
+ // Use NULL when no buttons were found.
+ $placement = NULL;
+ }
+ }
+
+ // Store calculated placement in cache.
+ $placement_map[$form_id] = $placement;
+ $write_cache = TRUE;
+ }
+
+ if ($write_cache) {
+ \Drupal::cache()->set('captcha_placement_map_cache', $placement_map);
+ }
+ return $placement;
+}
+
+/**
+ * Helper function for searching the buttons in a form.
+ *
+ * @param array $form
+ * The form to search button elements in.
+ *
+ * @return array
+ * Array of paths to the buttons.
+ * A path is an array of keys leading to the button, the last
+ * item in the path is the weight of the button element
+ * (or NULL if undefined).
+ */
+function _captcha_search_buttons(array $form) {
+ $buttons = [];
+
+ foreach (Element::children($form, FALSE) as $key) {
+ // Look for submit or button type elements.
+ if (isset($form[$key]['#type']) && ($form[$key]['#type'] == 'submit' || $form[$key]['#type'] == 'button')) {
+ $weight = $form[$key]['#weight'] ?? NULL;
+ $buttons[] = [
+ 'path' => [],
+ 'key' => $key,
+ 'weight' => $weight,
+ ];
+ }
+ // Process children recursively.
+ $children_buttons = _captcha_search_buttons($form[$key]);
+ foreach ($children_buttons as $b) {
+ $b['path'] = array_merge([$key], $b['path']);
+ $buttons[] = $b;
+ }
+ }
+
+ return $buttons;
+}
diff --git a/web/modules/contrib/captcha/captcha.info.yml b/web/modules/contrib/captcha/captcha.info.yml
new file mode 100644
index 000000000..aaec67503
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.info.yml
@@ -0,0 +1,11 @@
+name: CAPTCHA
+type: module
+description: Provides the CAPTCHA API for adding challenges to arbitrary forms.
+package: Spam control
+core_version_requirement: ^9.5 || ^10 || ^11
+configure: captcha_settings
+
+# Information added by Drupal.org packaging script on 2025-07-28
+version: '2.0.9'
+project: 'captcha'
+datestamp: 1753701291
diff --git a/web/modules/contrib/captcha/captcha.install b/web/modules/contrib/captcha/captcha.install
new file mode 100755
index 000000000..c68361abe
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.install
@@ -0,0 +1,269 @@
+ 'Stores the data about CAPTCHA sessions (solution, IP address, timestamp, ...).',
+ 'fields' => [
+ 'csid' => [
+ 'description' => 'CAPTCHA session ID.',
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ ],
+ 'token' => [
+ 'description' => 'One time CAPTCHA token.',
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => FALSE,
+ ],
+ 'uid' => [
+ 'description' => "User's {users}.uid.",
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ],
+ 'sid' => [
+ 'description' => "Session ID of the user.",
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'default' => '',
+ ],
+ 'ip_address' => [
+ 'description' => 'IP address of the visitor.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => FALSE,
+ ],
+ 'timestamp' => [
+ 'description' => 'A Unix timestamp indicating when the challenge was generated.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ],
+ 'form_id' => [
+ 'description' => 'The form_id of the form where the CAPTCHA is added to.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ],
+ 'solution' => [
+ 'description' => 'Solution of the challenge.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ],
+ 'status' => [
+ 'description' => 'Status of the CAPTCHA session (unsolved, solved, ...)',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ],
+ 'attempts' => [
+ 'description' => 'The number of attempts.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ],
+ ],
+ 'primary key' => ['csid'],
+ 'indexes' => [
+ 'csid_ip' => ['csid', 'ip_address'],
+ ],
+ ];
+
+ return $schema;
+}
+
+/**
+ * Implements hook_requirements().
+ */
+function captcha_requirements($phase) {
+ $requirements = [];
+ $config = \Drupal::config('captcha.settings');
+
+ if ($phase == 'runtime' && $config->get('enable_stats')) {
+ // Show the wrong response counter in the status report.
+ $requirements['captcha_wrong_response_counter'] = [
+ 'title' => \Drupal::translation()->translate('CAPTCHA'),
+ 'value' => \Drupal::translation()->formatPlural(
+ \Drupal::state()->get('captcha.wrong_response_counter', 0),
+ 'Already 1 blocked form submission',
+ 'Already @count blocked form submissions'
+ ),
+ 'severity' => REQUIREMENT_INFO,
+ ];
+ }
+ return $requirements;
+}
+
+/**
+ * Implements hook_install().
+ */
+function captcha_install() {
+
+ if (!\Drupal::service('config.installer')->isSyncing() && \Drupal::moduleHandler()->moduleExists('node')) {
+ $form_ids = [];
+ $label = [];
+ // Add form_ids of all currently known node types too.
+ foreach (node_type_get_names() as $type => $name) {
+ $form_ids[] = 'node_' . $type . '_form';
+ $label[] = 'node_' . $type . '_form';
+ }
+
+ $captcha_storage = \Drupal::entityTypeManager()
+ ->getStorage('captcha_point');
+ foreach ($form_ids as $index => $form_id) {
+ $values = [
+ 'label' => $label[$index],
+ 'formId' => $form_id,
+ 'captchaType' => CaptchaConstants::CAPTCHA_TYPE_DEFAULT,
+ 'status' => FALSE,
+ ];
+ $captcha_storage->create($values)->save();
+ }
+ }
+
+}
+
+/**
+ * Implements hook_update_N().
+ */
+function captcha_update_8901(&$sandbox) {
+ $entityType = \Drupal::entityTypeManager()
+ ->getDefinition('captcha_point');
+
+ if ($entityType) {
+ \Drupal::entityDefinitionUpdateManager()
+ ->installEntityType($entityType);
+ }
+}
+
+/**
+ * Implements hook_update_N().
+ */
+function captcha_update_8902(&$sandbox) {
+ $query = \Drupal::entityQuery('captcha_point');
+ $query->notExists('label');
+ $entity_ids = $query->execute();
+
+ if (!empty($entity_ids) && is_array($entity_ids)) {
+ foreach ($entity_ids as $entity_id) {
+ $captcha_point_id = $entity_id;
+ $captcha_point = \Drupal::entityTypeManager()
+ ->getStorage('captcha_point')
+ ->load($captcha_point_id);
+ $captcha_point->set('label', $captcha_point->getFormId());
+ $captcha_point->save();
+ }
+ }
+}
+
+/**
+ * Implements hook_update_N().
+ *
+ * Handle and delete "add_captcha_description".
+ */
+function captcha_update_8903(&$sandbox) {
+ $config_factory = \Drupal::configFactory();
+ $config = $config_factory->getEditable('captcha.settings');
+ $addDescription = $config->get('add_captcha_description');
+ // If description was disabled before, set 'description' to an empty string,
+ // so it is disabled again in the newest version:
+ if (!$addDescription) {
+ $config->set('description', '')->save();
+ }
+ // Delete old config:
+ $config->clear('add_captcha_description')->save();
+}
+
+/**
+ * Several changes.
+ *
+ * Remove old configuration keys and map to new ones.
+ */
+function captcha_update_8904(&$sandbox) {
+ // Update config:
+ $config_factory = \Drupal::configFactory();
+ $config = $config_factory->getEditable('captcha.settings');
+
+ $config->set('administration_mode_on_admin_routes', FALSE);
+ $config->set('enable_globally_on_admin_routes', FALSE);
+ $config->set('enable_globally', $config->get('enabled_default'));
+
+ if (!empty($config->get('allow_on_admin_pages')) && !empty($config->get('administration_mode'))) {
+ $config->set('administration_mode_on_admin_routes', TRUE);
+ }
+ if (!empty($config->get('allow_on_admin_pages')) && !empty($config->get('enabled_default'))) {
+ $config->set('enable_globally_on_admin_routes', TRUE);
+ }
+ $config
+ ->clear('allow_on_admin_pages')
+ ->clear('enabled_default')
+ ->save();
+}
+
+/**
+ * Add the captcha title, if the user has not set it manually yet.
+ *
+ * Note, that this is needed, because the update hook introducing the title
+ * was originally implemented in the wrong install file (image_captcha.install).
+ * See https://www.drupal.org/project/captcha/issues/3356063 where it was
+ * wrongly added.
+ */
+function captcha_update_8905() {
+ $captchaConfig = \Drupal::configFactory()->getEditable('captcha.settings');
+ if ($captchaConfig->get('title') === NULL) {
+ // Set the title in config, if it wasn't existing before:
+ $captchaConfig->set('title', 'CAPTCHA')->save(TRUE);
+ }
+}
+
+/**
+ * Ensure caches are cleared with new module folder locations.
+ */
+function captcha_update_8906() {
+ // Remove test modules if they're on the site. They won't rebuild.
+ $extension = \Drupal::configFactory()->getEditable('core.extension');
+ $modules = $extension->get('module');
+ unset($modules['captcha_long_form_id_test']);
+ unset($modules['captcha_test']);
+ $extension->set('module', $modules);
+ $extension->save();
+
+ // Need to flush all caches due to module paths changing.
+ drupal_flush_all_caches();
+}
+
+/**
+ * Fix invalid config added in captcha_update_8906 (2.0.1)
+ */
+function captcha_update_8907() {
+ // Remove test modules if they're on the site. They won't rebuild.
+ $extension = \Drupal::configFactory()->getEditable('core.extension');
+
+ // Remove invalid config added in 8906 and do the actual removal.
+ if ($extension->get('modules') !== NULL) {
+ $extension->clear('modules');
+
+ $modules = $extension->get('module');
+ unset($modules['captcha_long_form_id_test']);
+ unset($modules['captcha_test']);
+ $extension->set('module', $modules);
+
+ $extension->save();
+
+ // Need to flush all caches due to module paths changing.
+ drupal_flush_all_caches();
+ }
+}
diff --git a/web/modules/contrib/captcha/captcha.libraries.yml b/web/modules/contrib/captcha/captcha.libraries.yml
new file mode 100644
index 000000000..0c2ea204b
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.libraries.yml
@@ -0,0 +1,5 @@
+base:
+ version: 1.0
+ css:
+ theme:
+ css/captcha.css: {}
diff --git a/web/modules/contrib/captcha/captcha.links.action.yml b/web/modules/contrib/captcha/captcha.links.action.yml
new file mode 100755
index 000000000..2df3643b7
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.links.action.yml
@@ -0,0 +1,5 @@
+captcha_point.add:
+ route_name: 'captcha_point.add'
+ title: 'Add Captcha Point'
+ appears_on:
+ - captcha_point.list
diff --git a/web/modules/contrib/captcha/captcha.links.menu.yml b/web/modules/contrib/captcha/captcha.links.menu.yml
new file mode 100755
index 000000000..cc80bfdaa
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.links.menu.yml
@@ -0,0 +1,13 @@
+captcha.settings:
+ title: 'CAPTCHA settings'
+ description: 'Administer CAPTCHA settings'
+ route_name: captcha_settings
+ parent: user.admin_index
+ weight: -1
+
+captcha.examples:
+ title: 'CAPTCHA examples'
+ description: Overview of the available CAPTCHA challenge types with examples'
+ route_name: captcha_examples
+ parent: captcha.settings
+ weight: 0
diff --git a/web/modules/contrib/captcha/captcha.links.task.yml b/web/modules/contrib/captcha/captcha.links.task.yml
new file mode 100755
index 000000000..b3b9940a5
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.links.task.yml
@@ -0,0 +1,14 @@
+captcha_settings:
+ route_name: captcha_settings
+ title: 'CAPTCHA Settings'
+ base_route: captcha_settings
+
+captcha_examples:
+ route_name: captcha_examples
+ title: 'CAPTCHA Examples'
+ base_route: captcha_settings
+
+captcha_points.list:
+ route_name: captcha_point.list
+ title: 'Captcha Points'
+ base_route: captcha_settings
diff --git a/web/modules/contrib/captcha/captcha.module b/web/modules/contrib/captcha/captcha.module
new file mode 100755
index 000000000..82a14f687
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.module
@@ -0,0 +1,779 @@
+' . t('About') . '';
+ $output .= '
' . t('"CAPTCHA" is an acronym for "Completely Automated Public Turing test to tell Computers and Humans Apart". It is typically a challenge-response test to determine whether the user is human. The CAPTCHA module is a tool to fight automated submission by malicious users (spamming) of for example comments forms, user registration forms, guestbook forms, etc. You can extend the desired forms with an additional challenge, which should be easy for a human to solve correctly, but hard enough to keep automated scripts and spam bots out.') . '
';
+ $output .= '
' . t('Note that the CAPTCHA module interacts with page caching (see performance settings). Because the challenge should be unique for each generated form, the caching of the page it appears on is prevented. Make sure that these forms do not appear on too many pages or you will lose much caching efficiency. For example, if you put a CAPTCHA on the user login block, which typically appears on each page for anonymous visitors, caching will practically be disabled. The comment submission forms are another example. In this case you should set the Location of comment submission form to Display on separate page in the comment settings of the relevant content types for better caching efficiency.', [
+ ':performancesettings' => Url::fromRoute('system.performance_settings')->toString(),
+ ':contenttypes' => \Drupal::moduleHandler()->moduleExists('node') ? Url::fromRoute('entity.node_type.collection')->toString() : '#',
+ ]) . '
';
+ $output .= '
' . t('CAPTCHA is a trademark of Carnegie Mellon University.') . '
' . t('A CAPTCHA can be added to virtually any Drupal form, through adding an affiliated CAPTCHA Point. Some default CAPTCHA Points are already provided in the CAPTCHA Point section, but arbitrary forms can be easily added and managed.') . '
';
+ $output .= '
' . t('Users with the Skip CAPTCHApermission won\'t be offered a challenge. Be sure to grant this permission to the trusted users (e.g. site administrators). If you want to test a protected form, be sure to do it as a user without the Skip CAPTCHA permission (e.g. as anonymous user).', [
+ ':perm' => Url::fromRoute('user.admin_permissions')->toString(),
+ ]) . '
';
+ $output .= '
' . t('Note that the CAPTCHA module disables page caching of pages that include a CAPTCHA challenge.', [
+ ':performancesettings' => Url::fromRoute('system.performance_settings')->toString(),
+ ]) . '
';
+ return ['#markup' => $output];
+ }
+}
+
+/**
+ * Loader for Captcha Point entity.
+ *
+ * @param string $id
+ * Form id string.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * An instance of an captcha_point entity.
+ */
+function captcha_point_load($id) {
+ return CaptchaPoint::load($id);
+}
+
+/**
+ * Implements hook_theme().
+ */
+function captcha_theme() {
+ $path = \Drupal::service('extension.list.module')->getPath('captcha');
+ return [
+ 'captcha' => [
+ 'render element' => 'element',
+ 'template' => 'captcha',
+ 'path' => $path . '/templates',
+ ],
+ ];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function captcha_theme_suggestions_captcha(array $variables) {
+ $suggestions = [];
+ $suggestions[] = 'captcha__' . strtolower($variables['element']['#captcha_type_challenge']);
+ return $suggestions;
+}
+
+/**
+ * Implements hook_cron().
+ *
+ * Remove old entries from captcha_sessions table.
+ */
+function captcha_cron() {
+ // Get request time.
+ $request_time = \Drupal::time()->getRequestTime();
+
+ // Remove challenges older than PHP's session.gc_maxlifetime value.
+ $connection = Database::getConnection();
+ $connection->delete('captcha_sessions')
+ ->condition('timestamp', $request_time - ini_get('session.gc_maxlifetime'), '<')
+ ->execute();
+}
+
+/**
+ * Theme function for a CAPTCHA element.
+ *
+ * Render it in a section element if a description of the CAPTCHA
+ * is available. Render it as is otherwise.
+ */
+function template_preprocess_captcha(&$variables, $hook, $info) {
+ $variables['title'] = _captcha_get_title();
+ $variables['description'] = _captcha_get_description();
+
+ // Add an indicator, if the captcha widget is visible, to
+ // account for _captcha_required_for_user().
+ // See Captcha::preRenderProcess().
+ // If the CAPTCHA was solved, hidden inputs have to be preserved to the form
+ // but the CAPTCHA and its wrappers are not visible.
+ // This variable exposed this to the theme layer transparently:
+ $variables['is_visible'] = isset($variables['element']['captcha_widgets']);
+ // Attach library:
+ $variables['#attached']['library'][] = 'captcha/base';
+}
+
+/**
+ * Implements hook_form_alter().
+ *
+ * This function adds a CAPTCHA to forms for untrusted users
+ * if needed and adds. CAPTCHA administration links for site
+ * administrators if this option is enabled.
+ */
+function captcha_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
+ $account = \Drupal::currentUser();
+ $config = \Drupal::config('captcha.settings');
+ $captchaService = \Drupal::service('captcha.helper');
+ $adminContext = \Drupal::service('router.admin_context');
+ $captcha_point = NULL;
+
+ \Drupal::moduleHandler()->loadInclude('captcha', 'inc');
+
+ // If the user does not have the "skip CAPTCHA" permission render
+ // it "normally":
+ if (!$account->hasPermission('skip CAPTCHA')) {
+ $query = \Drupal::entityQuery('captcha_point');
+ $query->condition('label', $form_id);
+ $entity_ids = $query->execute();
+
+ // If empty, see if it is a form provided by default config.
+ if (empty($entity_ids)) {
+ $query = \Drupal::entityQuery('captcha_point');
+ $query->condition('formId', $form_id);
+ $entity_ids = $query->execute();
+ }
+
+ if (!empty($entity_ids) && is_array($entity_ids)) {
+ $captcha_point_id = array_pop($entity_ids);
+ /** @var \Drupal\captcha\CaptchaPointInterface $captcha_point */
+ $captcha_point = \Drupal::entityTypeManager()
+ ->getStorage('captcha_point')
+ ->load($captcha_point_id);
+ }
+
+ // If there is no CaptchaPoint for the form_id, try to use the base_form_id.
+ if (empty($captcha_point) || !$captcha_point->status()) {
+ $form_object = $form_state->getFormObject();
+ if ($form_object instanceof BaseFormIdInterface) {
+ $base_form_id = $form_object->getBaseFormId();
+ if (!empty($base_form_id) && $base_form_id != $form_id) {
+ /** @var \Drupal\captcha\CaptchaPointInterface $captcha_point */
+ $captcha_point = \Drupal::entityTypeManager()
+ ->getStorage('captcha_point')
+ ->load($base_form_id);
+ }
+ }
+ }
+
+ // If there is no CaptchaPoint at all, but we want to add a captcha globally
+ // for every form do that here:
+ if (empty($captcha_point) && !empty($config->get('enable_globally'))) {
+ // Only add this captcha on non admin routes or if
+ // "enable_globally_on_admin_routes" is enabled, also on admin routes:
+ if (
+ !$adminContext->isAdminRoute()
+ ||
+ ($adminContext->isAdminRoute() && !empty($config->get('enable_globally_on_admin_routes')))
+ ) {
+ // Create captcha point without saving.
+ /** @var \Drupal\captcha\CaptchaPointInterface $captcha_point */
+ $captcha_point = new CaptchaPoint([
+ 'formId' => $form_id,
+ 'captchaType' => $config->get('default_challenge'),
+ ], 'captcha_point');
+ $captcha_point->enable();
+ }
+ }
+ if (!empty($captcha_point) && $captcha_point->status()) {
+ // Checking if user's ip is whitelisted.
+ if (captcha_whitelist_ip_whitelisted()) {
+ // If form is setup to have captcha, but user's ip is whitelisted, then
+ // we still have to disable form caching to prevent showing cached form
+ // for users with not whitelisted ips.
+ $form['#cache'] = ['max-age' => 0];
+ \Drupal::service('page_cache_kill_switch')->trigger();
+ }
+ else {
+ // Build CAPTCHA form element.
+ $captcha_element = [
+ '#type' => 'captcha',
+ '#captcha_type' => $captcha_point->getCaptchaType(),
+ ];
+
+ // Get placement in form and insert in form.
+ $captcha_placement = _captcha_get_captcha_placement($form_id, $form);
+ $captchaService->insertCaptchaElement($form, $captcha_placement, $captcha_element);
+ }
+ }
+ }
+ // If the user has the "skip CAPTCHA" permission, check, if it should be
+ // rendered in "administration_mode", which adds administrative informations
+ // to the captcha:
+ elseif (!empty($config->get('administration_mode')) && $account->hasPermission('administer CAPTCHA settings')) {
+ // Add informations if it isn't an admin route OR if
+ // "administration_mode_on_admin_routes" is set and the current route is
+ // and admin route:
+ if (
+ !$adminContext->isAdminRoute()
+ ||
+ ($config->get('administration_mode_on_admin_routes') && $adminContext->isAdminRoute())
+ ) {
+ // Add CAPTCHA administration tools.
+ /** @var \Drupal\captcha\CaptchaPointInterface $captcha_point */
+ $captcha_point = CaptchaPoint::load($form_id);
+
+ // Add admin information to the captcha:
+ $captcha_element = [
+ '#type' => 'details',
+ '#title' => t('CAPTCHA'),
+ '#attributes' => [
+ 'class' => ['captcha-admin-links'],
+ ],
+ '#open' => TRUE,
+ ];
+
+ if ($captcha_point !== NULL && $captcha_point->getCaptchaType()) {
+ $captcha_element['#title'] = $captcha_point->status() ? t('CAPTCHA: challenge "@type" enabled', ['@type' => $captcha_point->getCaptchaType()]) : t('CAPTCHA: challenge "@type" disabled', ['@type' => $captcha_point->getCaptchaType()]);
+ $captcha_point->status() ? $captchaElementDescription = t('Users without the "skip CAPTCHA" permission will see a CAPTCHA here (general CAPTCHA settings).',
+ [
+ '@settings' => Url::fromRoute('captcha_settings')
+ ->toString(),
+ ]) : $captchaElementDescription = t('CAPTCHA disabled, Untrusted users won\'t see the captcha (general CAPTCHA settings).',
+ ['@settings' => Url::fromRoute('captcha_settings')->toString()]
+ );
+ $captcha_element['#description'] = $captchaElementDescription;
+ $captcha_element['challenge'] = [
+ '#type' => 'item',
+ '#title' => t('Enabled challenge'),
+ '#markup' => $captcha_point->toLink(t('change'), 'edit-form', [
+ 'query' => \Drupal::destination()
+ ->getAsArray(),
+ ])->toString(),
+ ];
+ }
+ else {
+ $captcha_element['#title'] = t('CAPTCHA: no challenge enabled');
+ $captcha_element['add_captcha'] = [
+ '#markup' => Link::fromTextAndUrl(
+ t('Place a CAPTCHA here for untrusted users.'),
+ Url::fromRoute('captcha_point.add', [], [
+ 'query' => \Drupal::destination()
+ ->getAsArray() + ['form_id' => $form_id],
+ ])
+ )->toString(),
+ ];
+ }
+
+ // Get placement in form and insert in form.
+ $captcha_placement = _captcha_get_captcha_placement($form_id, $form);
+ $captchaService->insertCaptchaElement($form, $captcha_placement, $captcha_element);
+ }
+ // If the user has the "skip Captcha" permission, but the
+ // administration_mode is not enabled, simply do nothing and therefore
+ // "skip" the CAPTCHA.
+ }
+
+ // Add a warning about caching on the Performance settings page.
+ if ($form_id == 'system_performance_settings') {
+ $form['caching']['captcha'] = [
+ '#type' => 'item',
+ '#title' => t('CAPTCHA'),
+ '#markup' => '
' . t('Most CAPTCHA methods will disable the caching of pages that contain a CAPTCHA element. Check the different implementations to know more about how it affects caching.') . '
',
+ ];
+ }
+
+ // Disable captcha if override is set.
+ if (Settings::get('disable_captcha', FALSE) === TRUE) {
+ $override_notice = [
+ '#type' => 'html_tag',
+ '#tag' => 'strong',
+ '#value' => t('Captcha is currently disabled via settings.php.'),
+ ];
+ if (isset($form['elements']['captcha'])) {
+ $form['elements']['captcha'] = $override_notice;
+ }
+ if (isset($form['captcha'])) {
+ $form['captcha'] = $override_notice;
+ }
+ }
+}
+
+/**
+ * CAPTCHA validation function to tests strict equality.
+ *
+ * @param string $solution
+ * The solution of the test.
+ * @param string $response
+ * The response to the test.
+ *
+ * @return bool
+ * TRUE when case insensitive equal, FALSE otherwise.
+ */
+function captcha_validate_strict_equality($solution, $response) {
+ return $solution === $response;
+}
+
+/**
+ * CAPTCHA validation function to tests case insensitive equality.
+ *
+ * @param string $solution
+ * The solution of the test.
+ * @param string $response
+ * The response to the test.
+ *
+ * @return bool
+ * TRUE when case insensitive equal, FALSE otherwise.
+ */
+function captcha_validate_case_insensitive_equality($solution, $response) {
+ if ($solution == NULL || $response == NULL) {
+ // mb_strtolower wouldn't work on NULL and NULL is never a valid solution.
+ return FALSE;
+ }
+ return mb_strtolower($solution ?: '') === mb_strtolower($response ?: '');
+}
+
+/**
+ * CAPTCHA validation function to tests equality while ignoring spaces.
+ *
+ * @param string $solution
+ * The solution of the test.
+ * @param string $response
+ * The response to the test.
+ *
+ * @return bool
+ * TRUE when equal (ignoring spaces), FALSE otherwise.
+ */
+function captcha_validate_ignore_spaces($solution, $response) {
+ if ($solution == NULL || $response == NULL) {
+ // preg_replace wouldn't work on NULL and NULL is never a valid solution.
+ return FALSE;
+ }
+ return preg_replace('/\s/', '', $solution) === preg_replace('/\s/', '', $response);
+}
+
+/**
+ * Validation function to tests case insensitive equality while ignoring spaces.
+ *
+ * @param string $solution
+ * The solution of the test.
+ * @param string $response
+ * The response to the test.
+ *
+ * @return bool
+ * TRUE when equal (ignoring spaces), FALSE otherwise.
+ */
+function captcha_validate_case_insensitive_ignore_spaces($solution, $response) {
+ if ($solution == NULL || $response == NULL) {
+ // preg_replace wouldn't work on NULL and NULL is never a valid solution.
+ return FALSE;
+ }
+ return preg_replace('/\s/', '', mb_strtolower($solution ?: '')) === preg_replace('/\s/', '', mb_strtolower($response ?: ''));
+}
+
+/**
+ * Helper function for getting the posted CAPTCHA info.
+ *
+ * This function hides the form processing mess for several use cases an
+ * browser bug workarounds.
+ * For example: $element['#post'] can typically be used to get the posted
+ * form_id and captcha_sid, but in the case of node preview situations
+ * (with correct CAPTCHA response) that does not work and we can get them from
+ * $form_state['clicked_button']['#post'].
+ * However with Internet Explorer 7, the latter does not work either when
+ * submitting the forms (with only one text field) with the enter key
+ * (see http://drupal.org/node/534168), in which case we also have to check
+ * $form_state['buttons']['button']['0']['#post'].
+ *
+ * @param array $element
+ * The CAPTCHA element.
+ * @param Drupal\Core\Form\FormStateInterface $form_state
+ * The form state structure to extract the info from.
+ * @param string $this_form_id
+ * The form ID of the form we are currently processing
+ * (which is not necessarily the form that was posted).
+ *
+ * @return array
+ * Array with $posted_form_id and $post_captcha_sid (with NULL values
+ * if the values could not be found, e.g. for a fresh form).
+ */
+function _captcha_get_posted_captcha_info(array $element, FormStateInterface $form_state, $this_form_id) {
+ if ($form_state->has('captcha_info')) {
+ // We are handling (or rebuilding) an already submitted form,
+ // so we already determined the posted form ID and CAPTCHA session ID
+ // for this form (from before submitting). Reuse this info.
+ $posted_form_id = $form_state->get('captcha_info')['posted_form_id'];
+ $posted_captcha_sid = $form_state->get('captcha_info')['captcha_sid'];
+ }
+ else {
+ // We have to determine the posted form ID and CAPTCHA session ID
+ // from the post data.
+ // Because we possibly use raw post data here,
+ // we should be extra cautious and filter this data.
+ $input = &$form_state->getUserInput();
+ $posted_form_id = isset($input['form_id']) ?
+ preg_replace("/[^a-z0-9_-]/", "", (string) $input['form_id'])
+ : NULL;
+ $posted_captcha_sid = isset($input['captcha_sid']) ?
+ (int) $input['captcha_sid']
+ : NULL;
+ $posted_captcha_token = isset($input['captcha_token']) ?
+ preg_replace("/[^a-zA-Z0-9-_]/", "", (string) $input['captcha_token'])
+ : NULL;
+
+ if ($posted_form_id == $this_form_id) {
+ // Check if the posted CAPTCHA token is valid for the posted CAPTCHA
+ // session ID. Note that we could just check the validity of the CAPTCHA
+ // token and extract the CAPTCHA session ID from that (without looking at
+ // the actual posted CAPTCHA session ID). However, here we check both
+ // the posted CAPTCHA token and session ID: it is a bit more stringent
+ // and the database query should also be more efficient (because there is
+ // an index on the CAPTCHA session ID).
+ if ($posted_captcha_sid != NULL) {
+ $expected_captcha_token = \Drupal::database()
+ ->select('captcha_sessions', 'cs')
+ ->fields('cs', ['token'])
+ ->condition('csid', $posted_captcha_sid)
+ ->execute()
+ ->fetchField();
+
+ // If we do have a CAPTCHA token mismatch then log it.
+ try {
+ if (($expected_captcha_token !== $posted_captcha_token) && empty($input['captcha_cacheable'])) {
+ throw new \UnexpectedValueException('CAPTCHA session reuse attack detected.');
+ }
+ }
+ catch (\Exception $e) {
+ \Drupal::logger('captcha')->debug(
+ 'CAPTCHA session reuse attack detected on @form_id Posted CAPTCHA token: @posted_captcha_token Expected captcha token: @expected_captcha_token',
+ [
+ '@form_id' => $this_form_id,
+ '@expected_captcha_token' => var_export($expected_captcha_token, TRUE),
+ '@posted_captcha_token' => var_export($posted_captcha_token, TRUE),
+ ]
+ );
+ // Invalidate the CAPTCHA session.
+ $posted_captcha_sid = NULL;
+ }
+ }
+ }
+ else {
+ // The CAPTCHA session ID is specific to the posted form.
+ // Return NULL, so a new session will be generated for this other form.
+ $posted_captcha_sid = NULL;
+ }
+ }
+ return [$posted_form_id, $posted_captcha_sid];
+}
+
+/**
+ * CAPTCHA validation handler.
+ *
+ * This function is placed in the main captcha.module file to make sure that
+ * it is available (even for cached forms, which don't fire
+ * captcha_form_alter(), and subsequently don't include additional include
+ * files).
+ */
+function captcha_validate($element, FormStateInterface &$form_state) {
+
+ $captcha_info = $form_state->get('captcha_info');
+ $form_id = $captcha_info['this_form_id'];
+
+ // Get CAPTCHA response.
+ $captcha_response = $form_state->getValue('captcha_response');
+
+ // Get CAPTCHA session from CAPTCHA info.
+ // @todo is this correct in all cases: see comments in previous revisions?
+ $csid = $captcha_info['captcha_sid'];
+
+ // Bypass captcha validation if access attribute value is false.
+ if (empty($captcha_info['access'])) {
+ return FALSE;
+ }
+
+ // If the form is cacheable where all solution validation is handed off or if
+ // we found a session with a solution then continue with validation.
+ $is_cacheable = (bool) $form_state->getValue('captcha_cacheable', FALSE);
+
+ if ($is_cacheable) {
+ // Completely ignore the captcha_sessions table,
+ // since the captcha_sid can get reused by the cache.
+ $solution = FALSE;
+ $captcha_validate = $element['#captcha_validate'];
+ if (!function_exists($captcha_validate)) {
+ // Cacheable CAPTCHAs must provide their own validation function.
+ $form_state->setErrorByName('captcha', t('CAPTCHA configuration error: Contact the site administrator.'));
+ \Drupal::logger('CAPTCHA')->error(
+ 'CAPTCHA configuration error: cacheable CAPTCHA type %challenge did not provide a validation function.',
+ ['%challenge' => $captcha_info['captcha_type']]);
+ }
+ // Check the response with the CAPTCHA validation function.
+ // Apart from the traditional expected $solution and received $response,
+ // we also provide the CAPTCHA $element and $form_state
+ // arrays for more advanced use cases.
+ if (!$captcha_validate($solution, $captcha_response, $element, $form_state)) {
+ // Wrong answer.
+ $form_state->setErrorByName('captcha_response', _captcha_get_error_message());
+ // Update wrong response counter.
+ if (\Drupal::config('captcha.settings')->get('enable_stats', FALSE)) {
+ Drupal::state()->set('captcha.wrong_response_counter', Drupal::state()
+ ->get('captcha.wrong_response_counter', 0) + 1);
+ }
+
+ if (\Drupal::config('captcha.settings')
+ ->get('log_wrong_responses', FALSE)
+ ) {
+ \Drupal::logger('CAPTCHA')->notice(
+ '%form_id post blocked by CAPTCHA module: challenge %challenge (by module %module).',
+ [
+ '%form_id' => $form_id,
+ '%challenge' => $captcha_info['captcha_type'],
+ '%module' => $captcha_info['module'],
+ ]);
+ }
+ }
+ }
+ else {
+ $solution = \Drupal::database()
+ ->select('captcha_sessions', 'cs')
+ ->fields('cs', ['solution'])
+ ->condition('csid', $csid)
+ ->execute()
+ ->fetchField();
+
+ if ($solution !== FALSE) {
+ // Get CAPTCHA validate function or fall back on strict equality.
+ $captcha_validate = $element['#captcha_validate'];
+ if (!function_exists($captcha_validate)) {
+ $captcha_validate = 'captcha_validate_strict_equality';
+ }
+ // Check the response with the CAPTCHA validation function.
+ // Apart from the traditional expected $solution and received $response,
+ // we also provide the CAPTCHA $element and $form_state
+ // arrays for more advanced use cases.
+ if ($captcha_validate($solution, $captcha_response, $element, $form_state)) {
+
+ // Get the CAPTCHA persistence setting.
+ $captcha_persistence = \Drupal::config('captcha.settings')
+ ->get('persistence');
+
+ if (in_array($captcha_persistence,
+ [
+ CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL,
+ CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE,
+ ])) {
+ // Only save the success in $_SESSION if it is actually needed for
+ // further validation in _captcha_required_for_user(). Setting
+ // this kills the page cache so let's not be cavalier about it.
+ $_SESSION['captcha_success_form_ids'][$form_id] = $form_id;
+ }
+
+ // Record success.
+ \Drupal::database()->update('captcha_sessions')
+ ->condition('csid', $csid)
+ ->fields(['status' => CaptchaConstants::CAPTCHA_STATUS_SOLVED])
+ ->expression('attempts', 'attempts + 1')
+ ->execute();
+ }
+ else {
+ // Wrong answer.
+ \Drupal::database()->update('captcha_sessions')
+ ->condition('csid', $csid)
+ ->expression('attempts', 'attempts + 1')
+ ->execute();
+
+ $form_state->setErrorByName('captcha_response', _captcha_get_error_message());
+ // Update wrong response counter.
+ if (\Drupal::config('captcha.settings')->get('enable_stats', FALSE)) {
+ Drupal::state()->set('captcha.wrong_response_counter', Drupal::state()
+ ->get('captcha.wrong_response_counter', 0) + 1);
+ }
+
+ if (\Drupal::config('captcha.settings')
+ ->get('log_wrong_responses', FALSE)
+ ) {
+ \Drupal::logger('CAPTCHA')->notice(
+ '%form_id post blocked by CAPTCHA module: challenge %challenge (by module %module), user answered "@response", but the solution was "@solution".',
+ [
+ '%form_id' => $form_id,
+ '@response' => $captcha_response,
+ '@solution' => $solution,
+ '%challenge' => $captcha_info['captcha_type'],
+ '%module' => $captcha_info['module'],
+ ]);
+ }
+ }
+ }
+ else {
+ // If the session is gone and we can't confirm a solution error.
+ // Note: _captcha_get_posted_captcha_info() validates and triggers session
+ // rebuilds for re-use attacks during element processing so this should be
+ // rare if it ever happens.
+ $form_state->setErrorByName('captcha', t('CAPTCHA validation error: unknown CAPTCHA session ID. Contact the site administrator if this problem persists.'));
+ \Drupal::logger('CAPTCHA')->warning(
+ 'CAPTCHA validation error: unknown CAPTCHA session ID (%csid).',
+ ['%csid' => var_export($csid, TRUE)]);
+ }
+ }
+}
+
+/**
+ * Pre-render callback for additional processing of a CAPTCHA form element.
+ *
+ * This encompasses tasks that should happen after the general FAPI processing
+ * (building, submission and validation) but before rendering
+ * (e.g. storing the solution).
+ *
+ * @param array $element
+ * The CAPTCHA form element.
+ *
+ * @return array
+ * The manipulated element.
+ *
+ * @deprecated in captcha:8.x-1.0 and is removed from captcha:8.x-2.0.
+ * Use \Drupal\captcha\Element\Captcha::preRenderProcess() instead.
+ * @see https://www.drupal.org/project/captcha/issues/1949682
+ */
+function captcha_pre_render_process(array $element) {
+ return Captcha::preRenderProcess($element);
+}
+
+/**
+ * Default implementation of hook_captcha().
+ */
+function captcha_captcha($op, $captcha_type = '') {
+ switch ($op) {
+ case 'list':
+ return ['Math'];
+
+ case 'generate':
+ if ($captcha_type == 'Math') {
+ $result = [];
+ $answer = mt_rand(1, 20);
+ $x = mt_rand(1, $answer);
+ $y = $answer - $x;
+ $result['solution'] = "$answer";
+ // Build challenge widget.
+ // Note that we also use t() for the math challenge itself. This makes
+ // it possible to 'rephrase' the challenge a bit through localization
+ // or string overrides.
+ $result['form']['captcha_response'] = [
+ '#type' => 'textfield',
+ '#title' => t('Math question (@x + @y =)', ['@x' => $x, '@y' => $y]),
+ '#description' => t('Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.'),
+ '#size' => 4,
+ '#maxlength' => 2,
+ '#required' => TRUE,
+ '#attributes' => [
+ 'autocomplete' => 'off',
+ ],
+ '#cache' => ['max-age' => 0],
+ ];
+ \Drupal::service('page_cache_kill_switch')->trigger();
+
+ return $result;
+ }
+ elseif ($captcha_type == 'Test') {
+ // This challenge is not visible through the administrative interface
+ // as it is not listed in captcha_captcha('list'),
+ // but it is meant for debugging and testing purposes.
+ // @todo for Drupal 7 version: This should be done with a mock module,
+ // but Drupal 6 does not support this (mock modules can not be hidden).
+ $result = [
+ 'solution' => 'Test 123',
+ 'form' => [],
+ ];
+ $result['form']['captcha_response'] = [
+ '#type' => 'textfield',
+ '#title' => t('Test one two three'),
+ '#required' => TRUE,
+ '#cache' => ['max-age' => 0],
+ ];
+ \Drupal::service('page_cache_kill_switch')->trigger();
+
+ return $result;
+ }
+ break;
+ }
+}
+
+/**
+ * Parse values of whitelist ip addresses and ranges.
+ *
+ * @param string|null $whitelist_ips_value
+ * Contains list of ip addresses and ranges set one per line.
+ *
+ * @return array
+ * Array of parsed ip addresses and ranges.
+ */
+function captcha_whitelist_ips_parse_values($whitelist_ips_value) {
+ $whitelist_ips = [
+ CaptchaConstants::CAPTCHA_WHITELIST_IP_RANGE => [],
+ CaptchaConstants::CAPTCHA_WHITELIST_IP_ADDRESS => [],
+ ];
+
+ // Ensure the IPs value is trimmed before moving onward.
+ $whitelist_ips_value = trim($whitelist_ips_value ?? "");
+
+ if (empty($whitelist_ips_value)) {
+ return $whitelist_ips;
+ }
+
+ $value_rows = explode("\n", $whitelist_ips_value);
+ foreach ($value_rows as $value_row) {
+ $value_row = trim($value_row);
+ if (!empty($value_row) && strpos($value_row, '-') !== FALSE) {
+ $whitelist_ips[CaptchaConstants::CAPTCHA_WHITELIST_IP_RANGE][] = $value_row;
+ }
+ else {
+ $whitelist_ips[CaptchaConstants::CAPTCHA_WHITELIST_IP_ADDRESS][] = $value_row;
+ }
+ }
+
+ return $whitelist_ips;
+}
+
+/**
+ * Check if ip address is whitelisted.
+ *
+ * @param string $ip_address
+ * Optional. IP address to be checked if it is in whitelist. If no ip value
+ * provided user's current ip will be used to be verified.
+ *
+ * @return bool
+ * TRUE if requested IP address is whitelisted, FALSE if it is not.
+ */
+function captcha_whitelist_ip_whitelisted($ip_address = '') {
+ if (empty($ip_address)) {
+ $ip_address = Drupal::request()->getClientIp();
+ }
+
+ $config = \Drupal::config('captcha.settings');
+ $whitelist_ips_value = $config->get('whitelist_ips');
+ $whitelist_ips = captcha_whitelist_ips_parse_values($whitelist_ips_value);
+
+ if (in_array($ip_address, $whitelist_ips[CaptchaConstants::CAPTCHA_WHITELIST_IP_ADDRESS])) {
+ return TRUE;
+ }
+ elseif (empty($whitelist_ips[CaptchaConstants::CAPTCHA_WHITELIST_IP_RANGE])) {
+ return FALSE;
+ }
+
+ foreach ($whitelist_ips[CaptchaConstants::CAPTCHA_WHITELIST_IP_RANGE] as $ip_range) {
+ [$ip_lower, $ip_upper] = explode('-', $ip_range, 2);
+ $ip_lower_dec = (float) sprintf("%u", ip2long($ip_lower));
+ $ip_upper_dec = (float) sprintf("%u", ip2long($ip_upper));
+ $ip_address_dec = (float) sprintf("%u", ip2long($ip_address));
+ if (($ip_address_dec >= $ip_lower_dec) && ($ip_address_dec <= $ip_upper_dec)) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
diff --git a/web/modules/contrib/captcha/captcha.permissions.yml b/web/modules/contrib/captcha/captcha.permissions.yml
new file mode 100644
index 000000000..f745e2934
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.permissions.yml
@@ -0,0 +1,5 @@
+administer CAPTCHA settings:
+ title: 'Administer CAPTCHA settings'
+skip CAPTCHA:
+ title: 'Skip CAPTCHA'
+ description: 'Users with this permission will not be offered a CAPTCHA.'
diff --git a/web/modules/contrib/captcha/captcha.post_update.php b/web/modules/contrib/captcha/captcha.post_update.php
new file mode 100644
index 000000000..8512774f3
--- /dev/null
+++ b/web/modules/contrib/captcha/captcha.post_update.php
@@ -0,0 +1,13 @@
+.form-item {
+ flex: 1 1 calc(100% - var(--image-width) - var(--gap-h));
+ min-width: 13em;
+ margin: 0;
+}
+
+.captcha-type-challenge--image .captcha__element>.form-item>label {
+ margin-top: 0;
+}
+
+.captcha__image-wrapper {
+ position: relative;
+ flex: 0 0 var(--image-width);
+}
+
+.captcha__image-wrapper img {
+ display: block;
+ max-width: 100%;
+ border: 1px solid #eee;
+ padding: .35rem;
+ margin-bottom: .5rem;
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/css/image_captcha_refresh.css b/web/modules/contrib/captcha/modules/image_captcha/css/image_captcha_refresh.css
new file mode 100644
index 000000000..86ea703c8
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/css/image_captcha_refresh.css
@@ -0,0 +1,38 @@
+/* Show "Get new captcha"-link as simple icon */
+.reload-captcha {
+ display: block;
+ width: var(--reload-icon-size);
+ height: var(--reload-icon-size);
+ background-image: url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 56 56' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg transform='matrix(1,0,0,1,-344.937,-314.119)'%3E%3Cpath d='M399.28,314.12L390.155,321.37C385.356,317.346 379.193,314.901 372.499,314.901C357.37,314.901 344.937,327.335 344.937,342.463C344.937,357.591 357.371,369.744 372.499,369.744C384.359,369.744 394.551,362.29 398.405,351.775L388.53,349.369C385.887,355.601 379.763,359.9 372.499,359.9C362.775,359.9 355.061,352.187 355.061,342.462C355.061,332.737 362.774,324.743 372.499,324.743C376.107,324.743 379.422,325.86 382.186,327.743L373.155,334.931L399.28,339.774L399.28,314.118L399.28,314.12Z' style='fill-rule:nonzero;'/%3E%3C/g%3E%3C/svg%3E%0A");
+ background-repeat: no-repeat;
+ font-size: 0px;
+ text-align: left;
+ opacity: 0.7;
+ transition-property: opacity;
+ transition-duration: 150ms;
+ transition-timing-function: linear;
+}
+
+.reload-captcha:hover {
+ opacity: 1;
+}
+
+/* Ensure the reload button spins around once at minimum when clicked */
+.reload-captcha:focus:not(:focus-visible) {
+ animation: image_captcha_refresh_spin 500ms linear 1;
+}
+
+/* Loading Formatter: Rotate refresh button while loading new CAPTCHA image */
+.captcha--loading .reload-captcha {
+ animation: image_captcha_refresh_spin 500ms linear infinite;
+}
+
+@keyframes image_captcha_refresh_spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/fonts/README.txt b/web/modules/contrib/captcha/modules/image_captcha/fonts/README.txt
new file mode 100644
index 000000000..452586d74
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/fonts/README.txt
@@ -0,0 +1,7 @@
+
+It possible to put your own fonts for the Image CAPTCHA in this folder.
+However, this is not the recommended way, as they can get lost easily during
+a module update. The recommended way to provide your own fonts is putting them
+in the files directory of your Drupal setup or, just like with contributed
+modules and themes, in the "libraries" folders sites/all/libraries/fonts
+or sites//libraries/fonts.
diff --git a/web/modules/contrib/captcha/modules/image_captcha/fonts/Tesox/tesox.ttf b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tesox/tesox.ttf
new file mode 100644
index 000000000..31f91d349
Binary files /dev/null and b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tesox/tesox.ttf differ
diff --git a/web/modules/contrib/captcha/modules/image_captcha/fonts/Tesox/tesox_readme.txt b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tesox/tesox_readme.txt
new file mode 100644
index 000000000..97e2b6286
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tesox/tesox_readme.txt
@@ -0,0 +1,24 @@
+
+The Tesox typeface
+==================
+
+The Tesox typeface is created by Stefaan Lippens (also known as soxofaan on
+drupal.org, http://drupal.org/user/41478).
+It is based on hand drawn characters, converted to a TrueType font with the
+FontCapture web service (http://www.fontcapture.com).
+
+Background
+----------
+The Tesox typeface is created specifically for the image CAPTCHA module
+for Drupal (http://drupal.org/project/captcha). For a better out-of-the-box
+experience it was desired to include one or more typefaces with the CAPTCHA
+module package by default. However, this redistribution raised licensing issues.
+For example, according the code hosting policy of drupal.org, only GPL licensed
+code and resources are allowed in the drupal.org code repository (CVS).
+To avoid licensing and redistribution issues, it was decided to create a
+dedicated typeface for the image CAPTCHA module from scratch.
+
+Licencing
+---------
+The Tesox typeface is GPLv2 licensed to be compatible with the drupal.org code
+hosting and packaging policies, as explained above.
diff --git a/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/README.txt b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/README.txt
new file mode 100644
index 000000000..205343fb5
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/README.txt
@@ -0,0 +1,23 @@
+
+This directory contains a subset (Regular and Bold) of the Tuffy typeface
+created by Thatcher Ulrich (http://tulrich.com/fonts) and released in the
+public domain.
+
+Original licensing statement of the creator
+-------------------------------------------
+Here are my dabblings in font design. I have placed them in the Public Domain.
+This is all 100% my own work. Usage is totally unrestricted.
+If you want to make derivative works for any purpose, please go ahead.
+
+I welcome comments & constructive criticism.
+
+Put another way, a la PD-self (http://en.wikipedia.org/wiki/Template:PD-self):
+ I, the copyright holder of this work, hereby release it into the public
+ domain. This applies worldwide.
+
+ In case this is not legally possible,
+
+ I grant any entity the right to use this work for any purpose,
+ without any conditions, unless such conditions are required by law.
+
+-Thatcher Ulrich http://tulrich.com
diff --git a/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/Tuffy.ttf b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/Tuffy.ttf
new file mode 100644
index 000000000..8ea647090
Binary files /dev/null and b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/Tuffy.ttf differ
diff --git a/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/Tuffy_Bold.ttf b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/Tuffy_Bold.ttf
new file mode 100644
index 000000000..9574aab6e
Binary files /dev/null and b/web/modules/contrib/captcha/modules/image_captcha/fonts/Tuffy/Tuffy_Bold.ttf differ
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.admin.inc b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.admin.inc
new file mode 100644
index 000000000..a0de45eec
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.admin.inc
@@ -0,0 +1,72 @@
+get('image_captcha_fonts_preview_map_cache');
+ if (!isset($fonts[$font_token])) {
+ print 'Bad token';
+ exit();
+ }
+ // Get the font path.
+ $font = $fonts[$font_token]->uri;
+ // Some sanity checks if the given font is valid.
+ if (!is_file($font) || !is_readable($font)) {
+ print 'Bad font';
+ exit();
+ }
+ }
+
+ // Settings of the font preview.
+ $width = 120;
+ $text = 'AaBbCc123';
+ $font_size = 14;
+ $height = 2 * $font_size;
+
+ // Allocate image resource.
+ $image = imagecreatetruecolor($width, $height);
+ if (!$image) {
+ exit();
+ }
+ // White background and black foreground.
+ $background_color = imagecolorallocate($image, 255, 255, 255);
+ $color = imagecolorallocate($image, 0, 0, 0);
+ imagefilledrectangle($image, 0, 0, $width, $height, $background_color);
+
+ // Draw preview text.
+ if ($font == 'BUILTIN') {
+ imagestring($image, 5, 1, .5 * $height - 10, $text, $color);
+ }
+ else {
+ imagettftext($image, $font_size, 0, 1, 1.5 * $font_size, $color, realpath($font), $text);
+ }
+
+ $response = new Response();
+ $response->headers->set('Content-Type', 'image/png');
+ // Dump image data to client.
+ imagepng($image);
+ // Release image memory.
+ imagedestroy($image);
+ unset($image);
+
+ // Close connection.
+ exit();
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.info.yml b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.info.yml
new file mode 100644
index 000000000..82c600936
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.info.yml
@@ -0,0 +1,13 @@
+name: Image CAPTCHA
+type: module
+description: Provides an image based CAPTCHA.
+package: Spam control
+core_version_requirement: ^9.5 || ^10 || ^11
+dependencies:
+ - captcha:captcha
+configure: image_captcha.settings
+
+# Information added by Drupal.org packaging script on 2025-07-28
+version: '2.0.9'
+project: 'captcha'
+datestamp: 1753701291
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.install b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.install
new file mode 100644
index 000000000..462c93db5
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.install
@@ -0,0 +1,95 @@
+ \Drupal::translation()
+ ->translate('Image CAPTCHA requires GD library'),
+ 'description' =>
+ \Drupal::translation()
+ ->translate('The Image CAPTCHA module can not be installed because your PHP setup does not provide the GD library, which is required to generate images.',
+ ['!gddoc' => 'http://www.php.net/manual/en/book.image.php']
+ ),
+ 'severity' => REQUIREMENT_ERROR,
+ ];
+ }
+ }
+ return $requirements;
+}
+
+/**
+ * Implements hook_install().
+ */
+function image_captcha_install() {
+ $config = \Drupal::configFactory()->getEditable('image_captcha.settings');
+ $module_path = \Drupal::service('extension.list.module')->getPath('image_captcha');
+ $config->set('image_captcha_fonts', [
+ hash('sha256', $module_path . '/fonts/Tesox/tesox.ttf'),
+ hash('sha256', $module_path . '/fonts/Tuffy/Tuffy.ttf'),
+ ])->save(TRUE);
+}
+
+/**
+ * Convert existing setting to hashes.
+ */
+function image_captcha_update_8001() {
+ $config = \Drupal::configFactory()->getEditable('image_captcha.settings');
+
+ foreach ($config->get('image_captcha_fonts') as $index => $font) {
+ if (!empty($font) && strpos($font, '.ttf') !== FALSE) {
+ $config->set('image_captcha_fonts.' . $index, hash('sha256', $font));
+ }
+ }
+ $config->save(TRUE);
+}
+
+/**
+ * Fix possibly broken image_captcha fonts path after 8.x-1.7 update.
+ */
+function image_captcha_update_9001(&$sandbox) {
+ // Reset the font path from the correct module directory location:
+ $config = \Drupal::configFactory()->getEditable('image_captcha.settings');
+ $module_path = \Drupal::service('extension.list.module')->getPath('image_captcha');
+ $config->set('image_captcha_fonts', [
+ hash('sha256', $module_path . '/fonts/Tesox/tesox.ttf'),
+ hash('sha256', $module_path . '/fonts/Tuffy/Tuffy.ttf'),
+ ])->save(TRUE);
+}
+
+/**
+ * Set captcha 'title' default, as the setting did not exist before.
+ */
+function image_captcha_update_9002(&$sandbox) {
+ $config = \Drupal::configFactory()->getEditable('image_captcha.settings');
+ $config->set('title', 'CAPTCHA')->save(TRUE);
+}
+
+/**
+ * Remove image_captcha "title" config as it never should have existed.
+ */
+function image_captcha_update_9003(&$sandbox) {
+ $imageCaptchaConfig = \Drupal::configFactory()->getEditable('image_captcha.settings');
+ $imageCaptchaConfig->clear('title')->save(TRUE);
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.libraries.yml b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.libraries.yml
new file mode 100644
index 000000000..14cd986ab
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.libraries.yml
@@ -0,0 +1,31 @@
+base:
+ version: VERSION
+ css:
+ theme:
+ css/image_captcha.css: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - core/drupalSettings
+
+image-captcha-refresh:
+ version: VERSION
+ js:
+ js/image_captcha_refresh.js: {}
+ css:
+ theme:
+ css/image_captcha_refresh.css: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - image_captcha/base
+
+admin:
+ version: VERSION
+ js:
+ js/admin.image_captcha.js: {}
+ css:
+ theme:
+ css/admin.image_captcha.css: {}
+ dependencies:
+ - image_captcha/base
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.links.menu.yml b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.links.menu.yml
new file mode 100644
index 000000000..b2f3731dc
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.links.menu.yml
@@ -0,0 +1,6 @@
+image_captcha.settings:
+ title: 'Image Captcha'
+ route_name: image_captcha.settings
+ description: 'Configure Image Captcha Settings.'
+ parent: captcha.settings
+ weight: -1
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.links.task.yml b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.links.task.yml
new file mode 100644
index 000000000..0e18abcbf
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.links.task.yml
@@ -0,0 +1,5 @@
+image_captcha.settings:
+ title: 'Image Captcha'
+ route_name: image_captcha.settings
+ base_route: captcha_settings
+ weight: -1
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.module b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.module
new file mode 100644
index 000000000..edad41732
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.module
@@ -0,0 +1,367 @@
+' . t('The image CAPTCHA is a popular challenge where a random textual code is obfuscated in an image. The image is generated on the fly for each request, which is rather CPU intensive for the server. Be careful with the size and computation related settings.') . '
';
+ return $output;
+ }
+}
+
+/**
+ * Getter for fonts to use in the image CAPTCHA.
+ *
+ * @return array
+ * List of font paths.
+ */
+function _image_captcha_get_enabled_fonts() {
+ if (ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT & _image_captcha_check_setup(FALSE)) {
+ return ['BUILTIN'];
+ }
+ else {
+ return \Drupal::config('image_captcha.settings')
+ ->get('image_captcha_fonts');
+ }
+}
+
+/**
+ * Helper function to get font(s).
+ *
+ * @return string|array
+ * URI of file hash or List of font paths.
+ */
+function _image_captcha_get_font_uri($token = NULL) {
+ $fonts = [
+ 'BUILTIN' => 'BUILTIN',
+ ];
+ $available_fonts = _image_captcha_get_available_fonts_from_directories();
+ foreach ($available_fonts as $file_token => $font_info) {
+ $fonts[$file_token] = $font_info['uri'];
+ }
+ return ((!empty($token) && !empty($fonts[$token])) ? $fonts[$token] : $fonts);
+}
+
+/**
+ * Helper function to get fonts from the given directories.
+ *
+ * @param array|null $directories
+ * (Optional) an array of directories
+ * to recursively search through, if not given, the default
+ * directories will be used.
+ *
+ * @return array
+ * Fonts file objects (with fields 'name',
+ * 'basename' and 'filename'), keyed on the sha256 hash of the font
+ * path (to have an easy token that can be used in an url
+ * without en/decoding issues).
+ */
+function _image_captcha_get_available_fonts_from_directories($directories = NULL) {
+ // If no fonts directories are given: use the default.
+ if ($directories === NULL) {
+ $request = \Drupal::service('request_stack')->getCurrentRequest();
+ $directories = [
+ \Drupal::service('extension.list.module')->getPath('image_captcha') . '/fonts',
+ 'sites/all/libraries/fonts',
+ DrupalKernel::findSitePath($request) . '/libraries/fonts',
+ ];
+ }
+ // Collect the font information.
+ $fonts = [];
+ foreach ($directories as $directory) {
+ if (is_dir($directory) && is_readable($directory)) {
+ $files = \Drupal::service('file_system')->scanDirectory($directory, '/\.[tT][tT][fF]$/');
+ foreach ($files as $filename => $font) {
+ $fonts[hash('sha256', $filename)] = (array) $font;
+ }
+ }
+ }
+
+ return $fonts;
+}
+
+/**
+ * Helper function for checking if the specified fonts are available.
+ *
+ * @param array $fonts
+ * Paths of fonts to check.
+ *
+ * @return array
+ * List($readable_fonts, $problem_fonts).
+ */
+function _image_captcha_check_fonts(array $fonts) {
+ $problem_fonts = [];
+
+ foreach ($fonts as $font) {
+ if ($font != 'BUILTIN' && (!is_file($font) || !is_readable($font))) {
+ $problem_fonts[] = $font;
+ }
+ }
+
+ return $problem_fonts;
+}
+
+/**
+ * Helper function for splitting an utf8 string correctly in characters.
+ *
+ * Assumes the given utf8 string is well formed.
+ * See http://en.wikipedia.org/wiki/Utf8 for more info.
+ *
+ * @param string $str
+ * UTF8 string to be split.
+ *
+ * @return array
+ * List of characters given string consists of.
+ */
+function _image_captcha_utf8_split($str) {
+ $characters = [];
+ $len = mb_strlen($str);
+
+ for ($i = 0; $i < $len;) {
+ $chr = ord($str[$i]);
+ // One byte character (0zzzzzzz).
+ if (($chr & 0x80) == 0x00) {
+ $width = 1;
+ }
+ else {
+ // Two byte character (first byte: 110yyyyy).
+ if (($chr & 0xE0) == 0xC0) {
+ $width = 2;
+ }
+ // Three byte character (first byte: 1110xxxx).
+ elseif (($chr & 0xF0) == 0xE0) {
+ $width = 3;
+ }
+ // Four byte character (first byte: 11110www).
+ elseif (($chr & 0xF8) == 0xF0) {
+ $width = 4;
+ }
+ else {
+ \Drupal::logger('CAPTCHA')
+ ->error('Encountered an illegal byte while splitting an utf8 string in characters.');
+ return $characters;
+ }
+ }
+
+ $characters[] = mb_substr($str, $i, $width);
+ $i += $width;
+ }
+
+ return $characters;
+}
+
+/**
+ * Helper function for checking the setup of the Image CAPTCHA.
+ *
+ * The image CAPTCHA requires at least the GD PHP library.
+ * Support for TTF is recommended and the enabled
+ * font files should be readable.
+ * This functions checks these things.
+ *
+ * @param bool $check_fonts
+ * Whether or not the enabled fonts should be checked.
+ *
+ * @return int
+ * Status code: bitwise 'OR' of status flags like
+ * IMAGE_CAPTCHA_ERROR_NO_GDLIB, IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT,
+ * IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM.
+ */
+function _image_captcha_check_setup($check_fonts = TRUE) {
+ $status = 0;
+ // Check if we can use the GD library.
+ // We need at least the imagepng function.
+ // Note that the imagejpg function is optionally also used, but not required.
+ if (!function_exists('imagepng')) {
+ $status = $status | ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_NO_GDLIB;
+ }
+
+ if (!function_exists('imagettftext')) {
+ $status = $status | ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT;
+ }
+
+ if ($check_fonts) {
+ // Check availability of enabled fonts.
+ $fonts = _image_captcha_get_enabled_fonts();
+ if (empty($fonts)) {
+ // Config value might be wrong, try to reinstall the field.
+ require_once __DIR__ . '/image_captcha.install';
+ image_captcha_install();
+
+ // Try again now.
+ $fonts = _image_captcha_get_enabled_fonts();
+ if (empty($fonts)) {
+ $status = $status | ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM;
+ }
+ }
+ if (!empty($fonts)) {
+ $problem_fonts = _image_captcha_check_fonts($fonts);
+ if (count($problem_fonts) != 0) {
+ $status = $status | ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM;
+ }
+ }
+ }
+
+ return $status;
+}
+
+/**
+ * Helper function for calculating image height and width.
+ *
+ * They are based on given code and current font/spacing settings.
+ *
+ * @param string $code
+ * The utf8 string which will be used to split in characters.
+ *
+ * @return array
+ * [$width, $heigh].
+ */
+function _image_captcha_image_size($code) {
+ $config = \Drupal::config('image_captcha.settings');
+ $font_size = (int) $config->get('image_captcha_font_size');
+ $character_spacing = (float) $config->get('image_captcha_character_spacing');
+ $characters = _image_captcha_utf8_split($code);
+ $character_quantity = count($characters);
+
+ // Calculate height and width.
+ $width = $character_spacing * $font_size * $character_quantity;
+ $height = 2 * $font_size;
+
+ // Return the values as full pixel values (no floats):
+ return [(int) $width, (int) $height];
+}
+
+/**
+ * Implements hook_captcha().
+ */
+function image_captcha_captcha($op, $captcha_type = '', $captcha_sid = NULL) {
+ $config = \Drupal::config('image_captcha.settings');
+
+ switch ($op) {
+ case 'list':
+ // Only offer the image CAPTCHA if it is possible to generate an image
+ // on this setup.
+ if (!(_image_captcha_check_setup() & ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_NO_GDLIB)) {
+ return ['Image'];
+ }
+ else {
+ return [];
+ }
+ break;
+
+ case 'generate':
+ if ($captcha_type == 'Image') {
+ // In maintenance mode, the image CAPTCHA does not work because
+ // the request for the image itself won't succeed (only ?q=user
+ // is permitted for unauthenticated users). We fall back to the
+ // Math CAPTCHA in that case.
+ if (\Drupal::state()->get('system.maintenance_mode')
+ && \Drupal::currentUser()->isAnonymous()
+ ) {
+ return captcha_captcha('generate', 'Math');
+ }
+ // Generate a CAPTCHA code.
+ $allowed_chars = _image_captcha_utf8_split($config->get('image_captcha_image_allowed_chars'));
+ $code_length = (int) $config->get('image_captcha_code_length');
+ $code = '';
+
+ for ($i = 0; $i < $code_length; $i++) {
+ $code .= $allowed_chars[array_rand($allowed_chars)];
+ }
+
+ // Build the result to return.
+ $result = [];
+
+ $result['solution'] = $code;
+ // Add CAPTCHA image wrapper (holds the refresh button + the image
+ // itself)
+ $result['form']['captcha_image_wrapper'] = [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['captcha__image-wrapper']],
+ ];
+ // Generate image source URL (add timestamp to avoid problems with
+ // client side caching: subsequent images of the same CAPTCHA session
+ // have the same URL, but should display a different code).
+ [$width, $height] = _image_captcha_image_size($code);
+ $result['form']['captcha_image_wrapper']['captcha_image'] = [
+ '#theme' => 'image',
+ '#uri' => Url::fromRoute('image_captcha.generator', [
+ 'session_id' => $captcha_sid,
+ 'timestamp' => \Drupal::time()->getRequestTime(),
+ ])->toString(),
+ '#width' => $width,
+ '#height' => $height,
+ '#alt' => t('Image CAPTCHA'),
+ '#title' => t('Image CAPTCHA'),
+ '#weight' => -2,
+ ];
+
+ $result['form']['captcha_response'] = [
+ '#type' => 'textfield',
+ '#title' => t('What code is in the image?'),
+ '#description' => t('Enter the characters shown in the image.'),
+ '#weight' => 0,
+ '#required' => TRUE,
+ '#size' => 15,
+ '#attributes' => ['autocomplete' => 'off'],
+ '#cache' => ['max-age' => 0],
+ ];
+
+ // Handle the case insensitive validation option combined with
+ // ignoring spaces.
+ switch (\Drupal::config('captcha.settings')
+ ->get('default_validation')) {
+ case CaptchaConstants::CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE:
+ $result['captcha_validate'] = 'captcha_validate_ignore_spaces';
+ break;
+
+ case CaptchaConstants::CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE:
+ $result['captcha_validate'] = 'captcha_validate_case_insensitive_ignore_spaces';
+ break;
+ }
+ \Drupal::service('page_cache_kill_switch')->trigger();
+
+ return $result;
+ }
+ break;
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function image_captcha_theme() {
+ return [
+ 'image_captcha_refresh' => [
+ 'variables' => ['captcha_refresh_link' => NULL],
+ ],
+ ];
+}
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function image_captcha_element_info_alter(array &$info) {
+ if (!empty($info['captcha'])) {
+ // Register the process callback. Sadly we can't determine here safely yet,
+ // if the processed captcha type is an image_captcha. That has to be done
+ // inside the #process callback.
+ $info['captcha']['#process'][] = [
+ ImageCaptchaRenderService::class,
+ 'imageCaptchaAfterBuildProcess',
+ ];
+ }
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.routing.yml b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.routing.yml
new file mode 100644
index 000000000..9a03e9532
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.routing.yml
@@ -0,0 +1,29 @@
+image_captcha.settings:
+ path: '/admin/config/people/captcha/image_captcha'
+ defaults:
+ _form: '\Drupal\image_captcha\Form\ImageCaptchaSettingsForm'
+ requirements:
+ _permission: 'administer CAPTCHA settings'
+
+image_captcha.font_preview:
+ path: '/admin/config/people/captcha/image_captcha/font_preview/{token}'
+ defaults:
+ _controller: '\Drupal\image_captcha\Controller\CaptchaFontPreviewController::getFont'
+ requirements:
+ _permission: 'administer CAPTCHA settings'
+
+image_captcha.generator:
+ path: '/image-captcha-generate/{session_id}/{timestamp}'
+ defaults:
+ _controller: '\Drupal\image_captcha\Controller\CaptchaImageGeneratorController::image'
+ requirements:
+ # Allow access, because anybody is allowed to generate the image captcha:
+ _access: 'TRUE'
+
+image_captcha.refresh:
+ path: '/image-captcha-refresh/{form_id}'
+ defaults:
+ _controller: '\Drupal\image_captcha\Controller\CaptchaImageRefreshController::refreshCaptcha'
+ requirements:
+ # Allow access, because anybody is allowed to refresh the image captcha:
+ _access: 'TRUE'
diff --git a/web/modules/contrib/captcha/modules/image_captcha/image_captcha.services.yml b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.services.yml
new file mode 100644
index 000000000..7581fa082
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/image_captcha.services.yml
@@ -0,0 +1,4 @@
+services:
+ image_captcha.render_service:
+ class: Drupal\image_captcha\Service\ImageCaptchaRenderService
+ arguments: [ '@config.factory', '@database', '@file_system']
diff --git a/web/modules/contrib/captcha/modules/image_captcha/js/admin.image_captcha.js b/web/modules/contrib/captcha/modules/image_captcha/js/admin.image_captcha.js
new file mode 100644
index 000000000..0bdaa9c8a
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/js/admin.image_captcha.js
@@ -0,0 +1,52 @@
+/**
+ * @file
+ * Contains helper js for Captcha admin pages.
+ *
+ * @TODO remove and use native states.
+ */
+
+(function ($) {
+ 'use strict';
+
+ Drupal.behaviors.captchaAdmin = {
+ attach: function (context) {
+
+ // Helper function to show/hide noise level widget.
+ var noise_level_shower = function (speed) {
+ speed = (typeof speed == 'undefined') ? 'slow' : speed;
+ if ($('#edit-image-captcha-dot-noise').is(':checked')
+ || $('#edit-image-captcha-line-noise').is(':checked')) {
+ $('.form-item-image-captcha-noise-level').show(speed);
+ }
+ else {
+ $('.form-item-image-captcha-noise-level').hide(speed);
+ }
+ };
+
+ // Add onclick handler to the dot and line noise check boxes.
+ $('#edit-image-captcha-dot-noise').click(noise_level_shower);
+ $('#edit-image-captcha-line-noise').click(noise_level_shower);
+ // Show or hide appropriately on page load.
+ noise_level_shower(0);
+
+ // Helper function to show/hide smooth distortion widget.
+ var smooth_distortion_shower = function (speed) {
+ speed = (typeof speed == 'undefined') ? 'slow' : speed;
+ if ($('#edit-image-captcha-distortion-amplitude').val() > 0) {
+ $('.form-item-image-captcha-bilinear-interpolation').show(speed);
+ }
+ else {
+ $('.form-item-image-captcha-bilinear-interpolation').hide(speed);
+ }
+ };
+
+ // Add onchange handler to the distortion level select widget.
+ $('#edit-image-captcha-distortion-amplitude').change(
+ smooth_distortion_shower);
+ // Show or hide appropriately on page load.
+ smooth_distortion_shower(0);
+
+ }
+ };
+
+})(jQuery);
diff --git a/web/modules/contrib/captcha/modules/image_captcha/js/image_captcha_refresh.js b/web/modules/contrib/captcha/modules/image_captcha/js/image_captcha_refresh.js
new file mode 100644
index 000000000..63f3a142b
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/js/image_captcha_refresh.js
@@ -0,0 +1,48 @@
+/**
+ * @file
+ * Attaches behaviors for the zipang captcha refresh module.
+ */
+
+(function ($) {
+ "use strict";
+
+ /**
+ * Attaches jQuery validate behavior to forms.
+ *
+ * @type {Drupal~behavior}
+ *
+ * @prop {Drupal~behaviorAttach} attach
+ * Attaches the outline behavior to the right context.
+ */
+ Drupal.behaviors.CaptchaRefresh = {
+ attach: function (context) {
+ $('.reload-captcha', context).not('.processed').bind('click', function () {
+ $(this).addClass('processed');
+ const $form = $(this).parents('form');
+ // Send post query for getting new captcha data.
+ const date = new Date();
+ const baseUrl = document.location.origin;
+ const url = baseUrl.replace(/\/$/g, '') + '/' + $(this).attr('href').replace(/^\//g, '') + '?' + date.getTime();
+ // Adding loader.
+ $('.captcha').addClass('captcha--loading');
+ $.get(
+ url,
+ {},
+ function (response) {
+ if (response.status === 1) {
+ $('.captcha', $form).find('img').attr('src', response.data.url);
+ $('input[name=captcha_sid]', $form).val(response.data.sid);
+ $('input[name=captcha_token]', $form).val(response.data.token);
+ $('.captcha').removeClass('captcha--loading');
+ }
+ else {
+ alert(response.message);
+ }
+ },
+ 'json'
+ );
+ return false;
+ });
+ }
+ };
+})(jQuery);
diff --git a/web/modules/contrib/captcha/modules/image_captcha/src/Constants/ImageCaptchaConstants.php b/web/modules/contrib/captcha/modules/image_captcha/src/Constants/ImageCaptchaConstants.php
new file mode 100644
index 000000000..2ba4562f6
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/src/Constants/ImageCaptchaConstants.php
@@ -0,0 +1,28 @@
+config = $config;
+ $this->killSwitch = $kill_switch;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('config.factory')->get('image_captcha.settings'),
+ $container->get('page_cache_kill_switch')
+ );
+ }
+
+ /**
+ * Main method that throw ImageResponse object to generate image.
+ *
+ * @return \Symfony\Component\HttpFoundation\StreamedResponse
+ * Make a StreamedResponse with the correct configuration and return it.
+ */
+ public function getFont($token) {
+ $this->killSwitch->trigger();
+ return new StreamedResponse(function () use ($token) {
+ // Get the font from the given font token.
+ if ($token == 'BUILTIN') {
+ $font = 'BUILTIN';
+ }
+ else {
+ // Get the mapping of font tokens to font file objects.
+ $fonts = $this->config->get('image_captcha_fonts_preview_map_cache');
+ if (!isset($fonts[$token])) {
+ throw new \LogicException('Given font token does not exist.');
+ }
+ // Get the font path.
+ $font = $fonts[$token]['uri'];
+ // Some sanity checks if the given font is valid.
+ if (!is_file($font) || !is_readable($font)) {
+ throw new \LogicException('Font could not be loaded.');
+ }
+ }
+ // Settings of the font preview.
+ $width = 120;
+ $text = 'AaBbCc123';
+ $font_size = 14;
+ $height = 2 * $font_size;
+
+ // Allocate image resource.
+ $image = imagecreatetruecolor($width, $height);
+ if (!$image) {
+ return NULL;
+ }
+ // White background and black foreground.
+ $background_color = imagecolorallocate($image, 255, 255, 255);
+ $color = imagecolorallocate($image, 0, 0, 0);
+ imagefilledrectangle($image, 0, 0, $width, $height, $background_color);
+
+ // Draw preview text.
+ if ($font == 'BUILTIN') {
+ imagestring($image, 5, 1, .5 * $height - 10, $text, $color);
+ }
+ else {
+ imagettftext($image, $font_size, 0, 1, 1.5 * $font_size, $color, realpath($font), $text);
+ }
+ // Dump image data to client.
+ imagepng($image);
+ // Release image memory.
+ imagedestroy($image);
+ unset($image);
+ }, 200, ['Content-Type' => 'image/png']);
+ }
+
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/src/Controller/CaptchaImageGeneratorController.php b/web/modules/contrib/captcha/modules/image_captcha/src/Controller/CaptchaImageGeneratorController.php
new file mode 100644
index 000000000..26b962aff
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/src/Controller/CaptchaImageGeneratorController.php
@@ -0,0 +1,151 @@
+config = $config;
+ $this->logger = $logger;
+ $this->killSwitch = $kill_switch;
+ $this->connection = $connection;
+ $this->fileSystem = $file_system;
+ $this->imageRender = $image_capcha_render;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('config.factory')->get('image_captcha.settings'),
+ $container->get('logger.factory')->get('captcha'),
+ $container->get('page_cache_kill_switch'),
+ $container->get('database'),
+ $container->get('file_system'),
+ $container->get('image_captcha.render_service')
+ );
+ }
+
+ /**
+ * Main method that throw ImageResponse object to generate image.
+ *
+ * @return \Symfony\Component\HttpFoundation\StreamedResponse
+ * Make a StreamedResponse with the correct configuration and return it.
+ */
+ public function image(Request $request) {
+ $this->killSwitch->trigger();
+
+ // Process the response headers for the image.
+ if ($this->config->get('image_captcha_file_format') == ImageCaptchaConstants::IMAGE_CAPTCHA_FILE_FORMAT_JPG) {
+ $response_headers = ['content-type' => 'image/jpeg'];
+ }
+ else {
+ $response_headers = ['content-type' => 'image/png'];
+ }
+ $response_headers['cache-control'] = 'no-store, must-revalidate';
+
+ // Check for existing session IDs.
+ $session_id = $request->get('session_id');
+ $code = $this->connection
+ ->select('captcha_sessions', 'cs')
+ ->fields('cs', ['solution'])
+ ->condition('csid', $session_id)
+ ->execute()
+ ->fetchField();
+
+ // If there is an existing session, process the image.
+ $image = NULL;
+ if ($code !== FALSE) {
+ $image = $this->imageRender->generateImage($code);
+
+ if (!$image) {
+ $this->logger->log('error', 'Generation of image CAPTCHA failed. Check your image CAPTCHA configuration and especially the used font.', []);
+ }
+ }
+ return new StreamedResponse(function () use ($image) {
+ if (!$image) {
+ return $this;
+ }
+
+ // Begin capturing the byte stream.
+ ob_start();
+
+ // Get the file format of the image.
+ $file_format = $this->config->get('image_captcha_file_format');
+ if ($file_format == ImageCaptchaConstants::IMAGE_CAPTCHA_FILE_FORMAT_JPG) {
+ imagejpeg($image);
+ }
+ else {
+ imagepng($image);
+
+ }
+ // Release image memory.
+ imagedestroy($image);
+ unset($image);
+ return $this;
+
+ }, 200, $response_headers);
+ }
+
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/src/Controller/CaptchaImageRefreshController.php b/web/modules/contrib/captcha/modules/image_captcha/src/Controller/CaptchaImageRefreshController.php
new file mode 100644
index 000000000..e41592c03
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/src/Controller/CaptchaImageRefreshController.php
@@ -0,0 +1,96 @@
+time = $time;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('datetime.time')
+ );
+ }
+
+ /**
+ * Put your code here.
+ */
+ public function refreshCaptcha($form_id = NULL) {
+ $result = [
+ 'status' => 0,
+ 'message' => '',
+ ];
+ try {
+ $this->moduleHandler()->loadInclude('captcha', 'inc', 'captcha');
+ $config = $this->config('image_captcha.settings');
+ $captcha_sid = _captcha_generate_captcha_session($form_id);
+ $captcha_token = Crypt::randomBytesBase64();
+ $allowed_char = $config->get('image_captcha_image_allowed_chars') ? $config->get('image_captcha_image_allowed_chars') : ImageCaptchaConstants::IMAGE_CAPTCHA_ALLOWED_CHARACTERS;
+ $allowed_chars = _image_captcha_utf8_split($allowed_char);
+ $code_length = (int) $config->get('image_captcha_code_length');
+ $code = '';
+ for ($i = 0; $i < $code_length; $i++) {
+ $code .= $allowed_chars[array_rand($allowed_chars)];
+ }
+ $connection = Database::getConnection();
+ $connection->update('captcha_sessions')
+ ->fields([
+ 'token' => $captcha_token,
+ 'solution' => $code,
+ ])
+ ->condition('csid', $captcha_sid, '=')
+ ->execute();
+ $result['data'] = [
+ //phpcs:ignore
+ 'url' => Url::fromRoute('image_captcha.generator', ['session_id' => $captcha_sid, 'timestamp' => $this->time->getRequestTime()])->toString(),
+ 'token' => $captcha_token,
+ 'sid' => $captcha_sid,
+ ];
+ $result['status'] = 1;
+ }
+ catch (\Exception $e) {
+ if ($message = $e->getMessage()) {
+ $result['message'] = $message;
+ }
+ else {
+ $result['message'] = $this->t('Error has occurred. Please contact to site administrator.');
+ }
+ }
+ return new JsonResponse($result);
+ }
+
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/src/Form/ImageCaptchaSettingsForm.php b/web/modules/contrib/captcha/modules/image_captcha/src/Form/ImageCaptchaSettingsForm.php
new file mode 100644
index 000000000..d13bb9612
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/src/Form/ImageCaptchaSettingsForm.php
@@ -0,0 +1,433 @@
+languageManager = $container->get('language_manager');
+ $instance->fileSystem = $container->get('file_system');
+
+ return $instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'image_captcha_settings';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEditableConfigNames() {
+ return ['image_captcha.settings'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $config = $this->config('image_captcha.settings');
+ // Add CSS and JS for theming and added usability on admin form.
+ $form['#attached']['library'][] = 'image_captcha/admin';
+
+ // First some error checking.
+ $setup_status = _image_captcha_check_setup(FALSE);
+ if ($setup_status & ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_NO_GDLIB) {
+ $this->messenger()->addError($this->t(
+ 'The Image CAPTCHA module can not generate images because your PHP setup does not support it (no GD library with JPEG support).',
+ ['!gdlib' => 'http://php.net/manual/en/book.image.php']
+ ));
+ // It is no use to continue building the rest of the settings form.
+ return $form;
+ }
+
+ $form['image_captcha_example'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Example'),
+ '#description' => $this->t('Presolved image CAPTCHA example, generated with the current settings.'),
+ ];
+
+ $form['image_captcha_example']['image'] = [
+ '#type' => 'captcha',
+ '#captcha_type' => ImageCaptchaConstants::IMAGE_CAPTCHA_CAPTCHA_TYPE,
+ '#captcha_admin_mode' => TRUE,
+ ];
+
+ // General code settings.
+ $form['image_captcha_code_settings'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Code settings'),
+ ];
+
+ $form['image_captcha_code_settings']['image_captcha_image_allowed_chars'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Characters to use in the code'),
+ '#default_value' => $config->get('image_captcha_image_allowed_chars') ? $config->get('image_captcha_image_allowed_chars') : ImageCaptchaConstants::IMAGE_CAPTCHA_ALLOWED_CHARACTERS,
+ ];
+ $form['image_captcha_code_settings']['image_captcha_code_length'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Code length'),
+ '#options' => [2 => 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ '#default_value' => $config->get('image_captcha_code_length'),
+ '#description' => $this->t('The code length influences the size of the image. Note that larger values make the image generation more CPU intensive.'),
+ ];
+ // RTL support option (only show this option when there are RTL languages).
+ $language = $this->languageManager->getCurrentLanguage();
+ if ($language->getDirection() == Language::DIRECTION_RTL) {
+ $form['image_captcha_code_settings']['image_captcha_rtl_support'] = [
+ '#title' => $this->t('RTL support'),
+ '#type' => 'checkbox',
+ '#default_value' => $config->get('image_captcha_rtl_support'),
+ '#description' => $this->t('Enable this option to render the code from right to left for right to left languages.'),
+ ];
+ }
+
+ // Font related stuff.
+ $form['image_captcha_font_settings'] = $this->settingsDotSection();
+
+ // Color and file format settings.
+ $form['image_captcha_color_settings'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Color and image settings'),
+ '#description' => $this->t('Configuration of the background, text colors and file format of the image CAPTCHA.'),
+ ];
+
+ $form['image_captcha_color_settings']['image_captcha_background_color'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Background color'),
+ '#description' => $this->t('Enter the hexadecimal code for the background color (e.g. #FFF or #FFCE90). When using the PNG file format with transparent background, it is recommended to set this close to the underlying background color.'),
+ '#default_value' => $config->get('image_captcha_background_color'),
+ '#maxlength' => 7,
+ '#size' => 8,
+ ];
+ $form['image_captcha_color_settings']['image_captcha_foreground_color'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Text color'),
+ '#description' => $this->t('Enter the hexadecimal code for the text color (e.g. #000 or #004283).'),
+ '#default_value' => $config->get('image_captcha_foreground_color'),
+ '#maxlength' => 7,
+ '#size' => 8,
+ ];
+ $form['image_captcha_color_settings']['image_captcha_foreground_color_randomness'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Additional variation of text color'),
+ '#options' => [
+ 0 => $this->t('No variation'),
+ 50 => $this->t('Little variation'),
+ 100 => $this->t('Medium variation'),
+ 150 => $this->t('High variation'),
+ 200 => $this->t('Very high variation'),
+ ],
+ '#default_value' => $config->get('image_captcha_foreground_color_randomness'),
+ '#description' => $this->t('The different characters will have randomized colors in the specified range around the text color.'),
+ ];
+ $form['image_captcha_color_settings']['image_captcha_file_format'] = [
+ '#type' => 'select',
+ '#title' => $this->t('File format'),
+ '#description' => $this->t('Select the file format for the image. JPEG usually results in smaller files, PNG allows transparency.'),
+ '#default_value' => $config->get('image_captcha_file_format'),
+ '#options' => [
+ ImageCaptchaConstants::IMAGE_CAPTCHA_FILE_FORMAT_JPG => $this->t('JPEG'),
+ ImageCaptchaConstants::IMAGE_CAPTCHA_FILE_FORMAT_PNG => $this->t('PNG'),
+ ImageCaptchaConstants::IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG => $this->t('PNG with transparent background'),
+ ],
+ ];
+
+ // Distortion and noise settings.
+ $form['image_captcha_distortion_and_noise'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Distortion and noise'),
+ '#description' => $this->t('With these settings you can control the degree of obfuscation by distortion and added noise. Do not exaggerate the obfuscation and assure that the code in the image is reasonably readable. For example, do not combine high levels of distortion and noise.'),
+ ];
+
+ $form['image_captcha_distortion_and_noise']['image_captcha_distortion_amplitude'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Distortion level'),
+ '#options' => [
+ 0 => $this->t('@level - no distortion', ['@level' => '0']),
+ 1 => $this->t('@level - low', ['@level' => '1']),
+ 2 => '2',
+ 3 => '3',
+ 4 => '4',
+ 5 => $this->t('@level - medium', ['@level' => '5']),
+ 6 => '6',
+ 7 => '7',
+ 8 => '8',
+ 9 => '9',
+ 10 => $this->t('@level - high', ['@level' => '10']),
+ ],
+ '#default_value' => $config->get('image_captcha_distortion_amplitude'),
+ '#description' => $this->t('Set the degree of wave distortion in the image.'),
+ ];
+ $form['image_captcha_distortion_and_noise']['image_captcha_bilinear_interpolation'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Smooth distortion'),
+ '#default_value' => $config->get('image_captcha_bilinear_interpolation'),
+ '#description' => $this->t('This option enables bilinear interpolation of the distortion which makes the image look smoother, but it is more CPU intensive.'),
+ ];
+
+ $form['image_captcha_distortion_and_noise']['image_captcha_dot_noise'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Add salt and pepper noise'),
+ '#default_value' => $config->get('image_captcha_dot_noise'),
+ '#description' => $this->t('This option adds randomly colored point noise.'),
+ ];
+
+ $form['image_captcha_distortion_and_noise']['image_captcha_line_noise'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Add line noise'),
+ '#default_value' => $config->get('image_captcha_line_noise', 0),
+ '#description' => $this->t('This option enables lines randomly drawn on top of the text code.'),
+ ];
+
+ $form['image_captcha_distortion_and_noise']['image_captcha_noise_level'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Noise level'),
+ '#options' => [
+ 1 => '1 - ' . $this->t('low'),
+ 2 => '2',
+ 3 => '3 - ' . $this->t('medium'),
+ 4 => '4',
+ 5 => '5 - ' . $this->t('high'),
+ 7 => '7',
+ 10 => '10 - ' . $this->t('severe'),
+ ],
+ '#default_value' => (int) $config->get('image_captcha_noise_level'),
+ ];
+
+ $form['image_captcha_text_settings'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Text Content'),
+ '#description' => $this->t('Customize image CAPTCHA text content.'),
+ ];
+ $form['image_captcha_text_settings']['image_captcha_text_refresh'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Refresh button text.'),
+ '#description' => $this->t('Customize the refresh button text.'),
+ '#default_value' => $config->get('image_captcha_text_refresh'),
+ '#size' => 15,
+ '#required' => TRUE,
+ ];
+ // Enter the characters shown in the image.
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+
+ // Check image_captcha_image_allowed_chars for spaces.
+ if (preg_match('/\s/', $form_state->getValue('image_captcha_image_allowed_chars'))) {
+ $form_state->setErrorByName('image_captcha_image_allowed_chars', $this->t('The list of characters to use should not contain spaces.'));
+ }
+
+ if (!isset($form['image_captcha_font_settings']['no_ttf_support'])) {
+ // Check the selected fonts.
+ // Filter the image_captcha fonts array to pick out the selected ones.
+ $fonts = array_filter($form_state->getValue('image_captcha_fonts'));
+ if (count($fonts) < 1) {
+ $form_state->setErrorByName('image_captcha_fonts', $this->t('You need to select at least one font.'));
+ }
+ if ($form_state->getValue('image_captcha_fonts')['BUILTIN']) {
+ // With the built in font, only latin2 characters should be used.
+ if (preg_match('/[^a-zA-Z0-9]/', $form_state->getValue('image_captcha_image_allowed_chars'))) {
+ $form_state->setErrorByName('image_captcha_image_allowed_chars', $this->t('The built-in font only supports Latin2 characters. Only use "a" to "z" and numbers.'));
+ }
+ }
+ $available_fonts = _image_captcha_get_font_uri();
+ foreach ($fonts as $token) {
+ $fonts[$token] = $available_fonts[$token];
+ }
+ $problem_fonts = _image_captcha_check_fonts($fonts);
+ if (count($problem_fonts) > 0) {
+ $form_state->setErrorByName('image_captcha_fonts', $this->t('The following fonts are not readable: %fonts.', ['%fonts' => implode(', ', $problem_fonts)]));
+ }
+ }
+
+ // Check color settings.
+ if (!preg_match('/^#([0-9a-fA-F]{3}){1,2}$/', $form_state->getValue('image_captcha_background_color'))) {
+ $form_state->setErrorByName('image_captcha_background_color', $this->t('Background color is not a valid hexadecimal color value.'));
+ }
+ if (!preg_match('/^#([0-9a-fA-F]{3}){1,2}$/', $form_state->getValue('image_captcha_foreground_color'))) {
+ $form_state->setErrorByName('image_captcha_foreground_color', $this->t('Text color is not a valid hexadecimal color value.'));
+ }
+
+ parent::validateForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $imageSettings = $form_state->cleanValues()->getValues();
+ if (!isset($form['image_captcha_font_settings']['no_ttf_support'])) {
+ // Filter the image_captcha fonts array to pick out the selected ones.
+ $imageSettings['image_captcha_fonts'] = array_filter($imageSettings['image_captcha_fonts']);
+ }
+ $config = $this->config('image_captcha.settings');
+ // Exclude few fields from config.
+ $exclude = ['image', 'captcha_sid', 'captcha_token', 'captcha_response'];
+ foreach ($imageSettings as $configName => $configValue) {
+ if (!in_array($configName, $exclude)) {
+ $config->set($configName, $configValue);
+ }
+ }
+ $config->save();
+
+ parent::SubmitForm($form, $form_state);
+ }
+
+ /**
+ * Form elements for the font specific setting.
+ *
+ * This is refactored to a separate function to avoid polluting the
+ * general form function image_captcha_settings_form with some
+ * specific logic.
+ *
+ * @return array
+ * The font settings specific form elements.
+ */
+ protected function settingsDotSection() {
+ $config = $this->config('image_captcha.settings');
+ // Put it all in a details element.
+ $form = [
+ '#type' => 'details',
+ '#title' => $this->t('Font settings'),
+ ];
+
+ // First check if there is TrueType support.
+ $setup_status = _image_captcha_check_setup(FALSE);
+ if ($setup_status & ImageCaptchaConstants::IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT) {
+ // Show a warning that there is no TrueType support.
+ $form['no_ttf_support'] = [
+ '#type' => 'item',
+ '#title' => $this->t('No TrueType support'),
+ '#markup' => $this->t('The Image CAPTCHA module can not use TrueType fonts because your PHP setup does not support it. You can only use a PHP built-in bitmap font of fixed size.'),
+ ];
+ }
+ else {
+ // Build a list of all available fonts.
+ $available_fonts = [];
+
+ // List of folders to search through for TrueType fonts.
+ $fonts = _image_captcha_get_available_fonts_from_directories();
+ // Cache the list of previewable fonts. All the previews are done
+ // in separate requests, and we don't want to rescan the filesystem
+ // every time, so we cache the result.
+ $config->set('image_captcha_fonts_preview_map_cache', $fonts);
+ $config->save();
+ // Put these fonts with preview image in the list.
+ foreach ($fonts as $token => $font) {
+
+ $title = $this->t('Font preview of @font (@file)', [
+ '@font' => $font['name'],
+ '@file' => $font['uri'],
+ ]);
+ $attributes = [
+ 'src' => Url::fromRoute('image_captcha.font_preview', ['token' => $token])
+ ->toString(),
+ 'title' => $title,
+ 'alt' => $title,
+ ];
+ $available_fonts[$token] = '';
+ }
+
+ // Append the PHP built-in font at the end.
+ $title = $this->t('Preview of built-in font');
+ $available_fonts['BUILTIN'] = $this->t('PHP built-in font: ', [
+ '@font_preview_url' => Url::fromRoute('image_captcha.font_preview', ['token' => 'BUILTIN'])
+ ->toString(),
+ '@title' => $title,
+ ])->__toString();
+
+ $default_fonts = _image_captcha_get_enabled_fonts();
+ $conf_path = DrupalKernel::findSitePath($this->getRequest());
+
+ $form['image_captcha_fonts'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Fonts'),
+ '#default_value' => $default_fonts,
+ '#description' => $this->t('Select the fonts to use for the text in the image CAPTCHA. Apart from the provided defaults, you can also use your own TrueType fonts (filename extension .ttf) by putting them in %fonts_library_general or %fonts_library_specific.',
+ [
+ '%fonts_library_general' => 'sites/all/libraries/fonts',
+ '%fonts_library_specific' => $conf_path . '/libraries/fonts',
+ ]
+ ),
+ '#options' => $available_fonts,
+ '#attributes' => ['class' => ['image-captcha-admin-fonts-selection']],
+ ];
+
+ $form['image_captcha_font_size'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Font size'),
+ // @codingStandardsIgnoreStart
+ // We should not translate "pt", so we ignore the coding standards here:
+ '#options' => [
+ 9 => '9 pt - ' . $this->t('tiny'),
+ 12 => '12 pt - ' . $this->t('small'),
+ 18 => '18 pt',
+ 24 => '24 pt - ' . $this->t('normal'),
+ 30 => '30 pt',
+ 36 => '36 pt - ' . $this->t('large'),
+ 48 => '48 pt',
+ 64 => '64 pt - ' . $this->t('extra large'),
+ ],
+ // @codingStandardsIgnoreEnd
+ '#default_value' => (int) $config->get('image_captcha_font_size'),
+ '#description' => $this->t('The font size influences the size of the image. Note that larger values make the image generation more CPU intensive.'),
+ ];
+ }
+
+ // Character spacing (available for both the TrueType
+ // fonts and the builtin font.
+ $form['image_captcha_font_settings']['image_captcha_character_spacing'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Character spacing'),
+ '#description' => $this->t('Define the average spacing between characters. Note that larger values make the image generation more CPU intensive.'),
+ '#default_value' => $config->get('image_captcha_character_spacing'),
+ '#options' => [
+ '0.75' => $this->t('tight'),
+ '1' => $this->t('normal'),
+ '1.2' => $this->t('wide'),
+ '1.5' => $this->t('extra wide'),
+ ],
+ ];
+
+ return $form;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/src/Service/ImageCaptchaRenderService.php b/web/modules/contrib/captcha/modules/image_captcha/src/Service/ImageCaptchaRenderService.php
new file mode 100644
index 000000000..d082d1911
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/src/Service/ImageCaptchaRenderService.php
@@ -0,0 +1,433 @@
+config = $config_factory->get('image_captcha.settings');
+ $this->connection = $connection;
+ $this->fileSystem = $fileSystem;
+ }
+
+ /**
+ * Small helper function for parsing a hexadecimal color to a RGB tuple.
+ *
+ * @param string $hex
+ * String representation of HEX color value.
+ *
+ * @return array
+ * Array representation of RGB color value.
+ */
+ protected function hexToRgb($hex) {
+ if (mb_strlen($hex) == 4) {
+ $hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
+ }
+ $hex = trim($hex, " #&Hh");
+ $c = hexdec($hex);
+ $rgb = [];
+ for ($i = 16; $i >= 0; $i -= 8) {
+ $rgb[] = ($c >> $i) & 0xFF;
+ }
+ return $rgb;
+ }
+
+ /**
+ * Base function for generating a image CAPTCHA.
+ *
+ * @param string $code
+ * String code to be presented on image.
+ *
+ * @return resource
+ * Image to be outputted contained $code string.
+ */
+ public function generateImage($code) {
+ $fonts = _image_captcha_get_enabled_fonts();
+
+ $font_size = $this->config->get('image_captcha_font_size');
+ [$width, $height] = _image_captcha_image_size($code);
+
+ $image = imagecreatetruecolor($width, $height);
+ if (!$image) {
+ return FALSE;
+ }
+
+ // Get the background color and paint the background.
+ $background_rgb = $this->hexToRGB($this->config->get('image_captcha_background_color'));
+ $background_color = imagecolorallocate($image, $background_rgb[0], $background_rgb[1], $background_rgb[2]);
+ // Set transparency if needed.
+ $file_format = $this->config->get('image_captcha_file_format');
+ if ($file_format == ImageCaptchaConstants::IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG) {
+ imagecolortransparent($image, $background_color);
+ }
+ imagefilledrectangle($image, 0, 0, $width, $height, $background_color);
+
+ $result = $this->printString($image, $width, $height, $fonts, $font_size, $code);
+ if (!$result) {
+ return FALSE;
+ }
+
+ $noise_colors = [];
+ for ($i = 0; $i < 20; $i++) {
+ $noise_colors[] = imagecolorallocate($image, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255));
+ }
+
+ // Add additional noise.
+ if ($this->config->get('image_captcha_dot_noise')) {
+ $this->addDots($image, $width, $height, $noise_colors);
+ }
+
+ if ($this->config->get('image_captcha_line_noise')) {
+ $this->addLines($image, $width, $height, $noise_colors);
+ }
+
+ $distortion_amplitude = .25 * $font_size * $this->config->get('image_captcha_distortion_amplitude') / 10.0;
+
+ if ($distortion_amplitude > 1) {
+ $wavelength_xr = (2 + 3 * lcg_value()) * $font_size;
+ $wavelength_yr = (2 + 3 * lcg_value()) * $font_size;
+ $freq_xr = 2 * 3.141592 / $wavelength_xr;
+ $freq_yr = 2 * 3.141592 / $wavelength_yr;
+ $wavelength_xt = (2 + 3 * lcg_value()) * $font_size;
+ $wavelength_yt = (2 + 3 * lcg_value()) * $font_size;
+ $freq_xt = 2 * 3.141592 / $wavelength_xt;
+ $freq_yt = 2 * 3.141592 / $wavelength_yt;
+
+ $distorted_image = imagecreatetruecolor($width, $height);
+
+ if ($file_format == ImageCaptchaConstants::IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG) {
+ imagecolortransparent($distorted_image, $background_color);
+ }
+
+ if (!$distorted_image) {
+ return FALSE;
+ }
+
+ if ($this->config->get('image_captcha_bilinear_interpolation')) {
+ // Distortion with bilinear interpolation.
+ for ($x = 0; $x < $width; $x++) {
+ for ($y = 0; $y < $height; $y++) {
+ // Get distorted sample point in source image.
+ $r = $distortion_amplitude * sin($x * $freq_xr + $y * $freq_yr);
+ $theta = $x * $freq_xt + $y * $freq_yt;
+ $sx = $x + $r * cos($theta);
+ $sy = $y + $r * sin($theta);
+ $sxf = (int) floor($sx);
+ $syf = (int) floor($sy);
+ if ($sxf < 0 || $syf < 0 || $sxf >= $width - 1 || $syf >= $height - 1) {
+ $color = $background_color;
+ }
+ else {
+ // Bilinear interpolation: sample at four corners.
+ $color_00 = imagecolorat($image, $sxf, $syf);
+ $color_00_r = ($color_00 >> 16) & 0xFF;
+ $color_00_g = ($color_00 >> 8) & 0xFF;
+ $color_00_b = $color_00 & 0xFF;
+ $color_10 = imagecolorat($image, $sxf + 1, $syf);
+ $color_10_r = ($color_10 >> 16) & 0xFF;
+ $color_10_g = ($color_10 >> 8) & 0xFF;
+ $color_10_b = $color_10 & 0xFF;
+ $color_01 = imagecolorat($image, $sxf, $syf + 1);
+ $color_01_r = ($color_01 >> 16) & 0xFF;
+ $color_01_g = ($color_01 >> 8) & 0xFF;
+ $color_01_b = $color_01 & 0xFF;
+ $color_11 = imagecolorat($image, $sxf + 1, $syf + 1);
+ $color_11_r = ($color_11 >> 16) & 0xFF;
+ $color_11_g = ($color_11 >> 8) & 0xFF;
+ $color_11_b = $color_11 & 0xFF;
+ // Interpolation factors.
+ $u = $sx - $sxf;
+ $v = $sy - $syf;
+ $r = (int) ((1 - $v) * ((1 - $u) * $color_00_r + $u * $color_10_r) + $v * ((1 - $u) * $color_01_r + $u * $color_11_r));
+ $g = (int) ((1 - $v) * ((1 - $u) * $color_00_g + $u * $color_10_g) + $v * ((1 - $u) * $color_01_g + $u * $color_11_g));
+ $b = (int) ((1 - $v) * ((1 - $u) * $color_00_b + $u * $color_10_b) + $v * ((1 - $u) * $color_01_b + $u * $color_11_b));
+ $color = ($r << 16) + ($g << 8) + $b;
+ }
+
+ imagesetpixel($distorted_image, $x, $y, $color);
+ }
+ }
+ }
+ else {
+ // Distortion with nearest neighbor interpolation.
+ for ($x = 0; $x < $width; $x++) {
+ for ($y = 0; $y < $height; $y++) {
+ // Get distorted sample point in source image.
+ $r = $distortion_amplitude * sin($x * $freq_xr + $y * $freq_yr);
+ $theta = $x * $freq_xt + $y * $freq_yt;
+ $sx = $x + $r * cos($theta);
+ $sy = $y + $r * sin($theta);
+ $sxf = (int) floor($sx);
+ $syf = (int) floor($sy);
+ if ($sxf < 0 || $syf < 0 || $sxf >= $width - 1 || $syf >= $height - 1) {
+ $color = $background_color;
+ }
+ else {
+ $color = imagecolorat($image, $sxf, $syf);
+ }
+ imagesetpixel($distorted_image, $x, $y, $color);
+ }
+ }
+ }
+ return $distorted_image;
+ }
+ else {
+ return $image;
+ }
+ }
+
+ /**
+ * Add random noise lines to image with given color.
+ *
+ * @param resource $image
+ * Link to image stream resource.
+ * @param int $width
+ * Suggested output image width.
+ * @param int $height
+ * Suggested input image width.
+ * @param array $colors
+ * Font color.
+ */
+ protected function addLines(&$image, $width, $height, array $colors) {
+ $line_quantity = $width * $height / 200.0 * ((int) $this->config->get('image_captcha_noise_level')) / 10.0;
+
+ for ($i = 0; $i < $line_quantity; $i++) {
+ imageline($image, mt_rand(0, $width), mt_rand(0, $height), mt_rand(0, $width), mt_rand(0, $height), $colors[array_rand($colors)]);
+ }
+ }
+
+ /**
+ * Add random noise dots to image with given color.
+ *
+ * @param resource $image
+ * Link to image stream resource.
+ * @param int $width
+ * Suggested output image width.
+ * @param int $height
+ * Suggested input image width.
+ * @param array $colors
+ * Font color.
+ */
+ protected function addDots(&$image, $width, $height, array $colors) {
+ $noise_quantity = $width * $height * ((int) $this->config->get('image_captcha_noise_level')) / 10.0;
+
+ for ($i = 0; $i < $noise_quantity; $i++) {
+ imagesetpixel($image, mt_rand(0, $width), mt_rand(0, $height), $colors[array_rand($colors)]);
+ }
+ }
+
+ /**
+ * Helper function for drawing text on the image.
+ *
+ * @param resource $image
+ * Link to image stream resource.
+ * @param int $width
+ * Suggested output image width.
+ * @param int $height
+ * Suggested input image width.
+ * @param array $fonts
+ * Array of fonts names and paths.
+ * @param int $font_size
+ * Suggested font size.
+ * @param string $text
+ * Text to be written on the image.
+ * @param bool $rtl
+ * RTL.
+ *
+ * @return bool
+ * TRUE if image generation was successful, FALSE otherwise.
+ */
+ protected function printString(&$image, $width, $height, array $fonts, $font_size, $text, $rtl = FALSE) {
+ $characters = _image_captcha_utf8_split($text);
+ $character_quantity = count($characters);
+
+ $foreground_rgb = $this->hexToRgb($this->config->get('image_captcha_foreground_color'));
+ $foreground_color = imagecolorallocate($image, $foreground_rgb[0], $foreground_rgb[1], $foreground_rgb[2]);
+ // Precalculate the value ranges for color randomness.
+ $foreground_randomness = $this->config->get('image_captcha_foreground_color_randomness');
+ $foreground_color_range = [];
+
+ if ($foreground_randomness) {
+ for ($i = 0; $i < 3; $i++) {
+ $foreground_color_range[$i] = [
+ max(0, $foreground_rgb[$i] - $foreground_randomness),
+ min(255, $foreground_rgb[$i] + $foreground_randomness),
+ ];
+ }
+ }
+
+ // Set default text color.
+ $color = $foreground_color;
+
+ // The image is separated in different character cages, one for
+ // each character that will be somewhere inside that cage.
+ $ccage_width = $width / $character_quantity;
+ $ccage_height = $height;
+
+ foreach ($characters as $c => $character) {
+ // Initial position of character: in the center of its cage.
+ $center_x = ($c + 0.5) * $ccage_width;
+ if ($rtl) {
+ $center_x = $width - $center_x;
+ }
+ $center_y = 0.5 * $height;
+
+ // Pick a random font from the list.
+ $font = $fonts[array_rand($fonts)];
+ $font = _image_captcha_get_font_uri($font);
+
+ // Get character dimensions for TrueType fonts.
+ if ($font != 'BUILTIN') {
+ putenv('GDFONTPATH=' . realpath('.'));
+ $bbox = imagettfbbox($font_size, 0, $this->fileSystem->realpath($font), $character);
+ // In very rare cases with some versions of the GD library, the x-value
+ // of the left side of the bounding box as returned by the first call of
+ // imagettfbbox is corrupt (value -2147483648 = 0x80000000).
+ // The weird thing is that calling the function a second time
+ // can be used as workaround.
+ // This issue is discussed at http://drupal.org/node/349218.
+ if ($bbox[2] < 0) {
+ $bbox = imagettfbbox($font_size, 0, $this->fileSystem->realpath($font), $character);
+ }
+ }
+ else {
+ $character_width = imagefontwidth(5);
+ $character_height = imagefontheight(5);
+ $bbox = [
+ 0,
+ $character_height,
+ $character_width,
+ $character_height,
+ $character_width,
+ 0,
+ 0,
+ 0,
+ ];
+ }
+
+ // Random (but small) rotation of the character.
+ // @todo add a setting for this?
+ $angle = mt_rand(-10, 10);
+
+ // Determine print position: at what coordinate should the character be
+ // printed so that the bounding box would be nicely centered in the cage?
+ $bb_center_x = .5 * ($bbox[0] + $bbox[2]);
+ $bb_center_y = .5 * ($bbox[1] + $bbox[7]);
+ $angle_cos = cos($angle * 3.1415 / 180);
+ $angle_sin = sin($angle * 3.1415 / 180);
+ $pos_x = $center_x - ($angle_cos * $bb_center_x + $angle_sin * $bb_center_y);
+ $pos_y = $center_y - (-$angle_sin * $bb_center_x + $angle_cos * $bb_center_y);
+
+ // Calculate available room to jitter: how much
+ // can the character be moved. So that it stays inside its cage?
+ $bb_width = $bbox[2] - $bbox[0];
+ $bb_height = $bbox[1] - $bbox[7];
+ $dev_x = .5 * max(0, $ccage_width - abs($angle_cos) * $bb_width - abs($angle_sin) * $bb_height);
+ $dev_y = .5 * max(0, $ccage_height - abs($angle_cos) * $bb_height - abs($angle_sin) * $bb_width);
+
+ // Add jitter to position.
+ $pos_x = $pos_x + mt_rand(-(int) $dev_x, (int) $dev_x);
+ $pos_y = $pos_y + mt_rand(-(int) $dev_y, (int) $dev_y);
+
+ // Calculate text color in case of randomness.
+ if ($foreground_randomness) {
+ $color = imagecolorallocate($image,
+ mt_rand($foreground_color_range[0][0], $foreground_color_range[0][1]),
+ mt_rand($foreground_color_range[1][0], $foreground_color_range[1][1]),
+ mt_rand($foreground_color_range[2][0], $foreground_color_range[2][1])
+ );
+ }
+
+ // Draw character.
+ if ($font == 'BUILTIN') {
+ imagestring($image, 5, (int) $pos_x, (int) $pos_y, $character, $color);
+ }
+ else {
+ imagettftext($image, $font_size, $angle, (int) $pos_x, (int) $pos_y, $color, $this->fileSystem->realpath($font), $character);
+ }
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Add image refresh button to captcha form element.
+ *
+ * @return array
+ * The processed element.
+ *
+ * @see image_captcha_element_info_alter()
+ */
+ public static function imageCaptchaAfterBuildProcess(array $element) {
+ $isCaptchaType = !empty($element['#captcha_type']) ?
+ $element['#captcha_type'] == ImageCaptchaConstants::IMAGE_CAPTCHA_CAPTCHA_TYPE : NULL;
+ // Only proceed, if we can determine the form_id and the captcha type:
+ if (!empty($element['#captcha_type']) && !empty($isCaptchaType)) {
+ if (!empty($element['#captcha_info']['form_id']) && !empty($element['#captcha_type'])) {
+ // We need the form_id for regenerating the image captcha:
+ $form_id = $element['#captcha_info']['form_id'];
+ // Check if this is an image_captcha:
+ if (isset($element['captcha_widgets']['captcha_image_wrapper']['captcha_image'])) {
+ $uri = Link::fromTextAndUrl(t('Get new captcha!'),
+ new Url('image_captcha.refresh',
+ ['form_id' => $form_id],
+ ['attributes' => ['class' => ['reload-captcha']]]
+ )
+ );
+ $element['captcha_widgets']['captcha_image_wrapper']['captcha_refresh'] = [
+ '#theme' => 'image_captcha_refresh',
+ '#captcha_refresh_link' => $uri,
+ ];
+ }
+ }
+ else {
+ \Drupal::service('logger.factory')->get('image_captcha')->error('Missing required form ID on route @route', [
+ '@route' => \Drupal::routeMatch()->getRouteName() ?? 'Unknown',
+ ]);
+ }
+ }
+ return $element;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/templates/image-captcha-refresh.html.twig b/web/modules/contrib/captcha/modules/image_captcha/templates/image-captcha-refresh.html.twig
new file mode 100644
index 000000000..41bfc9c60
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/templates/image-captcha-refresh.html.twig
@@ -0,0 +1,19 @@
+{#
+/**
+ * @file
+ * Custom theme implementation for custom field type defined.
+ *
+ * Available variables:
+ * - uri: An optional URL the image can be linked to.
+ * - title: An optional Title value which will be shown as text.
+ * - link_value: Value used to open the link in new or same tab.
+ *
+ * @see template_preprocess_custom_zipang_link_formatter()
+ *
+ * @ingroup themeable
+ */
+#}
+{{ attach_library('image_captcha/image-captcha-refresh') }}
+
+ {{captcha_refresh_link}}
+
diff --git a/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/image_captcha_test.info.yml b/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/image_captcha_test.info.yml
new file mode 100644
index 000000000..8bff14f35
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/image_captcha_test.info.yml
@@ -0,0 +1,13 @@
+name: 'Image Captcha Test'
+type: module
+description: 'Provides captcha types for tests.'
+package: Testing
+hidden: true
+dependencies:
+ - captcha:captcha
+ - captcha:image_captcha
+
+# Information added by Drupal.org packaging script on 2025-07-28
+version: '2.0.9'
+project: 'captcha'
+datestamp: 1753701291
diff --git a/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/image_captcha_test.routing.yml b/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/image_captcha_test.routing.yml
new file mode 100644
index 000000000..2d8c3896a
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/image_captcha_test.routing.yml
@@ -0,0 +1,7 @@
+image_captcha_test.test:
+ path: '/captcha-test/image-test'
+ defaults:
+ _title: 'Test'
+ _form: 'Drupal\image_captcha_test\Form\ImageCaptchaTestForm'
+ requirements:
+ _permission: 'access content'
diff --git a/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/src/Form/ImageCaptchaTestForm.php b/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/src/Form/ImageCaptchaTestForm.php
new file mode 100644
index 000000000..2ca854518
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/tests/modules/image_captcha_test/src/Form/ImageCaptchaTestForm.php
@@ -0,0 +1,43 @@
+ 'captcha',
+ '#captcha_type' => ImageCaptchaConstants::IMAGE_CAPTCHA_CAPTCHA_TYPE,
+ '#captcha_admin_mode' => TRUE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->messenger()->addStatus($this->t('Form submitted!'));
+ $form_state->setRedirect('');
+ }
+
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/tests/src/Functional/ImageCaptchaBasicFunctionalTest.php b/web/modules/contrib/captcha/modules/image_captcha/tests/src/Functional/ImageCaptchaBasicFunctionalTest.php
new file mode 100644
index 000000000..381d9581c
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/tests/src/Functional/ImageCaptchaBasicFunctionalTest.php
@@ -0,0 +1,105 @@
+config('system.site')->set('page.front', '/test-page')->save();
+ $this->user = $this->drupalCreateUser([]);
+ $this->adminUser = $this->drupalCreateUser([]);
+ $this->adminUser->addRole($this->createAdminRole('admin', 'admin'));
+ $this->adminUser->save();
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests if the image captcha settings page is accessible.
+ */
+ public function testImageCaptchaSettingsPage() {
+ $session = $this->assertSession();
+ $this->drupalGet('admin/config/people/captcha/image_captcha');
+ $session->statusCodeEquals(200);
+ $session->pageTextContains('Example');
+ $session->pageTextContains('Color and image settings');
+ }
+
+ /**
+ * Tests the image form element and it's structure.
+ */
+ public function testImageFormElement() {
+ $session = $this->assertSession();
+
+ $this->drupalLogin($this->adminUser);
+
+ $this->drupalGet('/captcha-test/image-test');
+ $session->statusCodeEquals(200);
+
+ $session->elementExists('css', '#image-captcha-test-test');
+
+ $session->elementExists('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"]');
+
+ // Check the first captcha form element and see if it is complete:
+ // Check captcha description:
+ $session->elementExists('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__description');
+ $session->elementTextContains('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__description', 'This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.');
+ $session->elementExists('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__element > div.form-item-captcha-response');
+ // Check Question label:
+ $session->elementExists('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__element > div.form-item-captcha-response > label.form-required');
+ $session->elementTextContains('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__element > div.form-item-captcha-response > label.form-required', 'What code is in the image?');
+ // Check other text elements:
+ $session->elementExists('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__element > div.form-item-captcha-response > input.form-text');
+ $session->elementExists('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__element > div.form-item-captcha-response > div#edit-captcha-response--description');
+ $session->elementTextContains('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__element > div.form-item-captcha-response > div#edit-captcha-response--description', 'Enter the characters shown in the image.');
+ // Check image exists:
+ $session->elementExists('css', '#image-captcha-test-test > fieldset[data-drupal-selector="edit-image-captcha"] > div.captcha__element > div#edit-captcha-image-wrapper');
+ $session->elementExists('css', '#edit-captcha-image-wrapper > img[data-drupal-selector="edit-captcha-image"]');
+ $session->elementExists('css', '#edit-captcha-image-wrapper > img[src*="/image-captcha-generate"]');
+ // Check refresh button:
+ $session->elementExists('css', '#edit-captcha-image-wrapper > div.reload-captcha-wrapper');
+ $session->elementExists('css', '#edit-captcha-image-wrapper > div.reload-captcha-wrapper > a.reload-captcha');
+ }
+
+}
diff --git a/web/modules/contrib/captcha/modules/image_captcha/tests/src/Functional/ImageCaptchaInstallationTest.php b/web/modules/contrib/captcha/modules/image_captcha/tests/src/Functional/ImageCaptchaInstallationTest.php
new file mode 100644
index 000000000..0910ec34e
--- /dev/null
+++ b/web/modules/contrib/captcha/modules/image_captcha/tests/src/Functional/ImageCaptchaInstallationTest.php
@@ -0,0 +1,127 @@
+config('system.site')->set('page.front', '/test-page')->save();
+ $this->user = $this->drupalCreateUser([]);
+ $this->adminUser = $this->drupalCreateUser([]);
+ $this->adminUser->addRole($this->createAdminRole('admin', 'admin'));
+ $this->adminUser->save();
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests if installing the module, won't break the site.
+ */
+ public function testInstallation() {
+ // Remove when dropping support for Drupal 10.3 and below.
+ $installed = version_compare(\Drupal::VERSION, '10.3', '>=') ? 'installed' : 'enabled';
+ $install = version_compare(\Drupal::VERSION, '10.3', '>=') ? 'install' : 'enable';
+
+ $session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ // As simply adding the module to the $modules array only installs required
+ // modules one by one, we also need to test installing both captcha and
+ // image captcha at once:
+ $this->drupalGet('/admin/modules');
+ $page->checkField('edit-modules-image-captcha-enable');
+ $page->pressButton('edit-submit');
+ // Also install required modules:
+ $session->statusCodeEquals(200);
+ $session->pageTextContains('Some required modules must be ' . $installed);
+ $session->pageTextContains('You must ' . $install . ' the CAPTCHA module to install Image CAPTCHA.');
+ // Continue:
+ $page->pressButton('edit-submit');
+ $session->statusCodeEquals(200);
+ $session->pageTextContains('2 modules have been ' . $installed . ': Image CAPTCHA, CAPTCHA');
+ // Go to front page and see if the site isn't broken:
+ $this->drupalGet('');
+ // Ensure the status code is success:
+ $session->statusCodeEquals(200);
+ // Ensure the correct test page is loaded as front page:
+ $session->pageTextContains('Test page text.');
+ }
+
+ /**
+ * Tests if uninstalling the module, won't break the site.
+ */
+ public function testUninstallation() {
+ // Remove when dropping support for Drupal 10.3 and below.
+ $installed = version_compare(\Drupal::VERSION, '10.3', '>=') ? 'installed' : 'enabled';
+ $install = version_compare(\Drupal::VERSION, '10.3', '>=') ? 'install' : 'enable';
+
+ $session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ // Installation process:
+ $this->drupalGet('/admin/modules');
+ $page->checkField('edit-modules-image-captcha-enable');
+ $page->pressButton('edit-submit');
+ // Also install required modules:
+ $session->statusCodeEquals(200);
+ $session->pageTextContains('Some required modules must be ' . $installed);
+ $session->pageTextContains('You must ' . $install . ' the CAPTCHA module to install Image CAPTCHA.');
+ // Continue:
+ $page->pressButton('edit-submit');
+ $session->statusCodeEquals(200);
+ $session->pageTextContains('2 modules have been ' . $installed . ': Image CAPTCHA, CAPTCHA');
+ // Go to uninstallation page an uninstall image_captcha:
+ $this->drupalGet('/admin/modules/uninstall');
+ $session->statusCodeEquals(200);
+ $page->checkField('edit-uninstall-image-captcha');
+ $page->pressButton('edit-submit');
+ $session->statusCodeEquals(200);
+ // Confirm uninstall:
+ $page->pressButton('edit-submit');
+ $session->statusCodeEquals(200);
+ $session->pageTextContains('The selected modules have been uninstalled.');
+ // Retest the frontpage:
+ $this->drupalGet('');
+ // Ensure the status code is success:
+ $session->statusCodeEquals(200);
+ // Ensure the correct test page is loaded as front page:
+ $session->pageTextContains('Test page text.');
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/CaptchaPointInterface.php b/web/modules/contrib/captcha/src/CaptchaPointInterface.php
new file mode 100755
index 000000000..0e322e0bb
--- /dev/null
+++ b/web/modules/contrib/captcha/src/CaptchaPointInterface.php
@@ -0,0 +1,64 @@
+configFactory = $configFactory;
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('config.factory')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo() {
+ $captcha_element = [
+ '#input' => TRUE,
+ '#process' => [[static::class, 'processCaptchaElement']],
+ // The type of challenge: e.g. 'default', 'captcha/Math', etc.
+ '#captcha_type' => CaptchaConstants::CAPTCHA_TYPE_DEFAULT,
+ '#default_value' => '',
+ // CAPTCHA in admin mode: presolve the CAPTCHA and always show
+ // it (despite previous successful responses).
+ '#captcha_admin_mode' => FALSE,
+ // The default CAPTCHA validation function.
+ // @todo should this be a single string or an array of strings?
+ '#captcha_validate' => 'captcha_validate_strict_equality',
+ ];
+ // Override the default CAPTCHA validation function for case
+ // insensitive validation.
+ // @todo shouldn't this be done somewhere else, e.g. in form_alter?
+ if (CaptchaConstants::CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE == $this->configFactory->get('captcha.settings')
+ ->get('default_validation')
+ ) {
+ $captcha_element['#captcha_validate'] = 'captcha_validate_case_insensitive_equality';
+ }
+ return $captcha_element;
+ }
+
+ /**
+ * Process callback for CAPTCHA form element.
+ */
+ public static function processCaptchaElement(&$element, FormStateInterface $form_state, &$complete_form) {
+ // Add captcha.inc file.
+ \Drupal::moduleHandler()->loadInclude('captcha', 'inc');
+
+ // Add JavaScript for general CAPTCHA functionality.
+ $element['#attached']['library'][] = 'captcha/base';
+
+ if ($form_state->getTriggeringElement() && isset($form_state->getTriggeringElement()['#limit_validation_errors']) && is_array($form_state->getTriggeringElement()['#limit_validation_errors'])) {
+ // This is a partial (ajax) submission with limited validation. Do not
+ // change anything about the captcha element, assume that it will not
+ // update the captcha element, do not generate anything, which keeps the
+ // current token intact for the real submission.
+ return $element;
+ }
+
+ // Get the form ID of the form we are currently processing (which is not
+ // necessary the same form that is submitted (if any).
+ $this_form_id = isset($complete_form['form_id']['#value']) ?
+ preg_replace("/[^a-z0-9_-]/", "", (string) $complete_form['form_id']['#value'])
+ : NULL;
+
+ // Get the CAPTCHA session ID.
+ // If there is a submitted form: try to retrieve and reuse the
+ // CAPTCHA session ID from the posted data.
+ [$posted_form_id, $posted_captcha_sid] = _captcha_get_posted_captcha_info($element, $form_state, $this_form_id);
+ if ($this_form_id == $posted_form_id && isset($posted_captcha_sid)) {
+ $captcha_sid = $posted_captcha_sid;
+ }
+ else {
+ // Generate a new CAPTCHA session if we could
+ // not reuse one from a posted form.
+ $captcha_sid = _captcha_generate_captcha_session($this_form_id, CaptchaConstants::CAPTCHA_STATUS_UNSOLVED);
+ $captcha_token = Crypt::randomBytesBase64();
+ \Drupal::database()->update('captcha_sessions')
+ ->fields(['token' => $captcha_token])
+ ->condition('csid', $captcha_sid)
+ ->execute();
+ }
+
+ // Store CAPTCHA session ID as hidden field.
+ // Strictly speaking, it is not necessary to send the CAPTCHA session id
+ // with the form, as the one time CAPTCHA token (see lower) is enough.
+ // However, we still send it along, because it can help debugging
+ // problems on live sites with only access to the markup.
+ $element['captcha_sid'] = [
+ '#type' => 'hidden',
+ '#value' => $captcha_sid,
+ ];
+
+ // Store CAPTCHA token as hidden field.
+ $captcha_token = \Drupal::database()
+ ->select('captcha_sessions', 'cs')
+ ->fields('cs', ['token'])
+ ->condition('csid', $captcha_sid)
+ ->execute()
+ ->fetchField();
+
+ $element['captcha_token'] = [
+ '#type' => 'hidden',
+ '#value' => $captcha_token,
+ ];
+
+ // Get implementing module and challenge for CAPTCHA.
+ [$captcha_type_module, $captcha_type_challenge] = _captcha_parse_captcha_type($element['#captcha_type']);
+
+ // Store CAPTCHA information for further processing in
+ // - $form_state->get('captcha_info'), which survives
+ // a form rebuild (e.g. node preview),
+ // useful in _captcha_get_posted_captcha_info().
+ // - $element['#captcha_info'], for post processing functions that do not
+ // receive a $form_state argument (e.g. the pre_render callback).
+ // Added a new access attribute,
+ // by default it will be true if access attribute
+ // not defined in a custom form.
+ $info = [
+ 'this_form_id' => $this_form_id,
+ 'posted_form_id' => $posted_form_id,
+ 'captcha_sid' => $captcha_sid,
+ 'module' => $captcha_type_module,
+ 'captcha_type' => $captcha_type_challenge,
+ 'access' => $element['#access'] ?? CaptchaConstants::CAPTCHA_FIELD_DEFAULT_ACCESS,
+ ];
+ $form_state->set('captcha_info', $info);
+ $element['#captcha_info'] = [
+ 'form_id' => $this_form_id,
+ 'captcha_sid' => $captcha_sid,
+ ];
+
+ if (_captcha_required_for_user($captcha_sid, $this_form_id) || $element['#captcha_admin_mode']) {
+ // Generate a CAPTCHA and its solution
+ // (note that the CAPTCHA session ID is given as third argument).
+ $captcha = \Drupal::moduleHandler()
+ ->invoke($captcha_type_module, 'captcha', [
+ 'generate',
+ $captcha_type_challenge,
+ $captcha_sid,
+ ]);
+
+ // Allow other modules to alter the generated captcha:
+ \Drupal::moduleHandler()->alter('captcha', $captcha, $info);
+
+ // @todo Isn't this moment a bit late to figure out
+ // that we don't need CAPTCHA?
+ if (!isset($captcha)) {
+ return $element;
+ }
+
+ if (!isset($captcha['form']) || !isset($captcha['solution'])) {
+ // The selected module did not return what we expected:
+ // log about it and quit.
+ \Drupal::logger('CAPTCHA')->error(
+ 'CAPTCHA problem: unexpected result from hook_captcha() of module %module when trying to retrieve challenge type %type for form %form_id.',
+ [
+ '%type' => $captcha_type_challenge,
+ '%module' => $captcha_type_module,
+ '%form_id' => $this_form_id,
+ ]
+ );
+
+ return $element;
+ }
+ // Add form elements from challenge as children to CAPTCHA form element.
+ $element['captcha_widgets'] = $captcha['form'];
+
+ // Add a validation callback for the CAPTCHA form element
+ // (when not in admin mode).
+ if (!$element['#captcha_admin_mode']) {
+ $element['#element_validate'] = ['captcha_validate'];
+ }
+
+ // Set a custom CAPTCHA validate function if requested.
+ if (isset($captcha['captcha_validate'])) {
+ $element['#captcha_validate'] = $captcha['captcha_validate'];
+ }
+
+ // Set the theme function.
+ $element['#theme'] = 'captcha';
+
+ // Provide the captcha type:
+ $element['#captcha_type_challenge'] = $captcha_type_challenge;
+
+ // Add pre_render callback for additional CAPTCHA processing.
+ if (!isset($element['#pre_render'])) {
+ $element['#pre_render'] = [];
+ }
+ $element['#pre_render'][] = [Captcha::class, 'preRenderProcess'];
+
+ // Store the solution in the #captcha_info array.
+ $element['#captcha_info']['solution'] = $captcha['solution'];
+
+ // Store if this captcha type is cacheable:
+ // A cacheable captcha must not depend on the sid or solution, but be
+ // independent - like for example recaptcha.
+ $element['#captcha_info']['cacheable'] = !empty($captcha['cacheable']);
+
+ if (!empty($element['#captcha_info']['cacheable'])) {
+ // This is only used to avoid the re-use message.
+ $element['captcha_cacheable'] = [
+ '#type' => 'hidden',
+ '#value' => 1,
+ ];
+ }
+
+ // Make sure we can use a top level form value
+ // $form_state->getValue('captcha_response'),
+ // even if the form has #tree=true.
+ $element['#tree'] = FALSE;
+ }
+
+ return $element;
+ }
+
+ /**
+ * Pre-render callback for additional processing of a CAPTCHA form element.
+ *
+ * This encompasses tasks that should happen after the general FAPI processing
+ * (building, submission and validation) but before rendering
+ * (e.g. storing the solution).
+ *
+ * @param array $element
+ * The CAPTCHA form element.
+ *
+ * @return array
+ * The manipulated element.
+ */
+ public static function preRenderProcess(array $element) {
+ \Drupal::moduleHandler()->loadInclude('captcha', 'inc');
+
+ // Get form and CAPTCHA information.
+ $captcha_info = $element['#captcha_info'];
+ $form_id = $captcha_info['form_id'];
+ $captcha_sid = (int) ($captcha_info['captcha_sid']);
+ // Check if CAPTCHA is still required.
+ // This check is done in a first phase during the element processing
+ // (@see captcha_process), but it is also done here for better support
+ // of multi-page forms. Take previewing a node submission for example:
+ // when the challenge is solved correctly on preview, the form is still
+ // not completely submitted, but the CAPTCHA can be skipped.
+ if (_captcha_required_for_user($captcha_sid, $form_id) || $element['#captcha_admin_mode']) {
+ // Update captcha_sessions table: store the solution
+ // of the generated CAPTCHA.
+ _captcha_update_captcha_session($captcha_sid, $captcha_info['solution']);
+
+ // Handle the response field if it is available and if it is a textfield.
+ if (isset($element['captcha_widgets']['captcha_response']['#type']) && $element['captcha_widgets']['captcha_response']['#type'] == 'textfield') {
+ // Before rendering: presolve an admin mode challenge or
+ // empty the value of the captcha_response form item.
+ $value = $element['#captcha_admin_mode'] ? $captcha_info['solution'] : '';
+ $element['captcha_widgets']['captcha_response']['#value'] = $value;
+ }
+ }
+ else {
+ // Remove CAPTCHA widgets from form.
+ unset($element['captcha_widgets']);
+ }
+
+ return $element;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Entity/CaptchaPoint.php b/web/modules/contrib/captcha/src/Entity/CaptchaPoint.php
new file mode 100755
index 000000000..5b3e3e76b
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Entity/CaptchaPoint.php
@@ -0,0 +1,133 @@
+formId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return $this->formId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFormId($form_id) {
+ $this->formId = $form_id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLabel() {
+ return $this->label;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setLabel($label) {
+ $this->label = $label;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCaptchaType() {
+ if (isset($this->captchaType)) {
+ return $this->captchaType;
+ }
+ else {
+ return static::getConfigManager()
+ ->getConfigFactory()
+ ->get('captcha.settings')
+ ->get('default_challenge');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCaptchaType($captcha_type) {
+ $this->captchaType = $captcha_type != CaptchaConstants::CAPTCHA_TYPE_DEFAULT ? $captcha_type : NULL;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Entity/Controller/CaptchaPointListBuilder.php b/web/modules/contrib/captcha/src/Entity/Controller/CaptchaPointListBuilder.php
new file mode 100755
index 000000000..f7888a462
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Entity/Controller/CaptchaPointListBuilder.php
@@ -0,0 +1,35 @@
+t('Captcha Point form ID');
+ $header['captcha_type'] = $this->t('Captcha Point challenge type');
+ $header['captcha_status'] = $this->t('Status');
+
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ $row['form_id'] = $entity->id();
+ $row['captcha_type'] = $entity->getCaptchaType();
+ $row['captcha_status'] = $entity->status() ? $this->t('Enabled') : $this->t('Disabled');
+
+ return $row + parent::buildRow($entity);
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/EventSubscriber/CaptchaCachedSettingsSubscriber.php b/web/modules/contrib/captcha/src/EventSubscriber/CaptchaCachedSettingsSubscriber.php
new file mode 100644
index 000000000..d518696c8
--- /dev/null
+++ b/web/modules/contrib/captcha/src/EventSubscriber/CaptchaCachedSettingsSubscriber.php
@@ -0,0 +1,54 @@
+elementInfo = $elementInfo;
+ }
+
+ /**
+ * Clearing the cached definitions whenever the settings are modified.
+ *
+ * @param \Drupal\Core\Config\ConfigCrudEvent $event
+ * The Event to process.
+ */
+ public function onSave(ConfigCrudEvent $event) {
+ // Changing the Captcha settings means that any page might result in other
+ // settings for captcha so the cached definitions need to be cleared.
+ if ($event->getConfig()->getName() === 'captcha.settings') {
+ $this->elementInfo->clearCachedDefinitions();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents(): array {
+ $events[ConfigEvents::SAVE][] = ['onSave'];
+ return $events;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Form/CaptchaExamplesForm.php b/web/modules/contrib/captcha/src/Form/CaptchaExamplesForm.php
new file mode 100755
index 000000000..73971d636
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Form/CaptchaExamplesForm.php
@@ -0,0 +1,114 @@
+moduleHandler = $moduleHandler;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('module_handler')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'captcha_examples';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $module = NULL, $challenge = NULL) {
+ $form = [];
+ if ($module && $challenge) {
+ // Generate 10 example challenges.
+ for ($i = 0; $i < 10; $i++) {
+ $form["challenge_{$i}"] = $this->buildChallenge($module, $challenge);
+ }
+ }
+ else {
+ // Generate a list with examples of the available CAPTCHA types.
+ $form['info'] = [
+ '#markup' => $this->t('This page gives an overview of all available challenge types, generated with their current settings.'),
+ ];
+ $challenges = [];
+ $this->moduleHandler->invokeAllWith('captcha', function (callable $hook, string $module) use (&$challenges) {
+ if ($challenge = $hook('list')) {
+ $challenges[$module] = $challenge;
+ }
+ });
+
+ if ($challenges) {
+ foreach ($challenges as $module => $challenge_list) {
+ foreach ($challenge_list as $ckey => $challenge) {
+ $form["captcha_{$module}_{$ckey}"] = [
+ '#type' => 'details',
+ '#title' => $this->t('Challenge %challenge by module %module', [
+ '%challenge' => $challenge,
+ '%module' => $module,
+ ]),
+ 'challenge' => $this->buildChallenge($module, $challenge),
+ 'more_examples' => [
+ '#markup' => Link::fromTextAndUrl($this->t('10 more examples of this challenge.'), Url::fromRoute('captcha_examples', [
+ 'module' => $module,
+ 'challenge' => $challenge,
+ ]))->toString(),
+ ],
+ ];
+ }
+ }
+ }
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {}
+
+ /**
+ * Returns a renderable array for a given CAPTCHA challenge.
+ */
+ protected function buildChallenge($module, $challenge) {
+ return [
+ '#type' => 'captcha',
+ '#captcha_type' => $module . '/' . $challenge,
+ '#captcha_admin_mode' => TRUE,
+ ];
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Form/CaptchaPointDeleteForm.php b/web/modules/contrib/captcha/src/Form/CaptchaPointDeleteForm.php
new file mode 100755
index 000000000..45ab12115
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Form/CaptchaPointDeleteForm.php
@@ -0,0 +1,44 @@
+t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('captcha_point.list');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->entity->delete();
+ $this->messenger()->addMessage($this->t('Captcha point %label has been deleted.', ['%label' => $this->entity->label() ?? $this->entity->id()]));
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Form/CaptchaPointDisableForm.php b/web/modules/contrib/captcha/src/Form/CaptchaPointDisableForm.php
new file mode 100644
index 000000000..02f7c66c4
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Form/CaptchaPointDisableForm.php
@@ -0,0 +1,52 @@
+t('Are you sure you want to disable the Captcha?');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('This will disable the captcha.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('captcha_point.list');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Disable');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->entity->disable();
+ $this->entity->save();
+ $this->messenger()->addMessage($this->t('Captcha point %label has been disabled.', ['%label' => $this->entity->label() ?? $this->entity->id()]));
+ $form_state->setRedirect('captcha_point.list');
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Form/CaptchaPointEnableForm.php b/web/modules/contrib/captcha/src/Form/CaptchaPointEnableForm.php
new file mode 100644
index 000000000..c54aa27ec
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Form/CaptchaPointEnableForm.php
@@ -0,0 +1,52 @@
+t('Are you sure you want to enable the Captcha?');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('This will enable the captcha.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('captcha_point.list');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Enable');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->entity->enable();
+ $this->entity->save();
+ $this->messenger()->addMessage($this->t('Captcha point %label has been enabled.', ['%label' => $this->entity->label() ?? $this->entity->id()]));
+ $form_state->setRedirect('captcha_point.list');
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Form/CaptchaPointForm.php b/web/modules/contrib/captcha/src/Form/CaptchaPointForm.php
new file mode 100755
index 000000000..d2f37c2d0
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Form/CaptchaPointForm.php
@@ -0,0 +1,126 @@
+requestStack = $request_stack;
+ $this->captchaService = $captcha_service;
+ }
+
+ /**
+ * Create Captcha Points.
+ *
+ * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+ * Event to create Captcha points.
+ *
+ * @return static
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('request_stack'),
+ $container->get('captcha.helper')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+
+ /** @var \Drupal\captcha\CaptchaPointInterface $captchaPoint */
+ $captcha_point = $this->entity;
+
+ // Support to set a default form_id through a query argument.
+ $request = $this->requestStack->getCurrentRequest();
+ if ($captcha_point->isNew() && !$captcha_point->id() && $request->query->has('form_id')) {
+ $captcha_point->set('formId', $request->query->get('form_id'));
+ $captcha_point->set('label', $request->query->get('form_id'));
+ }
+
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Form ID'),
+ '#description' => $this->t('Also works with the base form ID.'),
+ '#default_value' => $captcha_point->label(),
+ '#required' => TRUE,
+ ];
+
+ $form['formId'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $captcha_point->id(),
+ '#machine_name' => [
+ 'exists' => 'captcha_point_load',
+ ],
+ '#disable' => !$captcha_point->isNew(),
+ '#required' => TRUE,
+ ];
+
+ // Select widget for CAPTCHA type.
+ $form['captchaType'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Challenge type'),
+ '#description' => $this->t('The CAPTCHA type to use for this form.'),
+ '#default_value' => $captcha_point->getCaptchaType() ?: $this->config('captcha.settings')->get('default_challenge'),
+ '#options' => $this->captchaService->getAvailableChallengeTypes(),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ /** @var CaptchaPoint $captcha_point */
+ $captcha_point = $this->entity;
+ $status = $captcha_point->save();
+
+ if ($status == SAVED_NEW) {
+ $this->messenger()->addMessage($this->t('Captcha Point for %form_id form was created.', [
+ '%form_id' => $captcha_point->getFormId(),
+ ]));
+ }
+ else {
+ $this->messenger()->addMessage($this->t('Captcha Point for %form_id form was updated.', [
+ '%form_id' => $captcha_point->getFormId(),
+ ]));
+ }
+ $form_state->setRedirect('captcha_point.list');
+ return $status;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Form/CaptchaSettingsForm.php b/web/modules/contrib/captcha/src/Form/CaptchaSettingsForm.php
new file mode 100755
index 000000000..65bdd588a
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Form/CaptchaSettingsForm.php
@@ -0,0 +1,332 @@
+cacheBackend = $container->get('cache.default');
+ $instance->moduleHandler = $container->get('module_handler');
+ $instance->captchaService = $container->get('captcha.helper');
+ $instance->requestStack = $container->get('request_stack');
+
+ return $instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEditableConfigNames() {
+ return ['captcha.settings'];
+ }
+
+ /**
+ * Implements \Drupal\Core\Form\FormInterface::getFormID().
+ */
+ public function getFormId() {
+ return 'captcha_settings';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $config = $this->config('captcha.settings');
+ $this->moduleHandler->loadInclude('captcha', 'inc');
+
+ $form['default_challenge'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Default challenge type'),
+ '#description' => $this->t('Select the default CAPTCHA Point challenge type. This can be overridden for each CAPTCHA Point individually.'),
+ '#options' => $this->captchaService->getAvailableChallengeTypes(FALSE),
+ '#default_value' => $config->get('default_challenge'),
+ ];
+
+ // Option for enabling CAPTCHA for all forms.
+ $form['enable_globally'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Add CAPTCHA challenges on all forms'),
+ '#description' => $this->t('Adds CAPTCHA to all Drupal forms, regardless of the Captcha Points list. Note, that the captcha point default challenge will be used as the challenge type for the created CAPTCHA challenges.'),
+ '#default_value' => $config->get('enable_globally'),
+ ];
+ $form['enable_globally_on_admin_routes'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Additionally add CAPTCHA challenges on admin forms'),
+ '#default_value' => $config->get('enable_globally_on_admin_routes'),
+ '#states' => [
+ 'invisible' => [
+ ':input[name="enable_globally"]' => ['checked' => FALSE],
+ ],
+ ],
+ ];
+ // Field for the CAPTCHA administration mode.
+ $form['administration_mode'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Add CAPTCHA administration information to forms'),
+ '#default_value' => $config->get('administration_mode'),
+ '#description' => $this->t('This option makes it easy to manage CAPTCHA settings on forms. When enabled, users with the administer CAPTCHA settings permission will see a fieldset with CAPTCHA administration links and informations on all forms, except on administrative pages.'),
+ ];
+ // Field for the CAPTCHAs on admin pages.
+ $form['administration_mode_on_admin_routes'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Additionally add administration informations on admin pages'),
+ '#description' => $this->t("Typically this isn't needed. In some situations, e.g. in the case of demo sites, it can be useful to allow CAPTCHAs on administrative pages."),
+ '#default_value' => $config->get('administration_mode_on_admin_routes'),
+ '#states' => [
+ 'invisible' => [
+ ':input[name="administration_mode"]' => ['checked' => FALSE],
+ ],
+ ],
+ ];
+
+ // Adding configuration for ip protection.
+ $form['whitelist_ips_settings'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Whitelisted IP Addresses'),
+ '#description' => $this->t('Enter the IP addresses or IP address ranges you wish to whitelist. All CAPTCHA challenges will be skipped for these IP addresses.'),
+ '#open' => !empty($config->get('whitelist_ips')),
+ ];
+
+ $ip_address = $this->requestStack->getCurrentRequest()->getClientIp();
+ $form['whitelist_ips_settings']['whitelist_ips'] = [
+ '#title' => $this->t('IP addresses list'),
+ '#type' => 'textarea',
+ '#required' => FALSE,
+ '#default_value' => $config->get('whitelist_ips'),
+ '#description' => $this->t('Enter one IP-address per row in the format XXX.XXX.XXX.XXX. Alternatively you can also define IP-address ranges per row in the format XXX.XXX.XXX.YYY-XXX.XXX.XXX.ZZZ. No spaces allowed. Your current IP address is %ip_address.', ['%ip_address' => $ip_address]),
+ ];
+
+ // Button for clearing the CAPTCHA placement cache.
+ // Based on Drupal core's "Clear all caches" (performance settings page).
+ $form['placement_caching'] = [
+ '#type' => 'item',
+ '#title' => $this->t('CAPTCHA placement caching'),
+ '#description' => $this->t('For efficiency, the positions of the CAPTCHA elements in each of the configured forms are cached. Most of the time, the structure of a form does not change and it would be a waste to recalculate the positions every time. Occasionally however, the form structure can change (e.g. during site building) and clearing the CAPTCHA placement cache can be required to fix the CAPTCHA placement.'),
+ ];
+ $form['placement_caching']['placement_cache_clear'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Clear the CAPTCHA placement cache'),
+ '#submit' => ['::clearCaptchaPlacementCacheSubmit'],
+ ];
+
+ $form['title'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Challenge title'),
+ '#description' => $this->t('Configure the title for the CAPTCHA form. Leave empty to show no title. Default: "@title_default"', ['@title_default' => $this->t('CAPTCHA')]),
+ '#default_value' => _captcha_get_title(),
+ '#maxlength' => 256,
+ ];
+
+ $form['description'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Challenge description'),
+ '#description' => $this->t('Configurable description of the CAPTCHA. Leave empty to show no description. Default: "@description_default"', ['@description_default' => $this->t('This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.')]),
+ '#default_value' => _captcha_get_description(),
+ '#maxlength' => 256,
+ ];
+
+ // Field for the wrong captcha response error message.
+ $form['wrong_captcha_response_message'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Wrong CAPTCHA response error message'),
+ '#description' => $this->t('Configurable error message that the user gets when it enters an incorrect CAPTCHA answer.'),
+ '#default_value' => _captcha_get_error_message(),
+ '#maxlength' => 256,
+ '#required' => TRUE,
+ ];
+
+ // Option for case sensitive/insensitive validation of the responses.
+ $form['default_validation'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Default CAPTCHA validation'),
+ '#description' => $this->t('Define how the response should be processed by default. Note that the modules that provide the actual challenges can override or ignore this.'),
+ '#options' => [
+ CaptchaConstants::CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE => $this->t('Case sensitive validation: the response has to exactly match the solution.'),
+ CaptchaConstants::CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE => $this->t('Case insensitive validation: lowercase/uppercase errors are ignored.'),
+ ],
+ '#default_value' => $config->get('default_validation'),
+ ];
+
+ // Field for CAPTCHA persistence.
+ // @todo for D7: Rethink/simplify the explanation and UI strings.
+ $form['persistence'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Persistence'),
+ '#default_value' => $config->get('persistence'),
+ '#options' => [
+ CaptchaConstants::CAPTCHA_PERSISTENCE_SHOW_ALWAYS => $this->t('Always add a challenge.'),
+ CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE => $this->t('Omit challenges in a multi-step/preview workflow once the user successfully responds to a challenge.'),
+ CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE => $this->t('Omit challenges on a form type once the user successfully responds to a challenge on a form of that type.'),
+ CaptchaConstants::CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL => $this->t('Omit challenges on all forms once the user successfully responds to any challenge on the site.'),
+ ],
+ '#description' => $this->t('Define if challenges should be omitted during the rest of a session once the user successfully responds to a challenge.'),
+ ];
+
+ // Enable wrong response counter.
+ $form['enable_stats'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enable statistics'),
+ '#description' => $this->t('Keep CAPTCHA related counters in the status report. Note that this comes with a performance penalty as updating the counters results in clearing the variable cache.', [
+ ':statusreport' => Url::fromRoute('system.status')->toString(),
+ ]),
+ '#default_value' => $config->get('enable_stats'),
+ ];
+
+ // Option for logging wrong responses.
+ $form['log_wrong_responses'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Log wrong responses'),
+ '#description' => $this->t('Report information about wrong responses to the log.'),
+ '#default_value' => $config->get('log_wrong_responses'),
+ ];
+
+ // Replace the description with a link if dblog.module is enabled.
+ if ($this->moduleHandler->moduleExists('dblog')) {
+ $form['log_wrong_responses']['#description'] = $this->t('Report information about wrong responses to the log.', [
+ ':dblog' => Url::fromRoute('dblog.overview')->toString(),
+ ]);
+ }
+
+ // Submit button.
+ $form['actions'] = ['#type' => 'actions'];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save configuration'),
+ ];
+
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ // Validating whitelisted ip addresses.
+ $whitelist_ips_value = trim($form_state->getValue('whitelist_ips', ''));
+ if (!empty($whitelist_ips_value)) {
+ $whitelist_ips = captcha_whitelist_ips_parse_values($whitelist_ips_value);
+
+ // Checking single ip addresses.
+ foreach ($whitelist_ips[CaptchaConstants::CAPTCHA_WHITELIST_IP_ADDRESS] as $ip_address) {
+ if (filter_var($ip_address, FILTER_VALIDATE_IP) == FALSE) {
+ $form_state->setErrorByName('whitelist_ips', $this->t('IP address %ip_address is not valid.', ['%ip_address' => $ip_address]));
+ }
+ }
+
+ // Checking ip ranges.
+ foreach ($whitelist_ips[CaptchaConstants::CAPTCHA_WHITELIST_IP_RANGE] as $ip_range) {
+ [$ip_lower, $ip_upper] = explode('-', $ip_range, 2);
+
+ if (filter_var($ip_lower, FILTER_VALIDATE_IP) == FALSE) {
+ $form_state->setErrorByName('whitelist_ips', $this->t('Lower IP address %ip_address in range %ip_range is not valid.', [
+ '%ip_address' => $ip_lower,
+ '%ip_range' => $ip_range,
+ ]));
+ }
+
+ if (filter_var($ip_upper, FILTER_VALIDATE_IP) == FALSE) {
+ $form_state->setErrorByName('whitelist_ips', $this->t('Upper IP address %ip_address in range %ip_range is not valid.', [
+ '%ip_address' => $ip_upper,
+ '%ip_range' => $ip_range,
+ ]));
+ }
+
+ $ip_lower_dec = (float) sprintf("%u", ip2long($ip_lower));
+ $ip_upper_dec = (float) sprintf("%u", ip2long($ip_upper));
+
+ if ($ip_lower_dec == $ip_upper_dec) {
+ $form_state->setErrorByName('whitelist_ips', $this->t('Lower and upper IP addresses should be different. Please correct range %ip_range.', ['%ip_range' => $ip_range]));
+ }
+ elseif ($ip_lower_dec > $ip_upper_dec) {
+ $form_state->setErrorByName('whitelist_ips', $this->t("Lower IP can't be greater than upper IP addresses in range. Please correct range %ip_range.", ['%ip_range' => $ip_range]));
+ }
+ }
+ }
+
+ parent::validateForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $config = $this->config('captcha.settings');
+ $config->set('administration_mode', $form_state->getValue('administration_mode'));
+ $config->set('administration_mode_on_admin_routes', ($form_state->getValue('administration_mode') && $form_state->getValue('administration_mode_on_admin_routes')));
+ $config->set('enable_globally', $form_state->getValue('enable_globally'));
+ $config->set('enable_globally_on_admin_routes', ($form_state->getValue('enable_globally') && $form_state->getValue('enable_globally_on_admin_routes')));
+ $config->set('default_challenge', $form_state->getValue('default_challenge'));
+
+ // Whitelisted ip addresses and ranges.
+ $config->set('whitelist_ips', $form_state->getValue('whitelist_ips'));
+
+ // Save the CAPTCHA title:
+ $config->set('title', $form_state->getValue('title'));
+ // Save the CAPTCHA description:
+ $config->set('description', $form_state->getValue('description'));
+
+ $config->set('wrong_captcha_response_message', $form_state->getValue('wrong_captcha_response_message'));
+ $config->set('default_validation', $form_state->getValue('default_validation'));
+ $config->set('persistence', $form_state->getValue('persistence'));
+ $config->set('enable_stats', $form_state->getValue('enable_stats'));
+ $config->set('log_wrong_responses', $form_state->getValue('log_wrong_responses'));
+ $config->save();
+
+ parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * Submit callback; clear CAPTCHA placement cache.
+ *
+ * @param array $form
+ * Form structured array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state structured array.
+ */
+ public function clearCaptchaPlacementCacheSubmit(array $form, FormStateInterface $form_state) {
+ $this->cacheBackend->delete('captcha_placement_map_cache');
+ $this->messenger()->addMessage($this->t('Cleared the CAPTCHA placement cache.'));
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Plugin/migrate/process/CaptchaTypeFormatter.php b/web/modules/contrib/captcha/src/Plugin/migrate/process/CaptchaTypeFormatter.php
new file mode 100644
index 000000000..d83eb5c81
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Plugin/migrate/process/CaptchaTypeFormatter.php
@@ -0,0 +1,35 @@
+getSourceProperty('module') ?? 'captcha';
+ $type = $row->getSourceProperty('captcha_type');
+ return $module . '/' . $type;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Plugin/migrate/source/CaptchaPoints.php b/web/modules/contrib/captcha/src/Plugin/migrate/source/CaptchaPoints.php
new file mode 100644
index 000000000..3f3932f2d
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Plugin/migrate/source/CaptchaPoints.php
@@ -0,0 +1,59 @@
+get('state'),
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {
+ return $this->select('captcha_points', 'c')->fields('c');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fields() {
+ return [
+ 'form_id' => $this->t('The name of the form'),
+ 'module' => $this->t('The captcha point providing module.'),
+ 'captcha_type' => $this->t('The captcha type.'),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIds() {
+ $ids['form_id']['type'] = 'string';
+ return $ids;
+ }
+
+}
diff --git a/web/modules/contrib/captcha/src/Service/CaptchaService.php b/web/modules/contrib/captcha/src/Service/CaptchaService.php
new file mode 100644
index 000000000..770d96ad9
--- /dev/null
+++ b/web/modules/contrib/captcha/src/Service/CaptchaService.php
@@ -0,0 +1,154 @@
+moduleHandler = $module_handler;
+ }
+
+ /**
+ * Return an array with the available CAPTCHA types.
+ *
+ * For use as options array for a select form elements.
+ *
+ * @param bool $add_special_options
+ * If true: also add the 'default' option.
+ *
+ * @return array
+ * An associative array mapping "$module/$type" to
+ * "$type (from module $module)" with $module the module name
+ * implementing the CAPTCHA and $type the name of the CAPTCHA type.
+ */
+ public function getAvailableChallengeTypes(bool $add_special_options = TRUE) {
+ $challenges = [];
+
+ if ($add_special_options) {
+ $challenges[CaptchaConstants::CAPTCHA_TYPE_DEFAULT] = $this->t('Default challenge type');
+ }
+
+ // We do our own version of Drupal's module_invoke_all() here because
+ // we want to build an array with custom keys and values.
+ $types = [];
+ $this->moduleHandler->invokeAllWith('captcha', function (callable $hook, string $module) use (&$types) {
+ if ($type = $hook('list')) {
+ if (!is_array($type)) {
+ $types[$module] = [$type];
+ }
+ else {
+ $types[$module] = $type;
+ }
+ }
+ });
+ if (!empty($types)) {
+ foreach ($types as $module => $values) {
+ foreach ($values as $value) {
+ $challenges["$module/$value"] = $this->t('@type (from module @module)', [
+ '@type' => $value,
+ '@module' => $module,
+ ]);
+ }
+ }
+ }
+
+ return $challenges;
+ }
+
+ /**
+ * Helper function to insert a CAPTCHA element before a given form element.
+ *
+ * @param array $form
+ * the form to add the CAPTCHA element to.
+ * @param null|array $placement
+ * information where the CAPTCHA element should be inserted.
+ * $placement should be an associative array with fields:
+ * - 'path': path (array of path items) of the container in
+ * the form where the CAPTCHA element should be inserted.
+ * - 'key': the key of the element before which the CAPTCHA element
+ * should be inserted. If the field 'key' is undefined or NULL,
+ * the CAPTCHA will just be appended in the container.
+ * - 'weight': if 'key' is not NULL: should be the weight of the
+ * element defined by 'key'. If 'key' is NULL and weight is not NULL:
+ * set the weight property of the CAPTCHA element to this value.
+ * @param array $captcha_element
+ * the CAPTCHA element to insert.
+ */
+ public function insertCaptchaElement(array &$form, ?array $placement = NULL, ?array $captcha_element = NULL) {
+ // Get path, target and target weight or use defaults if not available.
+ $target_key = $placement['key'] ?? NULL;
+ $target_weight = $placement['weight'] ?? NULL;
+ $path = $placement['path'] ?? [];
+
+ // Walk through the form along the path.
+ $form_stepper = &$form;
+ foreach ($path as $step) {
+ if (isset($form_stepper[$step])) {
+ $form_stepper = &$form_stepper[$step];
+ }
+ else {
+ // Given path is invalid: stop stepping and
+ // continue in best effort (append instead of insert).
+ $target_key = NULL;
+ break;
+ }
+ }
+
+ // If no target is available: just append the CAPTCHA element
+ // to the container.
+ if ($target_key == NULL || !array_key_exists($target_key, $form_stepper)) {
+ // Optionally, set weight of CAPTCHA element.
+ if ($target_weight != NULL) {
+ $captcha_element['#weight'] = $target_weight;
+ }
+ $form_stepper['captcha'] = $captcha_element;
+ }
+ // If there is a target available: make sure the CAPTCHA element
+ // comes right before it.
+ else {
+ // If target has a weight: set weight of CAPTCHA element a bit smaller
+ // and just append the CAPTCHA: sorting will fix the ordering anyway.
+ if ($target_weight != NULL) {
+ $captcha_element['#weight'] = $target_weight - .1;
+ $form_stepper['captcha'] = $captcha_element;
+ }
+ else {
+ // If we can't play with weights: insert the CAPTCHA element
+ // at the right position. Because PHP lacks a function for
+ // this (array_splice() comes close, but it does not preserve
+ // the key of the inserted element), we do it by hand: chop of
+ // the end, append the CAPTCHA element and put the end back.
+ $offset = array_search($target_key, array_keys($form_stepper));
+ $end = array_splice($form_stepper, $offset);
+ $form_stepper['captcha'] = $captcha_element;
+ foreach ($end as $k => $v) {
+ $form_stepper[$k] = $v;
+ }
+ }
+
+ }
+ }
+
+}
diff --git a/web/modules/contrib/captcha/templates/captcha.html.twig b/web/modules/contrib/captcha/templates/captcha.html.twig
new file mode 100755
index 000000000..ce347c129
--- /dev/null
+++ b/web/modules/contrib/captcha/templates/captcha.html.twig
@@ -0,0 +1,57 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a captcha.
+ *
+ * Available variables:
+ * - is_visible: Boolean to indicate if the CAPTCHA is visible or was
+ * already solved (but element still needed for hidden inputs).
+ * - attributes: HTML attributes for the containing element.
+ * - title: The captcha title
+ * - description: The captcha description
+ * - element: The captcha element itself (image, recaptcha, ...)
+ *
+ * @see template_preprocess_captcha()
+ *
+ * @ingroup themeable
+ */
+#}
+
+{%
+ set classes = [
+ 'captcha',
+ ('captcha-type-challenge--' ~ element['#captcha_type_challenge'])|clean_class,
+ ]
+%}
+{# Tell search engines not to index the captcha element: #}
+{% set attributes = attributes.addClass(classes).setAttribute('data-nosnippet', true) %}
+
+{% block captcha %}
+ {% if is_visible %}
+ {% block captcha_display %}
+ {% if title is not empty %}
+