From 2af7c07f7581de9e1f94dbeaf2e2623cf1832e6d Mon Sep 17 00:00:00 2001 From: Michael Lee <lee.5151@osu.edu> Date: Thu, 10 Feb 2022 15:14:56 -0500 Subject: [PATCH] Upgrading drupal/captcha (1.1.0 => 1.2.0) --- composer.lock | 14 +- vendor/composer/autoload_classmap.php | 2 - vendor/composer/autoload_static.php | 2 - vendor/composer/installed.json | 16 +- vendor/composer/installed.php | 10 +- web/modules/captcha/README.md | 6 + web/modules/captcha/captcha.inc | 14 + web/modules/captcha/captcha.info.yml | 6 +- web/modules/captcha/captcha.module | 170 ++++++++---- .../config/schema/captcha.settings.yml | 3 + .../image_captcha/image_captcha.info.yml | 8 +- .../image_captcha/image_captcha.install | 4 +- .../image_captcha/image_captcha.libraries.yml | 3 + .../image_captcha.links.menu.yml | 6 + .../image_captcha.links.task.yml | 5 + .../image_captcha/image_captcha.module | 56 ++++ .../image_captcha/image_captcha.routing.yml | 2 +- .../image_captcha/image_captcha_refresh.css | 38 +++ .../image_captcha/js/image_captcha_refresh.js | 3 + .../CaptchaFontPreviewController.php | 59 ++++ .../CaptchaImageGeneratorController.php | 26 +- .../src/Form/ImageCaptchaSettingsForm.php | 72 ++--- .../src/Response/CaptchaImageResponse.php | 31 ++- .../CaptchaFontPreviewStreamedResponse.php} | 53 ++-- .../captcha/migrations/d7_captcha_points.yml | 18 ++ .../migrations/d7_captcha_settings.yml | 36 +++ .../captcha_long_form_id_test.info.yml | 6 +- .../captcha_test/captcha_test.info.yml | 11 + .../modules/captcha_test/captcha_test.module | 45 +++ web/modules/captcha/src/Element/Captcha.php | 2 +- .../captcha/src/Form/CaptchaSettingsForm.php | 13 +- .../migrate/process/CaptchaTypeFormatter.php | 35 +++ .../Plugin/migrate/source/CaptchaPoints.php | 68 +++++ .../captcha/tests/fixtures/drupal7.php | 256 ++++++++++++++++++ .../tests/src/Functional/CaptchaAdminTest.php | 133 +++++---- .../tests/src/Functional/CaptchaCacheTest.php | 74 ++++- .../tests/src/Functional/CaptchaCronTest.php | 2 +- .../src/Functional/CaptchaPersistenceTest.php | 17 +- .../CaptchaSessionReuseAttackTestCase.php | 35 +-- .../tests/src/Functional/CaptchaTest.php | 72 ++++- .../src/Functional/CaptchaWebTestBase.php | 14 +- .../Migrate/d7/MigrateCaptchaPointsTest.php | 94 +++++++ .../MigrateCaptchaSimpleConfigurationTest.php | 69 +++++ 43 files changed, 1321 insertions(+), 288 deletions(-) create mode 100644 web/modules/captcha/image_captcha/image_captcha.links.menu.yml create mode 100644 web/modules/captcha/image_captcha/image_captcha.links.task.yml create mode 100644 web/modules/captcha/image_captcha/image_captcha_refresh.css create mode 100644 web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreviewController.php rename web/modules/captcha/image_captcha/src/{Controller/CaptchaFontPreview.php => StreamedResponse/CaptchaFontPreviewStreamedResponse.php} (60%) create mode 100644 web/modules/captcha/migrations/d7_captcha_points.yml create mode 100644 web/modules/captcha/migrations/d7_captcha_settings.yml create mode 100644 web/modules/captcha/modules/captcha_test/captcha_test.info.yml create mode 100644 web/modules/captcha/modules/captcha_test/captcha_test.module create mode 100644 web/modules/captcha/src/Plugin/migrate/process/CaptchaTypeFormatter.php create mode 100644 web/modules/captcha/src/Plugin/migrate/source/CaptchaPoints.php create mode 100644 web/modules/captcha/tests/fixtures/drupal7.php create mode 100644 web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaPointsTest.php create mode 100644 web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaSimpleConfigurationTest.php diff --git a/composer.lock b/composer.lock index dd6dcc6451..d9e5d40ed0 100644 --- a/composer.lock +++ b/composer.lock @@ -2280,17 +2280,17 @@ }, { "name": "drupal/captcha", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/captcha.git", - "reference": "8.x-1.1" + "reference": "8.x-1.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/captcha-8.x-1.1.zip", - "reference": "8.x-1.1", - "shasum": "2eaf6f00ea256652c09f785b26dc933cc5b3dd26" + "url": "https://ftp.drupal.org/files/projects/captcha-8.x-1.2.zip", + "reference": "8.x-1.2", + "shasum": "e35a2ce42b652f833d140f7571d1eef0e06b0edc" }, "require": { "drupal/core": "^8.8 || ^9" @@ -2298,8 +2298,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.1", - "datestamp": "1591160977", + "version": "8.x-1.2", + "datestamp": "1619673374", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 8bdfb0c588..95ebedfe9f 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -2278,8 +2278,6 @@ 'Drupal\\Core\\Language\\LanguageManager' => $baseDir . '/web/core/lib/Drupal/Core/Language/LanguageManager.php', 'Drupal\\Core\\Language\\LanguageManagerInterface' => $baseDir . '/web/core/lib/Drupal/Core/Language/LanguageManagerInterface.php', 'Drupal\\Core\\Layout\\Annotation\\Layout' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Annotation/Layout.php', - 'Drupal\\Core\\Layout\\Icon\\IconBuilderInterface' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php', - 'Drupal\\Core\\Layout\\Icon\\SvgIconBuilder' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Icon/SvgIconBuilder.php', 'Drupal\\Core\\Layout\\LayoutDefault' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutDefault.php', 'Drupal\\Core\\Layout\\LayoutDefinition' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutDefinition.php', 'Drupal\\Core\\Layout\\LayoutInterface' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutInterface.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 3ff8bd2803..13f2b32f3a 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -3064,8 +3064,6 @@ class ComposerStaticInit5c689ffcd54b9e495ed983fdce09b530 'Drupal\\Core\\Language\\LanguageManager' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Language/LanguageManager.php', 'Drupal\\Core\\Language\\LanguageManagerInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Language/LanguageManagerInterface.php', 'Drupal\\Core\\Layout\\Annotation\\Layout' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Annotation/Layout.php', - 'Drupal\\Core\\Layout\\Icon\\IconBuilderInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php', - 'Drupal\\Core\\Layout\\Icon\\SvgIconBuilder' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Icon/SvgIconBuilder.php', 'Drupal\\Core\\Layout\\LayoutDefault' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutDefault.php', 'Drupal\\Core\\Layout\\LayoutDefinition' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutDefinition.php', 'Drupal\\Core\\Layout\\LayoutInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutInterface.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 24aec2af9b..359dd1abc7 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2350,18 +2350,18 @@ }, { "name": "drupal/captcha", - "version": "1.1.0", - "version_normalized": "1.1.0.0", + "version": "1.2.0", + "version_normalized": "1.2.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/captcha.git", - "reference": "8.x-1.1" + "reference": "8.x-1.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/captcha-8.x-1.1.zip", - "reference": "8.x-1.1", - "shasum": "2eaf6f00ea256652c09f785b26dc933cc5b3dd26" + "url": "https://ftp.drupal.org/files/projects/captcha-8.x-1.2.zip", + "reference": "8.x-1.2", + "shasum": "e35a2ce42b652f833d140f7571d1eef0e06b0edc" }, "require": { "drupal/core": "^8.8 || ^9" @@ -2369,8 +2369,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.1", - "datestamp": "1591160977", + "version": "8.x-1.2", + "datestamp": "1619673374", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 3b1cc54827..168c2ae453 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '017d554d05ee95263629d62bfe1ad0b3dcb57e47', + 'reference' => '9338a6f25615021ab7915cd9aa7304c374d1a068', 'name' => 'osu-asc-webservices/d8-upstream', 'dev' => true, ), @@ -434,12 +434,12 @@ 'dev_requirement' => false, ), 'drupal/captcha' => array( - 'pretty_version' => '1.1.0', - 'version' => '1.1.0.0', + 'pretty_version' => '1.2.0', + 'version' => '1.2.0.0', 'type' => 'drupal-module', 'install_path' => __DIR__ . '/../../web/modules/captcha', 'aliases' => array(), - 'reference' => '8.x-1.1', + 'reference' => '8.x-1.2', 'dev_requirement' => false, ), 'drupal/ckeditor' => array( @@ -2101,7 +2101,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '017d554d05ee95263629d62bfe1ad0b3dcb57e47', + 'reference' => '9338a6f25615021ab7915cd9aa7304c374d1a068', 'dev_requirement' => false, ), 'pantheon-systems/quicksilver-pushback' => array( diff --git a/web/modules/captcha/README.md b/web/modules/captcha/README.md index 61f7cf8301..771e737a4b 100755 --- a/web/modules/captcha/README.md +++ b/web/modules/captcha/README.md @@ -83,3 +83,9 @@ MAINTAINERS Supporting organizations: * Chuva Inc. - https://www.drupal.org/chuva-inc + +DEVELOPMENT +------------- + You can disable captcha in your local or test environment by adding the + following line to settings.php: + $settings['disable_captcha'] = TRUE; diff --git a/web/modules/captcha/captcha.inc b/web/modules/captcha/captcha.inc index b1fa2379aa..f390faadd1 100755 --- a/web/modules/captcha/captcha.inc +++ b/web/modules/captcha/captcha.inc @@ -178,6 +178,20 @@ function _captcha_get_description() { 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. * diff --git a/web/modules/captcha/captcha.info.yml b/web/modules/captcha/captcha.info.yml index 5fd7cf593a..d197d084a6 100644 --- a/web/modules/captcha/captcha.info.yml +++ b/web/modules/captcha/captcha.info.yml @@ -8,7 +8,7 @@ configure: captcha_settings dependencies: - drupal:node -# Information added by Drupal.org packaging script on 2020-06-03 -version: '8.x-1.1' +# Information added by Drupal.org packaging script on 2021-04-29 +version: '8.x-1.2' project: 'captcha' -datestamp: 1591160979 +datestamp: 1619673377 diff --git a/web/modules/captcha/captcha.module b/web/modules/captcha/captcha.module index 8174a92362..0b298252c8 100755 --- a/web/modules/captcha/captcha.module +++ b/web/modules/captcha/captcha.module @@ -19,6 +19,7 @@ use Drupal\Core\Render\Markup; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; +use Drupal\Core\Site\Settings; /** * Constants for CAPTCHA persistence. @@ -288,6 +289,22 @@ function captcha_form_alter(array &$form, FormStateInterface $form_state, $form_ '#markup' => '<div class="messages messages--warning">' . 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.') . '</div>', ]; } + + // 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; + } + } + } /** @@ -465,71 +482,34 @@ function captcha_validate($element, FormStateInterface &$form_state) { // TODO: is this correct in all cases: see comments in previous revisions? $csid = $captcha_info['captcha_sid']; - $solution = \Drupal::database() - ->select('captcha_sessions', 'cs') - ->fields('cs', ['solution']) - ->condition('csid', $csid) - ->execute() - ->fetchField(); - // Bypass captcha validation if access attribute value is false. if (empty($captcha_info['access'])) { return FALSE; } - // @todo: what is the result when there is no entry for - // the captcha_session? in D6 it was FALSE, what in D7? - if ($solution === FALSE) { - // Unknown challenge_id. - // TODO: this probably never happens anymore now that there is detection - // for CAPTCHA session reuse attacks in _captcha_get_posted_captcha_info(). - $form_state->setErrorByName('captcha', t('CAPTCHA validation error: unknown CAPTCHA session ID. Contact the site administrator if this problem persists.')); - \Drupal::logger('CAPTCHA')->error( - 'CAPTCHA validation error: unknown CAPTCHA session ID (%csid).', - ['%csid' => var_export($csid, TRUE)]); - } - else { - // Get CAPTCHA validate function or fall back on strict equality. + // 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)) { - $captcha_validate = 'captcha_validate_strict_equality'; + // 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)) { - - // Get the CAPTCHA persistence setting. - $captcha_persistence = \Drupal::config('captcha.settings') - ->get('persistence'); - - if (in_array($captcha_persistence, - [ - CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL, - 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' => CAPTCHA_STATUS_SOLVED]) - ->expression('attempts', 'attempts + 1') - ->execute(); - } - else { + if (!$captcha_validate($solution, $captcha_response, $element, $form_state)) { // Wrong answer. - \Drupal::database()->update('captcha_sessions') - ->condition('csid', $csid) - ->expression('attempts', 'attempts + 1') - ->execute(); - - $form_state->setErrorByName('captcha_response', t('The answer you entered for the CAPTCHA was not correct.')); + $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() @@ -540,17 +520,97 @@ function captcha_validate($element, FormStateInterface &$form_state) { ->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 post blocked by CAPTCHA module: challenge %challenge (by module %module).', [ '%form_id' => $form_id, - '@response' => $captcha_response, - '@solution' => $solution, '%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, + [ + CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL, + 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' => 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')->error( + 'CAPTCHA validation error: unknown CAPTCHA session ID (%csid).', + ['%csid' => var_export($csid, TRUE)]); + } + } } /** diff --git a/web/modules/captcha/config/schema/captcha.settings.yml b/web/modules/captcha/config/schema/captcha.settings.yml index b91cb03a54..483148a24f 100755 --- a/web/modules/captcha/config/schema/captcha.settings.yml +++ b/web/modules/captcha/config/schema/captcha.settings.yml @@ -23,6 +23,9 @@ captcha.settings: add_captcha_description: type: boolean label: 'Add a description to the CAPTCHA' + wrong_captcha_response_message: + type: label + label: 'The error message when a user has entered an incorrect CAPTCHA answer.' default_validation: type: integer label: 'Default CAPTCHA validation' diff --git a/web/modules/captcha/image_captcha/image_captcha.info.yml b/web/modules/captcha/image_captcha/image_captcha.info.yml index a1d5195a7f..859edd8942 100644 --- a/web/modules/captcha/image_captcha/image_captcha.info.yml +++ b/web/modules/captcha/image_captcha/image_captcha.info.yml @@ -4,10 +4,10 @@ description: Provides an image based CAPTCHA. package: Spam control core_version_requirement: ^8.8 || ^9 dependencies: - - captcha + - captcha:captcha configure: admin/config/people/captcha/image_captcha -# Information added by Drupal.org packaging script on 2020-06-03 -version: '8.x-1.1' +# Information added by Drupal.org packaging script on 2021-04-29 +version: '8.x-1.2' project: 'captcha' -datestamp: 1591160979 +datestamp: 1619673377 diff --git a/web/modules/captcha/image_captcha/image_captcha.install b/web/modules/captcha/image_captcha/image_captcha.install index 003f224697..913fbef329 100755 --- a/web/modules/captcha/image_captcha/image_captcha.install +++ b/web/modules/captcha/image_captcha/image_captcha.install @@ -38,7 +38,7 @@ function image_captcha_install() { $config = \Drupal::configFactory()->getEditable('image_captcha.settings'); $config->set('image_captcha_fonts', [ - drupal_get_path('module', 'image_captcha') . '/fonts/Tesox/tesox.ttf', - drupal_get_path('module', 'image_captcha') . '/fonts/Tuffy/Tuffy.ttf', + hash('sha256', drupal_get_path('module', 'image_captcha') . '/fonts/Tesox/tesox.ttf'), + hash('sha256', drupal_get_path('module', 'image_captcha') . '/fonts/Tuffy/Tuffy.ttf'), ])->save(TRUE); } diff --git a/web/modules/captcha/image_captcha/image_captcha.libraries.yml b/web/modules/captcha/image_captcha/image_captcha.libraries.yml index 4f37b86f0d..b9b8de2bc8 100755 --- a/web/modules/captcha/image_captcha/image_captcha.libraries.yml +++ b/web/modules/captcha/image_captcha/image_captcha.libraries.yml @@ -15,3 +15,6 @@ image-captcha-refresh: version: 1.0 js: js/image_captcha_refresh.js: {} + css: + theme: + image_captcha_refresh.css: {} diff --git a/web/modules/captcha/image_captcha/image_captcha.links.menu.yml b/web/modules/captcha/image_captcha/image_captcha.links.menu.yml new file mode 100644 index 0000000000..b2f3731dcc --- /dev/null +++ b/web/modules/captcha/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/captcha/image_captcha/image_captcha.links.task.yml b/web/modules/captcha/image_captcha/image_captcha.links.task.yml new file mode 100644 index 0000000000..0e18abcbf4 --- /dev/null +++ b/web/modules/captcha/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/captcha/image_captcha/image_captcha.module b/web/modules/captcha/image_captcha/image_captcha.module index 8832ac80b6..7ad3c1ad26 100755 --- a/web/modules/captcha/image_captcha/image_captcha.module +++ b/web/modules/captcha/image_captcha/image_captcha.module @@ -8,6 +8,7 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\Core\Link; +use Drupal\Core\DrupalKernel; define('IMAGE_CAPTCHA_ALLOWED_CHARACTERS', 'aAbBCdEeFfGHhijKLMmNPQRrSTtWXYZ23456789'); @@ -47,6 +48,61 @@ function _image_captcha_get_enabled_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_get_path('module', 'image_captcha') . '/fonts', + 'sites/all/libraries/fonts', + DrupalKernel::findSitePath($request) . '/libraries/fonts', + ]; + } + // Collect the font information. + $fonts = []; + foreach ($directories as $directory) { + if (\Drupal::service('file_system')->prepareDirectory($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. * diff --git a/web/modules/captcha/image_captcha/image_captcha.routing.yml b/web/modules/captcha/image_captcha/image_captcha.routing.yml index bd6c3ad866..e3d92a502e 100755 --- a/web/modules/captcha/image_captcha/image_captcha.routing.yml +++ b/web/modules/captcha/image_captcha/image_captcha.routing.yml @@ -8,7 +8,7 @@ image_captcha.settings: image_captcha.font_preview: path: '/admin/config/people/captcha/image_captcha/font_preview/{token}' defaults: - _controller: '\Drupal\image_captcha\Controller\CaptchaFontPreview::content' + _controller: '\Drupal\image_captcha\Controller\CaptchaFontPreviewController::getFont' requirements: _permission: 'administer CAPTCHA settings' diff --git a/web/modules/captcha/image_captcha/image_captcha_refresh.css b/web/modules/captcha/image_captcha/image_captcha_refresh.css new file mode 100644 index 0000000000..325b1f3f79 --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha_refresh.css @@ -0,0 +1,38 @@ +.image_captcha_refresh_loader { + border: 5px solid #f3f3f3; + border-radius: 50%; + border-top: 5px solid #000; + width: 30px; + height: 30px; + -webkit-animation: image_captcha_refresh_spin 1s linear infinite; + animation: image_captcha_refresh_spin 1s linear infinite; + position: absolute; + left: 0; + right: 0; + margin: auto; + top: 0; + bottom: 0; +} + +/* Safari */ +@-webkit-keyframes image_captcha_refresh_spin { + 0% { + -webkit-transform: rotate(0deg); +} + 100% { + -webkit-transform: rotate(360deg); +} +} + +@keyframes image_captcha_refresh_spin { + 0% { + transform: rotate(0deg); +} + 100% { + transform: rotate(360deg); +} +} + +.captcha { + position: relative; +} diff --git a/web/modules/captcha/image_captcha/js/image_captcha_refresh.js b/web/modules/captcha/image_captcha/js/image_captcha_refresh.js index 457c58e7d1..807fa510cc 100644 --- a/web/modules/captcha/image_captcha/js/image_captcha_refresh.js +++ b/web/modules/captcha/image_captcha/js/image_captcha_refresh.js @@ -24,6 +24,8 @@ var date = new Date(); var baseUrl = document.location.origin; var url = baseUrl + '/' + $(this).attr('href') + '?' + date.getTime(); + // Adding loader. + $('.captcha').append('<div class="image_captcha_refresh_loader"></div>'); $.get( url, {}, @@ -32,6 +34,7 @@ $('.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 .image_captcha_refresh_loader').remove(); } else { alert(response.message); diff --git a/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreviewController.php b/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreviewController.php new file mode 100644 index 0000000000..ed405fcff9 --- /dev/null +++ b/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreviewController.php @@ -0,0 +1,59 @@ +<?php + +namespace Drupal\image_captcha\Controller; + +use Drupal\Core\Config\ImmutableConfig; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\image_captcha\StreamedResponse\CaptchaFontPreviewStreamedResponse; +use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Controller which generates the image from defined settings. + */ +class CaptchaFontPreviewController implements ContainerInjectionInterface { + + /** + * Image Captcha config storage. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + protected $config; + + /** + * Kill Switch for page caching. + * + * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch + */ + protected $killSwitch; + + /** + * {@inheritdoc} + */ + public function __construct(ImmutableConfig $config, KillSwitch $kill_switch) { + $this->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 \Drupal\image_captcha\StreamedResponse\CaptchaFontPreviewStreamedResponse + * Make a CaptchaImageResponse with the correct configuration and return it. + */ + public function getFont($token) { + $this->killSwitch->trigger(); + return new CaptchaFontPreviewStreamedResponse($this->config, $token); + } + +} diff --git a/web/modules/captcha/image_captcha/src/Controller/CaptchaImageGeneratorController.php b/web/modules/captcha/image_captcha/src/Controller/CaptchaImageGeneratorController.php index 49bef40e60..241f2f4442 100644 --- a/web/modules/captcha/image_captcha/src/Controller/CaptchaImageGeneratorController.php +++ b/web/modules/captcha/image_captcha/src/Controller/CaptchaImageGeneratorController.php @@ -3,6 +3,8 @@ namespace Drupal\image_captcha\Controller; use Drupal\Core\Config\Config; +use Drupal\Core\Database\Connection; +use Drupal\Core\File\FileSystemInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Psr\Log\LoggerInterface; use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; @@ -14,6 +16,13 @@ */ class CaptchaImageGeneratorController implements ContainerInjectionInterface { + /** + * Connection container. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + /** * Image Captcha config storage. * @@ -21,6 +30,13 @@ class CaptchaImageGeneratorController implements ContainerInjectionInterface { */ protected $config; + /** + * File System Service. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + /** * Watchdog logger channel for captcha. * @@ -38,10 +54,12 @@ class CaptchaImageGeneratorController implements ContainerInjectionInterface { /** * {@inheritdoc} */ - public function __construct(Config $config, LoggerInterface $logger, KillSwitch $kill_switch) { + public function __construct(Config $config, LoggerInterface $logger, KillSwitch $kill_switch, Connection $connection, FileSystemInterface $file_system) { $this->config = $config; $this->logger = $logger; $this->killSwitch = $kill_switch; + $this->connection = $connection; + $this->fileSystem = $file_system; } /** @@ -51,7 +69,9 @@ 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('page_cache_kill_switch'), + $container->get('database'), + $container->get('file_system') ); } @@ -63,7 +83,7 @@ public static function create(ContainerInterface $container) { */ public function image() { $this->killSwitch->trigger(); - return new CaptchaImageResponse($this->config, $this->logger); + return new CaptchaImageResponse($this->config, $this->logger, $this->connection, $this->fileSystem); } } diff --git a/web/modules/captcha/image_captcha/src/Form/ImageCaptchaSettingsForm.php b/web/modules/captcha/image_captcha/src/Form/ImageCaptchaSettingsForm.php index 130ecf2aea..d56f0ce7cc 100755 --- a/web/modules/captcha/image_captcha/src/Form/ImageCaptchaSettingsForm.php +++ b/web/modules/captcha/image_captcha/src/Form/ImageCaptchaSettingsForm.php @@ -79,7 +79,7 @@ protected function getEditableConfigNames() { 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'][] = 'captcha_image/base'; + $form['#attached']['library'][] = 'image_captcha/base'; // First some error checking. $setup_status = _image_captcha_check_setup(FALSE); @@ -274,6 +274,10 @@ public function validateForm(array &$form, FormStateInterface $form_state) { } $readable_fonts = []; + $available_fonts = _image_captcha_get_font_uri(); + foreach ($fonts as $token) { + $fonts[$token] = $available_fonts[$token]; + } list($readable_fonts, $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)])); @@ -295,13 +299,21 @@ public function validateForm(array &$form, FormStateInterface $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. - $fonts = array_filter($form_state->getValue('image_captcha_fonts')); - $this->config('image_captcha.settings') - ->set('image_captcha_fonts', $fonts) - ->save(); + $image_captcha_fonts = $form_state->getValue('image_captcha_fonts'); + $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); } @@ -339,7 +351,7 @@ protected function settingsDotSection() { $available_fonts = []; // List of folders to search through for TrueType fonts. - $fonts = $this->getAvailableFontsFromDirectories(); + $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. @@ -358,20 +370,16 @@ protected function settingsDotSection() { 'title' => $title, 'alt' => $title, ]; - $available_fonts[$font['uri']] = '<img' . new Attribute($attributes) . ' />'; + $available_fonts[$token] = '<img' . new Attribute($attributes) . ' />'; } // Append the PHP built-in font at the end. $title = $this->t('Preview of built-in font'); - $attributes = [ - 'src' => Url::fromRoute('image_captcha.font_preview', ['token' => 'BUILTIN']) + $available_fonts['BUILTIN'] = $this->t('PHP built-in font: <img src="@font_preview_url" alt="@title" title="@title"', [ + '@font_preview_url' => Url::fromRoute('image_captcha.font_preview', ['token' => 'BUILTIN']) ->toString(), - 'alt' => $title, - 'title' => $title, - ]; - $available_fonts['BUILTIN'] = (string) $this->t('PHP built-in font: font_preview', [ - 'font_preview' => '<img' . new Attribute($attributes) . ' />', - ]); + '@title' => $title, + ])->__toString(); $default_fonts = _image_captcha_get_enabled_fonts(); $conf_path = DrupalKernel::findSitePath($this->getRequest()); @@ -426,38 +434,4 @@ protected function settingsDotSection() { return $form; } - /** - * 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). - */ - protected function getAvailableFontsFromDirectories($directories = NULL) { - // If no fonts directories are given: use the default. - if ($directories === NULL) { - $directories = [ - drupal_get_path('module', 'image_captcha') . '/fonts', - 'sites/all/libraries/fonts', - DrupalKernel::findSitePath($this->getRequest()) . '/libraries/fonts', - ]; - } - // Collect the font information. - $fonts = []; - foreach ($directories as $directory) { - foreach ($this->fileSystem->scanDirectory($directory, '/\.[tT][tT][fF]$/') as $filename => $font) { - $fonts[hash('sha256', $filename)] = $font; - } - } - - return $fonts; - } - } diff --git a/web/modules/captcha/image_captcha/src/Response/CaptchaImageResponse.php b/web/modules/captcha/image_captcha/src/Response/CaptchaImageResponse.php index 6f44106982..8668954dc0 100755 --- a/web/modules/captcha/image_captcha/src/Response/CaptchaImageResponse.php +++ b/web/modules/captcha/image_captcha/src/Response/CaptchaImageResponse.php @@ -3,6 +3,8 @@ namespace Drupal\image_captcha\Response; use Drupal\Core\Config\Config; +use Drupal\Core\Database\Connection; +use Drupal\Core\File\FileSystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -16,6 +18,13 @@ class CaptchaImageResponse extends Response { const LOG_LEVEL = 'ERROR'; + /** + * Database connection configuration container. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + /** * Image Captcha config storage. * @@ -23,6 +32,13 @@ class CaptchaImageResponse extends Response { */ protected $config; + /** + * File System container. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + /** * Watchdog logger channel for captcha. * @@ -40,11 +56,13 @@ class CaptchaImageResponse extends Response { /** * {@inheritdoc} */ - public function __construct(Config $config, LoggerInterface $logger, $callback = NULL, $status = 200, $headers = []) { + public function __construct(Config $config, LoggerInterface $logger, Connection $connection, FileSystemInterface $fileSystem, $callback = NULL, $status = 200, $headers = []) { parent::__construct(NULL, $status, $headers); $this->config = $config; $this->logger = $logger; + $this->connection = $connection; + $this->fileSystem = $fileSystem; } /** @@ -53,7 +71,7 @@ public function __construct(Config $config, LoggerInterface $logger, $callback = public function prepare(Request $request) { $session_id = $request->get('session_id'); - $code = \Drupal::database() + $code = $this->connection ->select('captcha_sessions', 'cs') ->fields('cs', ['solution']) ->condition('csid', $session_id) @@ -371,11 +389,12 @@ protected function printString(&$image, $width, $height, array $fonts, $font_siz // 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, \Drupal::service('file_system')->realpath($font), $character); + $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). @@ -383,7 +402,7 @@ protected function printString(&$image, $width, $height, array $fonts, $font_siz // can be used as workaround. // This issue is discussed at http://drupal.org/node/349218. if ($bbox[2] < 0) { - $bbox = imagettfbbox($font_size, 0, \Drupal::service('file_system')->realpath($font), $character); + $bbox = imagettfbbox($font_size, 0, $this->fileSystem->realpath($font), $character); } } else { @@ -402,7 +421,7 @@ protected function printString(&$image, $width, $height, array $fonts, $font_siz } // Random (but small) rotation of the character. - // TODO: add a setting for this? + // @todo add a setting for this? $angle = mt_rand(-10, 10); // Determine print position: at what coordinate should the character be @@ -439,7 +458,7 @@ protected function printString(&$image, $width, $height, array $fonts, $font_siz imagestring($image, 5, $pos_x, $pos_y, $character, $color); } else { - imagettftext($image, $font_size, $angle, $pos_x, $pos_y, $color, \Drupal::service('file_system')->realpath($font), $character); + imagettftext($image, $font_size, $angle, $pos_x, $pos_y, $color, $this->fileSystem->realpath($font), $character); } } diff --git a/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreview.php b/web/modules/captcha/image_captcha/src/StreamedResponse/CaptchaFontPreviewStreamedResponse.php similarity index 60% rename from web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreview.php rename to web/modules/captcha/image_captcha/src/StreamedResponse/CaptchaFontPreviewStreamedResponse.php index c843153f31..a5dbac3bdd 100755 --- a/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreview.php +++ b/web/modules/captcha/image_captcha/src/StreamedResponse/CaptchaFontPreviewStreamedResponse.php @@ -1,38 +1,58 @@ <?php -namespace Drupal\image_captcha\Controller; +namespace Drupal\image_captcha\StreamedResponse; -use Symfony\Component\HttpFoundation\Request; +use Drupal\Core\Config\ImmutableConfig; use Symfony\Component\HttpFoundation\StreamedResponse; /** * A Controller to preview the captcha font on the settings page. */ -class CaptchaFontPreview extends StreamedResponse { +class CaptchaFontPreviewStreamedResponse extends StreamedResponse { + + /** + * Config service. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + protected $config; + + /** + * Token font selector. + * + * @string + */ + protected $token; /** * {@inheritdoc} */ - public function content(Request $request) { - $token = $request->get('token'); + public function __construct(ImmutableConfig $config, $token, $callback = NULL, $status = 200, $headers = []) { + parent::__construct(NULL, $status, $headers); + + $this->config = $config; + $this->token = $token; + } + + /** + * {@inheritdoc} + */ + public function sendContent() { // Get the font from the given font token. - if ($token == 'BUILTIN') { + if ($this->token == 'BUILTIN') { $font = 'BUILTIN'; } else { // Get the mapping of font tokens to font file objects. - $fonts = \Drupal::config('image_captcha.settings') - ->get('image_captcha_fonts_preview_map_cache'); - if (!isset($fonts[$token])) { - echo 'bad token'; - exit(); + $fonts = $this->config->get('image_captcha_fonts_preview_map_cache'); + if (!isset($fonts[$this->token])) { + return 'bad token'; } // Get the font path. - $font = $fonts[$token]['uri']; + $font = $fonts[$this->token]['uri']; // Some sanity checks if the given font is valid. if (!is_file($font) || !is_readable($font)) { - echo 'bad font'; - exit(); + return 'bad font'; } } @@ -45,7 +65,7 @@ public function content(Request $request) { // Allocate image resource. $image = imagecreatetruecolor($width, $height); if (!$image) { - exit(); + return NULL; } // White background and black foreground. $background_color = imagecolorallocate($image, 255, 255, 255); @@ -65,9 +85,6 @@ public function content(Request $request) { imagepng($image); // Release image memory. imagedestroy($image); - - // Close connection. - exit(); } } diff --git a/web/modules/captcha/migrations/d7_captcha_points.yml b/web/modules/captcha/migrations/d7_captcha_points.yml new file mode 100644 index 0000000000..74d1cffdcb --- /dev/null +++ b/web/modules/captcha/migrations/d7_captcha_points.yml @@ -0,0 +1,18 @@ +id: d7_captcha_points +label: 'Captcha Points Table' +migration_tags: + - Drupal 7 + - Configuration +source: + plugin: d7_captcha_points +process: + formId: form_id + label: form_id + captchaType: + plugin: captcha_type_formatter + source: captcha_type + status: + plugin: default_value + default_value: TRUE +destination: + plugin: entity:captcha_point \ No newline at end of file diff --git a/web/modules/captcha/migrations/d7_captcha_settings.yml b/web/modules/captcha/migrations/d7_captcha_settings.yml new file mode 100644 index 0000000000..5e37885928 --- /dev/null +++ b/web/modules/captcha/migrations/d7_captcha_settings.yml @@ -0,0 +1,36 @@ +id: d7_captcha_settings +label: 'Captcha Settings' +migration_tags: + - Drupal 7 + - Configuration +source: + plugin: variable + variables: + - captcha_add_captcha_description + - captcha_administration_mode + - captcha_allow_on_admin_pages + - captcha_default_challenge + - captcha_default_challenge_on_nonlisted_forms + - captcha_default_validation + - captcha_description + - captcha_enable_stats + - captcha_error_message + - captcha_log_wrong_responses + - captcha_persistence + - captcha_placement_map_cache + source_module: + - captcha +process: + enabled_default: captcha_default_challenge_on_nonlisted_forms + default_challenge: captcha_default_challenge + description: captcha_description + administration_mode: captcha_administration_mode + allow_on_admin_pages: captcha_allow_on_admin_pages + add_captcha_description: captcha_add_captcha_description + default_validation: captcha_default_validation + persistence: captcha_persistence + enable_stats: captcha_enable_stats + log_wrong_responses: captcha_log_wrong_responses +destination: + plugin: config + config_name: captcha.settings diff --git a/web/modules/captcha/modules/captcha_long_form_id_test/captcha_long_form_id_test.info.yml b/web/modules/captcha/modules/captcha_long_form_id_test/captcha_long_form_id_test.info.yml index cf5b8d5668..6d6bce9f86 100644 --- a/web/modules/captcha/modules/captcha_long_form_id_test/captcha_long_form_id_test.info.yml +++ b/web/modules/captcha/modules/captcha_long_form_id_test/captcha_long_form_id_test.info.yml @@ -5,7 +5,7 @@ package: Testing core_version_requirement: ^8.8 || ^9 hidden: true -# Information added by Drupal.org packaging script on 2020-06-03 -version: '8.x-1.1' +# Information added by Drupal.org packaging script on 2021-04-29 +version: '8.x-1.2' project: 'captcha' -datestamp: 1591160979 +datestamp: 1619673377 diff --git a/web/modules/captcha/modules/captcha_test/captcha_test.info.yml b/web/modules/captcha/modules/captcha_test/captcha_test.info.yml new file mode 100644 index 0000000000..df735cfe5e --- /dev/null +++ b/web/modules/captcha/modules/captcha_test/captcha_test.info.yml @@ -0,0 +1,11 @@ +name: 'Captcha Test' +type: module +description: 'Provides captcha types for tests.' +package: Testing +core_version_requirement: ^8.8 || ^9 +hidden: true + +# Information added by Drupal.org packaging script on 2021-04-29 +version: '8.x-1.2' +project: 'captcha' +datestamp: 1619673377 diff --git a/web/modules/captcha/modules/captcha_test/captcha_test.module b/web/modules/captcha/modules/captcha_test/captcha_test.module new file mode 100644 index 0000000000..97d976847e --- /dev/null +++ b/web/modules/captcha/modules/captcha_test/captcha_test.module @@ -0,0 +1,45 @@ +<?php + +/** + * @file + * Contains hook implementations for the Captcha Test module. + */ + +/** + * Implements hook_captcha(). + */ +function captcha_test_captcha($op, $captcha_type = '') { + switch ($op) { + case 'list': + return []; + + case 'generate': + if ($captcha_type === 'TestCacheable') { + // A cacheable Captcha type. + $result = [ + 'cacheable' => TRUE, + // Cacheable captcha types need to provide a custom validation + // callback that doesn't care about the solution, because a form can + // be shown containing a cached CSID that has since been deleted + // from the {captcha_sessions} table. + 'captcha_validate' => 'captcha_test_captcha_captcha_validation', + 'solution' => 'Test 123', + 'form' => [], + ]; + $result['form']['captcha_response'] = [ + '#type' => 'textfield', + '#title' => t('Test one two three'), + '#required' => TRUE, + ]; + + return $result; + } + } +} + +/** + * Validation callback. + */ +function captcha_test_captcha_captcha_validation() { + return TRUE; +} diff --git a/web/modules/captcha/src/Element/Captcha.php b/web/modules/captcha/src/Element/Captcha.php index 51c5dcd769..9fd2c04b01 100644 --- a/web/modules/captcha/src/Element/Captcha.php +++ b/web/modules/captcha/src/Element/Captcha.php @@ -101,7 +101,7 @@ public static function processCaptchaElement(&$element, FormStateInterface $form // 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']) + preg_replace("/[^a-z0-9_-]/", "", (string) $complete_form['form_id']['#value']) : NULL; // Get the CAPTCHA session ID. diff --git a/web/modules/captcha/src/Form/CaptchaSettingsForm.php b/web/modules/captcha/src/Form/CaptchaSettingsForm.php index 36072f148e..32db4377a0 100755 --- a/web/modules/captcha/src/Form/CaptchaSettingsForm.php +++ b/web/modules/captcha/src/Form/CaptchaSettingsForm.php @@ -164,6 +164,16 @@ public function buildForm(array $form, FormStateInterface $form_state) { ], ]; + // 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', @@ -241,6 +251,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Save (or reset) the CAPTCHA descriptions. $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')); @@ -256,7 +267,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * * @param array $form * Form structured array. - * @param Drupal\Core\Form\FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state structured array. */ public function clearCaptchaPlacementCacheSubmit(array $form, FormStateInterface $form_state) { diff --git a/web/modules/captcha/src/Plugin/migrate/process/CaptchaTypeFormatter.php b/web/modules/captcha/src/Plugin/migrate/process/CaptchaTypeFormatter.php new file mode 100644 index 0000000000..d83eb5c811 --- /dev/null +++ b/web/modules/captcha/src/Plugin/migrate/process/CaptchaTypeFormatter.php @@ -0,0 +1,35 @@ +<?php + +namespace Drupal\captcha\Plugin\migrate\process; + +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Perform captcha type transformation. + * + * @MigrateProcessPlugin( + * id = "captcha_type_formatter" + * ) + * + * To do custom value transformations use the following: + * + * @code + * field_text: + * plugin: captcha_type_formatter + * source: text + * @endcode + */ +class CaptchaTypeFormatter extends ProcessPluginBase { + + /** + * Transforms the d7 separate captcha type and module into one row. + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $module = $row->getSourceProperty('module') ?? 'captcha'; + $type = $row->getSourceProperty('captcha_type'); + return $module . '/' . $type; + } + +} diff --git a/web/modules/captcha/src/Plugin/migrate/source/CaptchaPoints.php b/web/modules/captcha/src/Plugin/migrate/source/CaptchaPoints.php new file mode 100644 index 0000000000..18df2195fb --- /dev/null +++ b/web/modules/captcha/src/Plugin/migrate/source/CaptchaPoints.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\captcha\Plugin\migrate\source; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\State\StateInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Drupal 7 captcha point source from database. + * + * @MigrateSource( + * id = "d7_captcha_points", + * source_module = "captcha" + * ) + */ +class CaptchaPoints extends DrupalSqlBase { + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->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/captcha/tests/fixtures/drupal7.php b/web/modules/captcha/tests/fixtures/drupal7.php new file mode 100644 index 0000000000..8a78dcf1c5 --- /dev/null +++ b/web/modules/captcha/tests/fixtures/drupal7.php @@ -0,0 +1,256 @@ +<?php + +/** + * @file + * A database agnostic dump for testing purposes. + */ + +use Drupal\Core\Database\Database; + +$connection = Database::getConnection(); + +$connection->insert('variable') + ->fields([ + 'name', + 'value', + ]) + ->values([ + 'name' => 'captcha_add_captcha_description', + 'value' => 'i:1;', + ]) + ->values([ + 'name' => 'captcha_administration_mode', + 'value' => 'i:1;', + ]) + ->values([ + 'name' => 'captcha_allow_on_admin_pages', + 'value' => 'i:0;', + ]) + ->values([ + 'name' => 'captcha_default_challenge', + 'value' => 's:12:"captcha/Math";', + ]) + ->values([ + 'name' => 'captcha_default_challenge_on_nonlisted_forms', + 'value' => 'i:1;', + ]) + ->values([ + 'name' => 'captcha_default_validation', + 'value' => 's:1:"1";', + ]) + ->values([ + 'name' => 'captcha_description', + 'value' => 's:110:"This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.";', + ]) + ->values([ + 'name' => 'captcha_enable_stats', + 'value' => 'i:1;', + ]) + ->values([ + 'name' => 'captcha_error_message', + 'value' => 's:55:"The answer you entered for the CAPTCHA was not correct.";', + ]) + ->values([ + 'name' => 'captcha_log_wrong_responses', + 'value' => 'i:1;', + ]) + ->values([ + 'name' => 'captcha_persistence', + 'value' => 's:1:"1";', + ]) + ->execute(); + +$connection->insert('system') + ->fields([ + 'filename', + 'name', + 'type', + 'owner', + 'status', + 'bootstrap', + 'schema_version', + 'weight', + 'info', + ]) + ->values([ + 'filename' => 'sites/all/modules/captcha/captcha.module', + 'name' => 'captcha', + 'type' => 'module', + 'owner' => '', + 'status' => '1', + 'bootstrap' => '0', + 'schema_version' => '7001', + 'weight' => '0', + 'info' => 'a:13:{s:4:\"name\";s:7:\"CAPTCHA\";s:11:\"description\";s:61:\"Base CAPTCHA module for adding challenges to arbitrary forms.\";s:7:\"package\";s:12:\"Spam control\";s:4:\"core\";s:3:\"7.x\";s:9:\"configure\";s:27:\"admin/config/people/captcha\";s:5:\"files\";a:5:{i:0;s:14:\"captcha.module\";i:1;s:11:\"captcha.inc\";i:2;s:17:\"captcha.admin.inc\";i:3;s:15:\"captcha.install\";i:4;s:12:\"captcha.test\";}s:7:\"version\";s:7:\"7.x-1.7\";s:7:\"project\";s:7:\"captcha\";s:9:\"datestamp\";s:10:\"1582293280\";s:5:\"mtime\";i:1582293280;s:12:\"dependencies\";a:0:{}s:3:\"php\";s:5:\"5.2.4\";s:9:\"bootstrap\";i:0;}', + ]) + ->execute(); + +// Create the Captcha Points D7 Table. +$connection->schema()->createTable('captcha_points', [ + 'fields' => [ + 'form_id' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 128, + 'default' => '', + ], + 'module' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 64, + 'default' => '', + ], + 'captcha_type' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 64, + 'default' => '', + ], + ], + 'primary key' => [ + 'form_id', + ], + 'mysql_character_set' => 'utf8', +]); + +$connection->insert('captcha_points') + ->fields([ + 'form_id', + 'module', + 'captcha_type', + ]) + ->values([ + 'form_id' => 'comment_node_article_form', + 'module' => 'captcha', + 'captcha_type' => 'Math', + ]) + ->values([ + 'form_id' => 'user_pass', + 'module' => 'captcha', + 'captcha_type' => 'Math', + ]) + ->execute(); + +// Create the Captcha Points D7 Table. +$connection->schema()->createTable('captcha_sessions', [ + 'fields' => [ + 'csid' => [ + 'type' => 'serial', + 'not null' => TRUE, + 'size' => 'normal', + ], + 'token' => [ + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => 64, + ], + 'uid' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => 0, + ], + 'sid' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 128, + 'default' => '', + ], + 'ip_address' => [ + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => 128, + ], + 'timestamp' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => 0, + ], + 'form_id' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 128, + 'default' => '', + ], + 'solution' => [ + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 128, + 'default' => '', + ], + 'status' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => 0, + ], + 'attempts' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => 0, + ], + ], + 'primary key' => [ + 'csid', + ], + 'indexes' => [ + 'csid_ip' => [ + 'csid', + 'ip_address', + ], + ], + 'mysql_character_set' => 'utf8', +]); + +$connection->insert('captcha_sessions') + ->fields([ + 'csid', + 'token', + 'uid', + 'sid', + 'ip_address', + 'timestamp', + 'form_id', + 'solution', + 'status', + 'attempts', + ]) + ->values([ + 'csid' => 1, + 'token' => '69e2767a2c651a887764bb60ea04cd0a', + 'uid' => 0, + 'sid' => 'svBxnT_AK4YFTbiUdCN3g9lCEqhC66NEbxasNNvGRug', + 'ip_address' => '172.18.0.1', + 'timestamp' => 1617948210, + 'form_id' => 'user_login_block', + 'solution' => '11', + 'status' => 0, + 'attempts' => 0, + ]) + ->values([ + 'csid' => 2, + 'token' => '69e2767a2c651a887764bb60ea04cd0b', + 'uid' => 0, + 'sid' => 'avBxnT_AK4YFTbiUdCN3g9lCEqhC66NEbxasNNvGRug', + 'ip_address' => '172.18.0.1', + 'timestamp' => 1617948230, + 'form_id' => 'user_login_block', + 'solution' => '20', + 'status' => 0, + 'attempts' => 0, + ]) + ->values([ + 'csid' => 3, + 'token' => '69e2767a2c651a887764bb60ea04cd0c', + 'uid' => 0, + 'sid' => 'bvBxnT_AK4YFTbiUdCN3g9lCEqhC66NEbxasNNvGRug', + 'ip_address' => '172.18.0.1', + 'timestamp' => 1617948240, + 'form_id' => 'user_login_block', + 'solution' => '25', + 'status' => 0, + 'attempts' => 0, + ]) + ->execute(); diff --git a/web/modules/captcha/tests/src/Functional/CaptchaAdminTest.php b/web/modules/captcha/tests/src/Functional/CaptchaAdminTest.php index 8f519d2ff5..3d9afe9ee5 100755 --- a/web/modules/captcha/tests/src/Functional/CaptchaAdminTest.php +++ b/web/modules/captcha/tests/src/Functional/CaptchaAdminTest.php @@ -4,6 +4,7 @@ use Drupal\captcha\Entity\CaptchaPoint; use Drupal\Core\Url; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Tests CAPTCHA admin settings. @@ -12,17 +13,19 @@ */ class CaptchaAdminTest extends CaptchaWebTestBase { + use StringTranslationTrait; + /** * Test access to the admin pages. */ public function testAdminAccess() { $this->drupalLogin($this->normalUser); $this->drupalGet(self::CAPTCHA_ADMIN_PATH); - $this->assertText(t('Access denied'), 'Normal users should not be able to access the CAPTCHA admin pages', 'CAPTCHA'); + $this->assertSession()->pageTextContains($this->t('Access denied'), 'Normal users should not be able to access the CAPTCHA admin pages', 'CAPTCHA'); $this->drupalLogin($this->adminUser); $this->drupalGet(self::CAPTCHA_ADMIN_PATH); - $this->assertNoText(t('Access denied'), 'Admin users should be able to access the CAPTCHA admin pages', 'CAPTCHA'); + $this->assertSession()->pageTextNotContains($this->t('Access denied'), 'Admin users should be able to access the CAPTCHA admin pages', 'CAPTCHA'); } /** @@ -34,10 +37,10 @@ public function testCaptchaPointSettingGetterAndSetter() { /* @var CaptchaPoint $result */ $result = captcha_get_form_id_setting($comment_form_id); $this->assertNotNull($result, 'CAPTCHA exists', 'CAPTCHA'); - $this->assertEqual($result->getCaptchaType(), 'test', 'CAPTCHA type: default', 'CAPTCHA'); + $this->assertEquals($result->getCaptchaType(), 'test', 'CAPTCHA type: default'); $result = captcha_get_form_id_setting($comment_form_id, TRUE); $this->assertNotNull($result, 'CAPTCHA exists', 'CAPTCHA'); - $this->assertEqual($result, 'test', 'Setting and symbolic getting CAPTCHA point: "test"', 'CAPTCHA'); + $this->assertEquals($result, 'test', 'Setting and symbolic getting CAPTCHA point: "test"'); // Set to 'default'. captcha_set_form_id_setting($comment_form_id, 'default'); @@ -46,24 +49,24 @@ public function testCaptchaPointSettingGetterAndSetter() { ->save(); $result = captcha_get_form_id_setting($comment_form_id); $this->assertNotNull($result, 'CAPTCHA exists', 'CAPTCHA'); - $this->assertEqual($result->getCaptchaType(), 'foo/bar', 'Setting and getting CAPTCHA point: default', 'CAPTCHA'); + $this->assertEquals($result->getCaptchaType(), 'foo/bar', 'Setting and getting CAPTCHA point: default'); $result = captcha_get_form_id_setting($comment_form_id, TRUE); - $this->assertNotNull($result, 'Setting and symbolic getting CAPTCHA point: "default"', 'CAPTCHA'); - $this->assertEqual($result, 'foo/bar', 'Setting and symbolic getting CAPTCHA point: default', 'CAPTCHA'); + $this->assertNotNull($result, 'Setting and symbolic getting CAPTCHA point: "default"'); + $this->assertEquals($result, 'foo/bar', 'Setting and symbolic getting CAPTCHA point: default'); // Set to 'baz/boo'. captcha_set_form_id_setting($comment_form_id, 'baz/boo'); $result = captcha_get_form_id_setting($comment_form_id); $this->assertNotNull($result, 'CAPTCHA exists', 'CAPTCHA'); - $this->assertEqual($result->getCaptchaType(), 'baz/boo', 'Setting and getting CAPTCHA point: baz/boo', 'CAPTCHA'); + $this->assertEquals($result->getCaptchaType(), 'baz/boo', 'Setting and getting CAPTCHA point: baz/boo'); $result = captcha_get_form_id_setting($comment_form_id, TRUE); - $this->assertEqual($result, 'baz/boo', 'Setting and symbolic getting CAPTCHA point: "baz/boo"', 'CAPTCHA'); + $this->assertEquals($result, 'baz/boo', 'Setting and symbolic getting CAPTCHA point: "baz/boo"'); // Set to NULL (which should delete the CAPTCHA point setting entry). captcha_set_form_id_setting($comment_form_id, NULL); $result = captcha_get_form_id_setting($comment_form_id); $this->assertNotNull($result, 'CAPTCHA exists', 'CAPTCHA'); - $this->assertEqual($result->getCaptchaType(), 'foo/bar', 'Setting and getting CAPTCHA point: NULL', 'CAPTCHA'); + $this->assertEquals($result->getCaptchaType(), 'foo/bar', 'Setting and getting CAPTCHA point: NULL'); $result = captcha_get_form_id_setting($comment_form_id, TRUE); $this->assertNotNull($result, 'CAPTCHA exists', 'CAPTCHA'); @@ -75,9 +78,9 @@ public function testCaptchaPointSettingGetterAndSetter() { $this->assertNotNull($result, 'Setting and getting CAPTCHA point: baba/fofo', 'CAPTCHA'); // $this->assertEqual($result->module, 'baba', 'Setting and getting // CAPTCHA point: baba/fofo', 'CAPTCHA');. - $this->assertEqual($result->getCaptchaType(), 'baba/fofo', 'Setting and getting CAPTCHA point: baba/fofo', 'CAPTCHA'); + $this->assertEquals($result->getCaptchaType(), 'baba/fofo', 'Setting and getting CAPTCHA point: baba/fofo'); $result = captcha_get_form_id_setting($comment_form_id, TRUE); - $this->assertEqual($result, 'baba/fofo', 'Setting and symbolic getting CAPTCHA point: "baba/fofo"', 'CAPTCHA'); + $this->assertEquals($result, 'baba/fofo', 'Setting and symbolic getting CAPTCHA point: "baba/fofo"'); } /** @@ -91,13 +94,12 @@ public function testCaptchaPointSettingGetterAndSetter() { */ protected function assertCaptchaSetting($form_id, $challenge_type) { $result = captcha_get_form_id_setting(self::COMMENT_FORM_ID, TRUE); - $this->assertEqual($result, $challenge_type, - t('Check CAPTCHA setting for form: expected: @expected, received: @received.', + $this->assertEquals($result, $challenge_type, + $this->t('Check CAPTCHA setting for form: expected: @expected, received: @received.', [ '@expected' => var_export($challenge_type, TRUE), '@received' => var_export($result, TRUE), - ]), - 'CAPTCHA'); + ])); } /** @@ -110,8 +112,9 @@ public function testCaptchaAdminLinks() { $edit = [ 'administration_mode' => TRUE, ]; - - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH, $edit, t('Save configuration')); + + $this->drupalGet(self::CAPTCHA_ADMIN_PATH); + $this->submitForm($edit, $this->t('Save configuration')); // Create a node with comments enabled. $node = $this->drupalCreateNode(); @@ -120,7 +123,7 @@ public function testCaptchaAdminLinks() { $this->drupalGet('node/' . $node->id()); // Click the add new comment link. - $this->clickLink(t('Add new comment')); + $this->clickLink($this->t('Add new comment')); $add_comment_url = $this->getUrl(); // Remove fragment part from comment URL to avoid @@ -128,56 +131,59 @@ public function testCaptchaAdminLinks() { $add_comment_url = strtok($add_comment_url, "#"); // Click the CAPTCHA admin link to enable a challenge. - $this->clickLink(t('Place a CAPTCHA here for untrusted users.')); + $this->clickLink($this->t('Place a CAPTCHA here for untrusted users.')); // Enable Math CAPTCHA. $edit = ['captchaType' => 'captcha/Math']; - $this->drupalPostForm($this->getUrl(), $edit, t('Save')); + $this->drupalGet($this->getUrl()); + $this->submitForm($edit, $this->t('Save')); + // Check if returned to original comment form. - $this->assertUrl($add_comment_url, [], + $this->assertSession()->addressEquals($add_comment_url, [], 'After setting CAPTCHA with CAPTCHA admin links: should return to original form.', 'CAPTCHA'); // Check if CAPTCHA was successfully enabled // (on CAPTCHA admin links fieldset). - $this->assertText(t('CAPTCHA: challenge "@type" enabled', ['@type' => $edit['captchaType']]), + $this->assertSession()->pageTextContains($this->t('CAPTCHA: challenge "@type" enabled', ['@type' => $edit['captchaType']]), 'Enable a challenge through the CAPTCHA admin links', 'CAPTCHA'); // Check if CAPTCHA was successfully enabled (through API). $this->assertCaptchaSetting(self::COMMENT_FORM_ID, 'captcha/Math'); // Edit challenge type through CAPTCHA admin links. - $this->clickLink(t('change')); + $this->clickLink($this->t('change')); // Enable Math CAPTCHA. $edit = ['captchaType' => 'default']; - $this->drupalPostForm($this->getUrl(), $edit, t('Save')); + $this->drupalGet($this->getUrl()); + $this->submitForm($edit, 'Save'); // Check if returned to original comment form. - $this->assertEqual($add_comment_url, $this->getUrl(), - 'After editing challenge type CAPTCHA admin links: should return to original form.', 'CAPTCHA'); + $this->assertEquals($add_comment_url, $this->getUrl(), + 'After editing challenge type CAPTCHA admin links: should return to original form.'); // Check if CAPTCHA was successfully changed // (on CAPTCHA admin links fieldset). // This is actually the same as the previous setting because // the captcha/Math is the default for the default challenge. // TODO Make sure the edit is a real change. - $this->assertText(t('CAPTCHA: challenge "@type" enabled', ['@type' => $edit['captchaType']]), + $this->assertSession()->pageTextContains($this->t('CAPTCHA: challenge "@type" enabled', ['@type' => $edit['captchaType']]), 'Enable a challenge through the CAPTCHA admin links', 'CAPTCHA'); // Check if CAPTCHA was successfully edited (through API). $this->assertCaptchaSetting(self::COMMENT_FORM_ID, 'default'); // Disable challenge through CAPTCHA admin links. $this->drupalGet(Url::fromRoute('entity.captcha_point.disable', ['captcha_point' => self::COMMENT_FORM_ID])); - $this->drupalPostForm(NULL, [], t('Disable')); + $this->submitForm([], $this->t('Disable')); // Check if returned to captcha point list. global $base_url; - $this->assertEqual($base_url . '/admin/config/people/captcha/captcha-points', $this->getUrl(), - 'After disabling challenge in CAPTCHA admin: should return to captcha point list.', 'CAPTCHA'); + $this->assertEquals($base_url . '/admin/config/people/captcha/captcha-points', $this->getUrl(), + 'After disabling challenge in CAPTCHA admin: should return to captcha point list.'); // Check if CAPTCHA was successfully disabled // (on CAPTCHA admin links fieldset). - $this->assertRaw(t('Captcha point %form_id has been disabled.', ['%form_id' => self::COMMENT_FORM_ID]), + $this->assertSession()->responseContains($this->t('Captcha point %form_id has been disabled.', ['%form_id' => self::COMMENT_FORM_ID]), 'Disable challenge through the CAPTCHA admin links', 'CAPTCHA'); } @@ -196,7 +202,7 @@ public function testUntrustedUserPosting() { // Go to node page and click the "add comment" link. $this->drupalGet('node/' . $node->id()); - $this->clickLink(t('Add new comment')); + $this->clickLink($this->t('Add new comment')); $add_comment_url = $this->getUrl(); // Check if CAPTCHA is visible on form. @@ -204,8 +210,9 @@ public function testUntrustedUserPosting() { // Try to post a comment with wrong answer. $edit = $this->getCommentFormValues(); $edit['captcha_response'] = 'xx'; - $this->drupalPostForm($add_comment_url, $edit, t('Preview')); - $this->assertText(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, + $this->drupalGet($add_comment_url); + $this->submitForm($edit, $this->t('Preview')); + $this->assertSession()->pageTextContains(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, 'wrong CAPTCHA should block form submission.', 'CAPTCHA'); } @@ -220,12 +227,13 @@ public function testXssOnCaptchaDescription() { $this->drupalLogin($this->adminUser); $xss = '<script type="text/javascript">alert("xss")</script>'; $edit = ['description' => $xss]; - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH, $edit, 'Save configuration'); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH); + $this->submitForm($edit, $this->t('Save configuration')); // Visit user register form and check if JavaScript snippet is there. $this->drupalLogout(); $this->drupalGet('user/register'); - $this->assertNoRaw($xss, 'JavaScript should not be allowed in CAPTCHA description.', 'CAPTCHA'); + $this->assertSession()->responseNotContains($xss, 'JavaScript should not be allowed in CAPTCHA description.', 'CAPTCHA'); } /** @@ -242,7 +250,9 @@ public function testCaptchaPlacementCacheClearing() { $this->assertNotNull($placement_map, 'CAPTCHA placement cache should be set.'); // Clear the cache. $this->drupalLogin($this->adminUser); - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH, [], t('Clear the CAPTCHA placement cache')); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH); + $this->submitForm([], $this->t('Clear the CAPTCHA placement cache')); + // Check that the placement cache is unset. $placement_map = $this->container->get('cache.default') ->get('captcha_placement_map_cache'); @@ -286,23 +296,26 @@ public function testCaptchaPointAdministration() { 'formId' => $captcha_point_form_id, 'captchaType' => $captcha_point_module . '/' . $captcha_point_type, ]; - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add', $form_values, t('Save')); - $this->assertText(t('Form ID field is required.')); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add'); + $this->submitForm($form_values, 'Save'); + $this->assertSession()->pageTextContains($this->t('Form ID field is required.')); // Set CAPTCHA point through admin/user/captcha/captcha/captcha_point. $form_values['label'] = $label; - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add', $form_values, t('Save')); - $this->assertRaw(t('Captcha Point for %label form was created.', ['%label' => $captcha_point_form_id])); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add'); + $this->submitForm($form_values, $this->t('Save')); + $this->assertSession()->responseContains($this->t('Captcha Point for %label form was created.', ['%label' => $captcha_point_form_id])); // Check in database. /* @var CaptchaPoint result */ $result = $this->getCaptchaPointSettingFromDatabase($captcha_point_form_id); - $this->assertEqual($result->captchaType, $captcha_point_module . '/' . $captcha_point_type, + $this->assertEquals($result->captchaType, $captcha_point_module . '/' . $captcha_point_type, 'Enabled CAPTCHA point should have module and type set'); // Disable CAPTCHA point again. - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/disable', [], t('Disable')); - $this->assertRaw(t('Captcha point %label has been disabled.', ['%label' => $label]), 'Disabling of CAPTCHA point'); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/disable'); + $this->submitForm([], $this->t('Disable')); + $this->assertSession()->responseContains($this->t('Captcha point %label has been disabled.', ['%label' => $label]), 'Disabling of CAPTCHA point'); // Check in database. $result = $this->getCaptchaPointSettingFromDatabase($captcha_point_form_id); @@ -313,17 +326,19 @@ public function testCaptchaPointAdministration() { $form_values = [ 'captchaType' => $captcha_point_module . '/' . $captcha_point_type, ]; - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id, $form_values, t('Save')); - $this->assertRaw(t('Captcha Point for %form_id form was updated.', ['%form_id' => $captcha_point_form_id]), 'Saving of CAPTCHA point settings'); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id); + $this->submitForm($form_values, $this->t('Save')); + $this->assertSession()->responseContains($this->t('Captcha Point for %form_id form was updated.', ['%form_id' => $captcha_point_form_id]), 'Saving of CAPTCHA point settings'); // Check in database. $result = $this->getCaptchaPointSettingFromDatabase($captcha_point_form_id); - $this->assertEqual($result->captchaType, $captcha_point_module . '/' . $captcha_point_type, + $this->assertEquals($result->captchaType, $captcha_point_module . '/' . $captcha_point_type, 'Enabled CAPTCHA point should have module and type set'); // Delete CAPTCHA point. - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/delete', [], t('Delete')); - $this->assertRaw(t('Captcha point %label has been deleted.', ['%label' => $label]), + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/delete'); + $this->submitForm([], $this->t('Delete')); + $this->assertSession()->responseContains($this->t('Captcha point %label has been deleted.', ['%label' => $label]), 'Deleting of CAPTCHA point'); $result = $this->getCaptchaPointSettingFromDatabase($captcha_point_form_id); @@ -347,8 +362,9 @@ public function testCaptchaPointAdministrationByNonAdmin() { 'formId' => $captcha_point_form_id, 'captchaType' => $captcha_point_module . '/' . $captcha_point_type, ]; - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add', $form_values, 'Save'); - $this->assertRaw(t('Captcha Point for %form_id form was created.', ['%form_id' => $captcha_point_form_id])); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add'); + $this->submitForm($form_values, $this->t('Save')); + $this->assertSession()->responseContains($this->t('Captcha Point for %form_id form was created.', ['%form_id' => $captcha_point_form_id])); // Switch from admin to non-admin. $this->drupalLogin($this->normalUser); @@ -356,17 +372,17 @@ public function testCaptchaPointAdministrationByNonAdmin() { // Try to set CAPTCHA point // through admin/user/captcha/captcha/captcha_point. $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points'); - $this->assertText(t('You are not authorized to access this page.'), + $this->assertSession()->pageTextContains($this->t('You are not authorized to access this page.'), 'Non admin should not be able to set a CAPTCHA point'); // Try to disable the CAPTCHA point. $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/disable'); - $this->assertText(t('You are not authorized to access this page.'), + $this->assertSession()->pageTextContains($this->t('You are not authorized to access this page.'), 'Non admin should not be able to disable a CAPTCHA point'); // Try to delete the CAPTCHA point. $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/delete'); - $this->assertText(t('You are not authorized to access this page.'), + $this->assertSession()->pageTextContains($this->t('You are not authorized to access this page.'), 'Non admin should not be able to delete a CAPTCHA point'); // Switch from nonadmin to admin again. @@ -374,11 +390,12 @@ public function testCaptchaPointAdministrationByNonAdmin() { // Check if original CAPTCHA point still exists in database. $result = $this->getCaptchaPointSettingFromDatabase($captcha_point_form_id); - $this->assertEqual($result->captchaType, $captcha_point_module . '/' . $captcha_point_type, 'Enabled CAPTCHA point should have module and type set'); + $this->assertEquals($result->captchaType, $captcha_point_module . '/' . $captcha_point_type, 'Enabled CAPTCHA point should have module and type set'); // Delete captcha point. - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/delete', [], 'Delete'); - $this->assertRaw(t('Captcha point %label has been deleted.', ['%label' => $label]), 'Disabling of CAPTCHA point'); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/' . $captcha_point_form_id . '/delete'); + $this->submitForm([], 'Delete'); + $this->assertSession()->responseContains($this->t('Captcha point %label has been deleted.', ['%label' => $label]), 'Disabling of CAPTCHA point'); } } diff --git a/web/modules/captcha/tests/src/Functional/CaptchaCacheTest.php b/web/modules/captcha/tests/src/Functional/CaptchaCacheTest.php index 391856aac7..5e46398074 100644 --- a/web/modules/captcha/tests/src/Functional/CaptchaCacheTest.php +++ b/web/modules/captcha/tests/src/Functional/CaptchaCacheTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\captcha\Functional; +use Drupal\Core\Database\Database; + /** * Tests CAPTCHA caching on various pages. * @@ -14,12 +16,16 @@ class CaptchaCacheTest extends CaptchaWebTestBase { * * @var array */ - public static $modules = ['block', 'image_captcha']; + protected static $modules = [ + 'block', + 'image_captcha', + 'captcha_test', + ]; /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->drupalPlaceBlock('user_login_block', ['id' => 'login']); @@ -32,41 +38,85 @@ public function testCacheTags() { global $base_path; // Check caching without captcha as anonymous user. $this->drupalGet(''); - $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS'); + $this->assertEquals($this->getSession()->getResponseHeader('x-drupal-cache'), 'MISS'); $this->drupalGet(''); - $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'HIT'); + $this->assertEquals($this->getSession()->getResponseHeader('x-drupal-cache'), 'HIT'); // Enable captcha on login block and test caching. captcha_set_form_id_setting('user_login_form', 'captcha/Math'); $this->drupalGet(''); $sid = $this->getCaptchaSidFromForm(); - $this->assertNull($this->drupalGetHeader('x-drupal-cache'), 'Cache is disabled'); + $this->assertNull($this->getSession()->getResponseHeader('x-drupal-cache'), 'Cache is disabled'); $this->drupalGet(''); - $this->assertNotEqual($sid, $this->getCaptchaSidFromForm()); + $this->assertNotEquals($sid, $this->getCaptchaSidFromForm()); // Switch challenge to captcha/Test, check the captcha isn't cached. captcha_set_form_id_setting('user_login_form', 'captcha/Test'); $this->drupalGet(''); $sid = $this->getCaptchaSidFromForm(); - $this->assertNull($this->drupalGetHeader('x-drupal-cache'), 'Cache is disabled'); + $this->assertNull($this->getSession()->getResponseHeader('x-drupal-cache'), 'Cache is disabled'); $this->drupalGet(''); - $this->assertNotEqual($sid, $this->getCaptchaSidFromForm()); + $this->assertNotEquals($sid, $this->getCaptchaSidFromForm()); // Switch challenge to image_captcha/Image, check the captcha isn't cached. captcha_set_form_id_setting('user_login_form', 'image_captcha/Image'); $this->drupalGet(''); $image_path = $this->getSession()->getPage()->find('css', '.captcha img')->getAttribute('src'); - $this->assertNull($this->drupalGetHeader('x-drupal-cache'), 'Cache disabled'); + $this->assertNull($this->getSession()->getResponseHeader('x-drupal-cache'), 'Cache disabled'); // Check that we get a new image when vising the page again. $this->drupalGet(''); - $this->assertNotEqual($image_path, $this->getSession()->getPage()->find('css', '.captcha img')->getAttribute('src')); + $this->assertNotEquals($image_path, $this->getSession()->getPage()->find('css', '.captcha img')->getAttribute('src')); // Check image caching, remove the base path since drupalGet() expects the // internal path. $this->drupalGet(substr($image_path, strlen($base_path))); - $this->assertResponse(200); + $this->assertSession()->statusCodeEquals(200); // Request image twice to make sure no errors happen (due to page caching). $this->drupalGet(substr($image_path, strlen($base_path))); - $this->assertResponse(200); + $this->assertSession()->statusCodeEquals(200); + } + + /** + * Tests a cacheable captcha type. + */ + public function testCacheableCaptcha() { + $web_assert = $this->assertSession(); + + // Enable captcha on login block with a cacheable captcha. + captcha_set_form_id_setting('user_login_form', 'captcha_test/TestCacheable'); + + // Warm up the caches. + $this->drupalGet(''); + + // Let's check if the page is cached. + $this->drupalGet(''); + static::assertSame('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Cache enabled'); + + $edit = [ + 'name' => $this->normalUser->getDisplayName(), + 'pass' => $this->normalUser->pass_raw, + 'captcha_response' => 'Test 123', + ]; + $this->submitForm($edit, 'Log in'); + $web_assert->addressEquals('user/' . $this->normalUser->id()); + + // Simulate a cron run that deletes the {captcha_session} data. + $connection = Database::getConnection(); + $connection->delete('captcha_sessions')->execute(); + + // Log out and reload the form. Because the captcha is cacheable, the form + // is retrieved from the render cache, and contains the same CSID as + // previously. + $this->drupalLogout(); + $this->drupalGet(''); + static::assertSame('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Cache enabled'); + + $edit = [ + 'name' => $this->normalUser->getDisplayName(), + 'pass' => $this->normalUser->pass_raw, + 'captcha_response' => 'Test 123', + ]; + $this->submitForm($edit, 'Log in'); + $web_assert->addressEquals('user/' . $this->normalUser->id()); } } diff --git a/web/modules/captcha/tests/src/Functional/CaptchaCronTest.php b/web/modules/captcha/tests/src/Functional/CaptchaCronTest.php index f908e2378d..44f58ccd35 100644 --- a/web/modules/captcha/tests/src/Functional/CaptchaCronTest.php +++ b/web/modules/captcha/tests/src/Functional/CaptchaCronTest.php @@ -17,7 +17,7 @@ class CaptchaCronTest extends BrowserTestBase { * * @var array */ - public static $modules = ['captcha']; + protected static $modules = ['captcha']; /** * {@inheritdoc} diff --git a/web/modules/captcha/tests/src/Functional/CaptchaPersistenceTest.php b/web/modules/captcha/tests/src/Functional/CaptchaPersistenceTest.php index 9755da9001..725795f88f 100755 --- a/web/modules/captcha/tests/src/Functional/CaptchaPersistenceTest.php +++ b/web/modules/captcha/tests/src/Functional/CaptchaPersistenceTest.php @@ -23,7 +23,8 @@ private function setUpPersistence($persistence) { $this->drupalLogin($this->adminUser); // Set persistence. $edit = ['persistence' => $persistence]; - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH, $edit, 'Save configuration'); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH); + $this->submitForm($edit, 'Save configuration'); // Log admin out. $this->drupalLogout(); @@ -48,7 +49,7 @@ private function setUpPersistence($persistence) { */ protected function assertPreservedCsid($captcha_sid_initial) { $captcha_sid = $this->getCaptchaSidFromForm(); - $this->assertEqual($captcha_sid_initial, $captcha_sid, + $this->assertEquals($captcha_sid_initial, $captcha_sid, "CAPTCHA session ID should be preserved (expected: $captcha_sid_initial, found: $captcha_sid)."); } @@ -60,7 +61,7 @@ protected function assertPreservedCsid($captcha_sid_initial) { */ protected function assertDifferentCsid($captcha_sid_initial) { $captcha_sid = $this->getCaptchaSidFromForm(); - $this->assertNotEqual($captcha_sid_initial, $captcha_sid, "CAPTCHA session ID should be different."); + $this->assertNotEquals($captcha_sid_initial, $captcha_sid, "CAPTCHA session ID should be different."); } /** @@ -82,7 +83,7 @@ public function testPersistenceAlways() { 'pass' => 'bazlaz', 'captcha_response' => 'Test 123', ]; - $this->drupalPostForm(NULL, $edit, $this->t('Log in'), [], self::LOGIN_HTML_FORM_ID); + $this->submitForm($edit, $this->t('Log in'), self::LOGIN_HTML_FORM_ID); // Check that there was no error message for the CAPTCHA. $this->assertCaptchaResponseAccepted(); @@ -92,7 +93,7 @@ public function testPersistenceAlways() { $this->assertPreservedCsid($captcha_sid_initial); // Post from again. - $this->drupalPostForm(NULL, $edit, $this->t('Log in'), [], self::LOGIN_HTML_FORM_ID); + $this->submitForm($edit, $this->t('Log in'), self::LOGIN_HTML_FORM_ID); // Check that there was no error message for the CAPTCHA. $this->assertCaptchaResponseAccepted(); $this->assertPreservedCsid($captcha_sid_initial); @@ -116,7 +117,7 @@ public function testPersistencePerFormInstance() { 'pass' => 'bazlaz', 'captcha_response' => 'Test 123', ]; - $this->drupalPostForm(NULL, $edit, $this->t('Log in'), [], self::LOGIN_HTML_FORM_ID); + $this->submitForm($edit, $this->t('Log in'), self::LOGIN_HTML_FORM_ID); // Check that there was no error message for the CAPTCHA. $this->assertCaptchaResponseAccepted(); // There shouldn't be a CAPTCHA on the new form. @@ -153,7 +154,7 @@ public function testPersistencePerFormType() { 'pass' => 'bazlaz', 'captcha_response' => 'Test 123', ]; - $this->drupalPostForm(NULL, $edit, $this->t('Log in'), [], self::LOGIN_HTML_FORM_ID); + $this->submitForm($edit, $this->t('Log in'), self::LOGIN_HTML_FORM_ID); // Check that there was no error message for the CAPTCHA. $this->assertCaptchaResponseAccepted(); // There shouldn't be a CAPTCHA on the new form. @@ -195,7 +196,7 @@ public function testPersistenceOnlyOnce() { 'pass' => 'bazlaz', 'captcha_response' => 'Test 123', ]; - $this->drupalPostForm(NULL, $edit, $this->t('Log in'), [], self::LOGIN_HTML_FORM_ID); + $this->submitForm($edit, $this->t('Log in'), self::LOGIN_HTML_FORM_ID); // Check that there was no error message for the CAPTCHA. $this->assertCaptchaResponseAccepted(); // There shouldn't be a CAPTCHA on the new form. diff --git a/web/modules/captcha/tests/src/Functional/CaptchaSessionReuseAttackTestCase.php b/web/modules/captcha/tests/src/Functional/CaptchaSessionReuseAttackTestCase.php index b165f386f0..83c7d4ffe0 100755 --- a/web/modules/captcha/tests/src/Functional/CaptchaSessionReuseAttackTestCase.php +++ b/web/modules/captcha/tests/src/Functional/CaptchaSessionReuseAttackTestCase.php @@ -14,7 +14,7 @@ class CaptchaSessionReuseAttackTestCase extends CaptchaWebTestBase { */ protected function assertCaptchaSessionIdReuseAttackDetection() { // There should be an error message about wrong response. - $this->assertText(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, + $this->assertSession()->pageTextContains(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, 'CAPTCHA response should flagged as wrong.', 'CAPTCHA' ); @@ -47,7 +47,7 @@ public function testCaptchaSessionReuseAttackDetectionOnCommentPreview() { // Post the form with the solution. $edit = $this->getCommentFormValues(); $edit['captcha_response'] = $solution; - $this->drupalPostForm(NULL, $edit, t('Preview')); + $this->submitForm($edit, 'Preview'); // Answer should be accepted and further CAPTCHA omitted. $this->assertCaptchaResponseAccepted(); $this->assertCaptchaPresence(FALSE); @@ -57,10 +57,10 @@ public function testCaptchaSessionReuseAttackDetectionOnCommentPreview() { // Post a new comment, reusing the previous CAPTCHA session. $edit = $this->getCommentFormValues(); - $this->assertSession()->hiddenFieldExists("captcha_sid")->setValue((string) $captcha_sid); - $this->assertSession()->hiddenFieldExists("captcha_token")->setValue((string) $captcha_token); + $this->assertSession()->hiddenFieldExists("captcha_sid")->setValue((string)$captcha_sid); + $this->assertSession()->hiddenFieldExists("captcha_token")->setValue((string)$captcha_token); $edit['captcha_response'] = $solution; - $this->drupalPostForm(NULL, $edit, t('Preview')); + $this->submitForm($edit, 'Preview'); // CAPTCHA session reuse attack should be detected. $this->assertCaptchaSessionIdReuseAttackDetection(); // There should be a CAPTCHA. @@ -93,7 +93,7 @@ public function testCaptchaSessionReuseAttackDetectionOnNodeForm() { $edit = $this->getNodeFormValues(); $edit['captcha_response'] = $solution; // Preview the node. - $this->drupalPostForm(NULL, $edit, t('Preview')); + $this->submitForm($edit, 'Preview'); // Answer should be accepted. $this->assertCaptchaResponseAccepted(); // Check that there is no CAPTCHA after preview. @@ -105,10 +105,10 @@ public function testCaptchaSessionReuseAttackDetectionOnNodeForm() { // Post a new node, reusing the previous CAPTCHA session. $edit = $this->getNodeFormValues(); - $this->assertSession()->hiddenFieldExists("captcha_sid")->setValue((string) $captcha_sid); - $this->assertSession()->hiddenFieldExists("captcha_token")->setValue((string) $captcha_token); + $this->assertSession()->hiddenFieldExists("captcha_sid")->setValue((string)$captcha_sid); + $this->assertSession()->hiddenFieldExists("captcha_token")->setValue((string)$captcha_token); $edit['captcha_response'] = $solution; - $this->drupalPostForm(NULL, $edit, t('Preview')); + $this->submitForm($edit, 'Preview'); // CAPTCHA session reuse attack should be detected. $this->assertCaptchaSessionIdReuseAttackDetection(); // There should be a CAPTCHA. @@ -140,12 +140,12 @@ public function testCaptchaSessionReuseAttackDetectionOnLoginForm() { 'pass' => $this->normalUser->pass_raw, 'captcha_response' => $solution, ]; - $this->drupalPostForm(NULL, $edit, t('Log in'), [], self::LOGIN_HTML_FORM_ID); + $this->submitForm($edit, 'Log in', self::LOGIN_HTML_FORM_ID); $this->assertCaptchaResponseAccepted(); $this->assertCaptchaPresence(FALSE); // If a "log out" link appears on the page, it is almost certainly because // the login was successful. - $this->assertText($this->normalUser->getDisplayName()); + $this->assertSession()->pageTextContains($this->normalUser->getDisplayName()); // Log out again. $this->drupalLogout(); @@ -154,10 +154,10 @@ public function testCaptchaSessionReuseAttackDetectionOnLoginForm() { $this->drupalGet('<front>'); // Try to log in again, reusing the previous CAPTCHA session. - $this->assertSession()->hiddenFieldExists("captcha_sid")->setValue((string) $captcha_sid); - $this->assertSession()->hiddenFieldExists("captcha_token")->setValue((string) $captcha_token); - $this->assert('pass', json_encode($edit)); - $this->drupalPostForm(NULL, $edit, t('Log in')); + $this->assertSession()->hiddenFieldExists("captcha_sid")->setValue((string)$captcha_sid); + $this->assertSession()->hiddenFieldExists("captcha_token")->setValue((string)$captcha_token); + $this->assertNotEmpty(json_encode($edit)); + $this->submitForm($edit, 'Log in'); // CAPTCHA session reuse attack should be detected. $this->assertCaptchaSessionIdReuseAttackDetection(); // There should be a CAPTCHA. @@ -182,11 +182,12 @@ public function testMultipleCaptchaProtectedFormsOnOnePage() { $edit = $this->getCommentFormValues(); $comment_subject = $edit['subject[0][value]']; $edit['captcha_response'] = 'Test 123'; - $this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $edit, t('Preview')); + $this->drupalGet('comment/reply/node/' . $node->id() . '/comment'); + $this->submitForm($edit, 'Preview'); // Post should be accepted: no warnings, // no CAPTCHA reuse detection (which could be used by user log in block). $this->assertCaptchaResponseAccepted(); - $this->assertText($comment_subject); + $this->assertSession()->pageTextContains($comment_subject); } } diff --git a/web/modules/captcha/tests/src/Functional/CaptchaTest.php b/web/modules/captcha/tests/src/Functional/CaptchaTest.php index d28c54431d..5463998664 100755 --- a/web/modules/captcha/tests/src/Functional/CaptchaTest.php +++ b/web/modules/captcha/tests/src/Functional/CaptchaTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\captcha\Functional; +use Drupal\Core\StringTranslation\StringTranslationTrait; + /** * Tests CAPTCHA main test case sensitivity. * @@ -9,12 +11,14 @@ */ class CaptchaTest extends CaptchaWebTestBase { + use StringTranslationTrait; + /** * Modules to enable. * * @var array */ - public static $modules = ['block', 'captcha_long_form_id_test']; + protected static $modules = ['block', 'captcha_long_form_id_test']; /** * Testing the protection of the user log in form. @@ -44,15 +48,50 @@ public function testCaptchaOnLoginForm() { 'pass' => $user->pass_raw, 'captcha_response' => '?', ]; - $this->drupalPostForm(NULL, $edit, t('Log in'), [], self::LOGIN_HTML_FORM_ID); + $this->submitForm($edit, $this->t('Log in'), self::LOGIN_HTML_FORM_ID); // Check for error message. - $this->assertText(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, 'CAPTCHA should block user login form', 'CAPTCHA'); + $this->assertSession()->pageTextContains(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, 'CAPTCHA should block user login form', 'CAPTCHA'); // And make sure that user is not logged in: // check for name and password fields on ?q=user. $this->drupalGet('user'); - $this->assertField('name', t('Username field found.'), 'CAPTCHA'); - $this->assertField('pass', t('Password field found.'), 'CAPTCHA'); + $this->assertSession()->fieldExists('name'); + $this->assertSession()->fieldExists('pass'); + } + + /** + * Testing the response error menssage. + */ + public function testCaptchaResponseErrorMenssage() { + // Customize the response error message. + $this->drupalLogin($this->adminUser); + $customized_menssage = 'The answer you entered is wrong.'; + $edit = [ + 'wrong_captcha_response_message' => $customized_menssage, + ]; + $this->drupalGet("admin/config/people/captcha"); + $this->submitForm($edit, $this->t('Save configuration')); + + // Set a CAPTCHA on login form. + /* @var \Drupal\captcha\Entity\CaptchaPoint $captcha_point */ + $captcha_point = \Drupal::entityTypeManager() + ->getStorage('captcha_point') + ->load('user_login_form'); + $captcha_point->setCaptchaType('captcha/Math'); + $captcha_point->enable()->save(); + + // Check if the menssage is default. + $this->drupalLogout(); + $this->drupalGet('user'); + // Try to log in, which should fail. + $edit = [ + 'name' => $this->adminUser->getDisplayName(), + 'pass' => $this->adminUser->pass_raw, + 'captcha_response' => '?', + ]; + $this->submitForm($edit, $this->t('Log in'), self::LOGIN_HTML_FORM_ID); + $this->assertSession()->pageTextContains($customized_menssage, 'CAPTCHA should block user login form', 'CAPTCHA'); + } /** @@ -83,23 +122,24 @@ protected function assertCommentPosting($captcha_response, $should_pass, $messag $comment_subject = $edit['subject[0][value]']; $comment_body = $edit['comment_body[0][value]']; $edit['captcha_response'] = $captcha_response; - $this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $edit, t('Save'), [], 'comment-form'); + $this->drupalGet('comment/reply/node/' . $node->id() . '/comment'); + $this->submitForm($edit, $this->t('Save'), 'comment-form'); if ($should_pass) { // There should be no error message. $this->assertCaptchaResponseAccepted(); // Get node page and check that comment shows up. $this->drupalGet('node/' . $node->id()); - $this->assertText($comment_subject, $message . ' Comment should show up on node page.', 'CAPTCHA'); - $this->assertText($comment_body, $message . ' Comment should show up on node page.', 'CAPTCHA'); + $this->assertSession()->pageTextContains($comment_subject, $message . ' Comment should show up on node page.', 'CAPTCHA'); + $this->assertSession()->pageTextContains($comment_body, $message . ' Comment should show up on node page.', 'CAPTCHA'); } else { // Check for error message. - $this->assertText(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, $message . ' Comment submission should be blocked.', 'CAPTCHA'); + $this->assertSession()->pageTextContains(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, $message . ' Comment submission should be blocked.', 'CAPTCHA'); // Get node page and check that comment is not present. $this->drupalGet('node/' . $node->id()); - $this->assertNoText($comment_subject, $message . ' Comment should not show up on node page.', 'CAPTCHA'); - $this->assertNoText($comment_body, $message . ' Comment should not show up on node page.', 'CAPTCHA'); + $this->assertSession()->pageTextNotContains($comment_subject, $message . ' Comment should not show up on node page.', 'CAPTCHA'); + $this->assertSession()->pageTextNotContains($comment_body, $message . ' Comment should not show up on node page.', 'CAPTCHA'); } } @@ -153,7 +193,8 @@ public function testCaptchaDescriptionAfterCommentPreview() { // Preview comment with correct CAPTCHA answer. $edit = $this->getCommentFormValues(); $edit['captcha_response'] = 'Test 123'; - $this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $edit, t('Preview')); + $this->drupalGet('comment/reply/node/' . $node->id() . '/comment'); + $this->submitForm($edit, $this->t('Preview')); // Check that there is no CAPTCHA after preview. $this->assertCaptchaPresence(FALSE); @@ -180,7 +221,7 @@ public function testCaptchaSessionReuseOnNodeForms() { $edit = $this->getNodeFormValues(); $edit['captcha_response'] = 'Test 123'; $this->drupalGet('node/add/page'); - $this->drupalPostForm(NULL, $edit, t('Preview')); + $this->submitForm($edit, $this->t('Preview')); $this->assertCaptchaPresence(FALSE); } @@ -229,8 +270,9 @@ public function testLongFormId() { ]; // Create intentionally long id Captcha Point. - $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add', $form_values, t('Save')); - $this->assertRaw(t('Captcha Point for %label form was created.', ['%label' => $formId])); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH . '/captcha-points/add'); + $this->submitForm($form_values, $this->t('Save')); + $this->assertSession()->responseContains($this->t('Captcha Point for %label form was created.', ['%label' => $formId])); // We need to log out to test the captcha. $this->drupalLogout(); diff --git a/web/modules/captcha/tests/src/Functional/CaptchaWebTestBase.php b/web/modules/captcha/tests/src/Functional/CaptchaWebTestBase.php index 705684fd73..f4598ef8e0 100755 --- a/web/modules/captcha/tests/src/Functional/CaptchaWebTestBase.php +++ b/web/modules/captcha/tests/src/Functional/CaptchaWebTestBase.php @@ -44,7 +44,7 @@ abstract class CaptchaWebTestBase extends BrowserTestBase { * * @var array */ - public static $modules = ['captcha', 'comment']; + protected static $modules = ['captcha', 'comment']; /** * {@inheritdoc} @@ -120,12 +120,12 @@ public function setUp() { */ protected function assertCaptchaResponseAccepted() { // There should be no error message about unknown CAPTCHA session ID. - $this->assertNoText(self::CAPTCHA_UNKNOWN_CSID_ERROR_MESSAGE, + $this->assertSession()->pageTextNotContains(self::CAPTCHA_UNKNOWN_CSID_ERROR_MESSAGE, 'CAPTCHA response should be accepted (known CSID).', 'CAPTCHA' ); // There should be no error message about wrong response. - $this->assertNoText(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, + $this->assertSession()->pageTextNotContains(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, 'CAPTCHA response should be accepted (correct response).', 'CAPTCHA' ); @@ -139,12 +139,12 @@ protected function assertCaptchaResponseAccepted() { */ protected function assertCaptchaPresence($presence) { if ($presence) { - $this->assertText(_captcha_get_description(), + $this->assertSession()->pageTextContains(_captcha_get_description(), 'There should be a CAPTCHA on the form.', 'CAPTCHA' ); } else { - $this->assertNoText(_captcha_get_description(), + $this->assertSession()->pageTextNotContains(_captcha_get_description(), 'There should be no CAPTCHA on the form.', 'CAPTCHA' ); } @@ -236,9 +236,9 @@ protected function getMathCaptchaSolutionFromForm($form_html_id = NULL) { else { $elements = $this->xpath('//form[@id="' . $form_html_id . '"]//div[contains(@class, "form-item-captcha-response")]/span[@class="field-prefix"]'); } - $this->assert('pass', json_encode($elements)); + $this->assertTrue('pass', json_encode($elements)); $challenge = (string) $elements[0]; - $this->assert('pass', $challenge); + $this->assertTrue('pass', $challenge); // Extract terms and operator from challenge. $matches = []; preg_match('/\\s*(\\d+)\\s*(-|\\+)\\s*(\\d+)\\s*=\\s*/', $challenge, $matches); diff --git a/web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaPointsTest.php b/web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaPointsTest.php new file mode 100644 index 0000000000..b2148e4179 --- /dev/null +++ b/web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaPointsTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\Tests\captcha\Kernel\Migrate\d7; + +use Drupal\captcha\CaptchaPointInterface; +use Drupal\captcha\Entity\CaptchaPoint; +use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase; + +/** + * Migrates various configuration objects owned by the captcha module. + * + * @group captcha + */ +class MigrateCaptchaPointsTest extends MigrateDrupal7TestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['captcha']; + + /** + * The expected captcha points from the Captcha fixture. + * + * @var array[] + */ + protected $captchaPoints = [ + [ + 'form_id' => 'comment_node_article_form', + 'captcha_type' => 'captcha/Math', + 'status' => TRUE, + ], + [ + 'form_id' => 'user_pass', + 'captcha_type' => 'captcha/Math', + 'status' => TRUE, + ], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->loadFixture(implode(DIRECTORY_SEPARATOR, [ + DRUPAL_ROOT, + drupal_get_path('module', 'captcha'), + 'tests', + 'fixtures', + 'drupal7.php', + ])); + + $this->installEntitySchema('captcha_point'); + $this->installSchema('captcha', ['captcha_sessions']); + $this->installConfig('captcha'); + + $migrations = [ + 'd7_captcha_points', + ]; + $this->executeMigrations($migrations); + + $this->captchaStorage = $this->container->get('entity_type.manager') + ->getStorage('captcha_point'); + } + + /** + * Tests a single captcha point type. + * + * @param string $form_id + * The captcha point form id. + * @param string $captcha_type + * The expected captcha type for the config. + * @param bool $status + * The expected status for a captcha type. + */ + protected function assertEntity(string $form_id, string $captcha_type, $status) { + /** @var \Drupal\captcha\CaptchaPointInterface $entity */ + $entity = CaptchaPoint::load($form_id); + $this->assertInstanceOf(CaptchaPointInterface::class, $entity); + $this->assertSame($form_id, $entity->getFormId()); + $this->assertSame($captcha_type, $entity->getCaptchaType()); + $this->assertSame($status, $entity->status()); + } + + /** + * Tests that all expected configuration gets migrated. + */ + public function testCaptchaPointsMigration() { + // Test captcha points. + foreach ($this->captchaPoints as $captcha_point) { + $this->assertEntity($captcha_point['form_id'], $captcha_point['captcha_type'], $captcha_point['status']); + } + } + +} diff --git a/web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaSimpleConfigurationTest.php b/web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaSimpleConfigurationTest.php new file mode 100644 index 0000000000..82fa0447e0 --- /dev/null +++ b/web/modules/captcha/tests/src/Kernel/Migrate/d7/MigrateCaptchaSimpleConfigurationTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\Tests\captcha\Kernel\Migrate\d7; + +use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase; + +/** + * Migrates various configuration objects owned by the captcha module. + * + * @group captcha + */ +class MigrateCaptchaSimpleConfigurationTest extends MigrateDrupal7TestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['captcha']; + + /** + * The expected configuration from the Captcha fixture. + * + * @var array[] + */ + protected $expectedConfig = [ + 'captcha.settings' => [ + 'enabled_default' => 1, + 'default_challenge' => 'captcha/Math', + 'description' => 'This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.', + 'administration_mode' => TRUE, + 'allow_on_admin_pages' => FALSE, + 'add_captcha_description' => TRUE, + 'default_validation' => 1, + 'persistence' => 1, + 'enable_stats' => TRUE, + 'log_wrong_responses' => TRUE, + ], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->loadFixture(implode(DIRECTORY_SEPARATOR, [ + DRUPAL_ROOT, + drupal_get_path('module', 'captcha'), + 'tests', + 'fixtures', + 'drupal7.php', + ])); + + $migrations = [ + 'd7_captcha_settings', + ]; + $this->executeMigrations($migrations); + } + + /** + * Tests that all expected configuration gets migrated. + */ + public function testConfigurationMigration() { + // Test Config. + foreach ($this->expectedConfig as $config_id => $values) { + $actual = \Drupal::config($config_id)->get(); + $this->assertSame($values, $actual); + } + } + +} -- GitLab