diff --git a/composer.json b/composer.json index 2c8c75f307464b4a2db4e24b73138aeff2fd90b0..6744661d4180f8e9d7efc033bc10274ba7d193fd 100644 --- a/composer.json +++ b/composer.json @@ -152,6 +152,7 @@ "drupal/paragraphs": "1.6", "drupal/pathauto": "1.0", "drupal/realname": "^1.0@RC", + "drupal/recaptcha": "^2.4", "drupal/redirect": "^1.3", "drupal/redis": "1.0", "drupal/roleassign": "^1.0@alpha", diff --git a/composer.lock b/composer.lock index 0daea23f3b82839f9ea3226064fcabff8704a901..82e47ddba1a30a6258be2d1fb7e96007bdbe2e91 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "05189e0457ebaecf7dad6250a18bce1b", + "content-hash": "17eeff467ad7e469cf72b3edf5b29978", "packages": [ { "name": "alchemy/zippy", @@ -2716,6 +2716,73 @@ "irc": "irc://irc.freenode.org/drupal-bootstrap" } }, + { + "name": "drupal/captcha", + "version": "1.0.0-beta1", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/captcha.git", + "reference": "8.x-1.0-beta1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/captcha-8.x-1.0-beta1.zip", + "reference": "8.x-1.0-beta1", + "shasum": "5b6c701bdab332bac61f59dcc78e05578f94d8b7" + }, + "require": { + "drupal/core": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.0-beta1", + "datestamp": "1487198586", + "security-coverage": { + "status": "not-covered", + "message": "Beta releases are not covered by Drupal security advisories." + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "RobLoach", + "homepage": "https://www.drupal.org/user/61114" + }, + { + "name": "elachlan", + "homepage": "https://www.drupal.org/user/1021502" + }, + { + "name": "naveenvalecha", + "homepage": "https://www.drupal.org/user/2665733" + }, + { + "name": "podarok", + "homepage": "https://www.drupal.org/user/116002" + }, + { + "name": "soxofaan", + "homepage": "https://www.drupal.org/user/41478" + }, + { + "name": "wundo", + "homepage": "https://www.drupal.org/user/25523" + } + ], + "description": "The CAPTCHA module provides this feature to virtually any user facing web form on a Drupal site.", + "homepage": "https://www.drupal.org/project/captcha", + "support": { + "source": "https://git.drupalcode.org/project/captcha" + } + }, { "name": "drupal/ckeditor_indentblock", "version": "1.0.0-beta1", @@ -6444,6 +6511,71 @@ "issues": "https://www.drupal.org/project/issues/realname" } }, + { + "name": "drupal/recaptcha", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/recaptcha.git", + "reference": "8.x-2.4" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/recaptcha-8.x-2.4.zip", + "reference": "8.x-2.4", + "shasum": "ee68020dc33f880313b83d8bbcb159ef85285c7a" + }, + "require": { + "drupal/captcha": "^1.0.0-alpha1", + "drupal/core": "~8.0" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + }, + "drupal": { + "version": "8.x-2.4", + "datestamp": "1548967980", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "hass", + "homepage": "https://www.drupal.org/u/hass" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/147903/committers" + }, + { + "name": "diolan", + "homepage": "https://www.drupal.org/user/2336786" + }, + { + "name": "hass", + "homepage": "https://www.drupal.org/user/85918" + }, + { + "name": "id.medion", + "homepage": "https://www.drupal.org/user/2542592" + } + ], + "description": "Protect your website from spam and abuse while letting real people pass through with ease.", + "homepage": "https://www.drupal.org/project/recaptcha", + "support": { + "source": "https://git.drupal.org/project/recaptcha.git", + "issues": "https://www.drupal.org/project/issues/recaptcha" + } + }, { "name": "drupal/redirect", "version": "1.3.0", diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 0866de11b1d6ebe0ea6d1989cb8f1abb3339de8a..58182d5d45851236b569bffe2268727748fe0e89 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -2201,8 +2201,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 8e61c95c2025b506d02d980b38ead6d277628265..831410e9886223c79ebd75399ed2b202a630c789 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -2847,8 +2847,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 81688fa97cd6f4ddfc7d9e009c627f9be1c4ae67..6e89ad29b014982bbd9e506a8715b8e6a45a3aa2 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2803,6 +2803,75 @@ "irc": "irc://irc.freenode.org/drupal-bootstrap" } }, + { + "name": "drupal/captcha", + "version": "1.0.0-beta1", + "version_normalized": "1.0.0.0-beta1", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/captcha.git", + "reference": "8.x-1.0-beta1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/captcha-8.x-1.0-beta1.zip", + "reference": "8.x-1.0-beta1", + "shasum": "5b6c701bdab332bac61f59dcc78e05578f94d8b7" + }, + "require": { + "drupal/core": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.0-beta1", + "datestamp": "1487198586", + "security-coverage": { + "status": "not-covered", + "message": "Beta releases are not covered by Drupal security advisories." + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "RobLoach", + "homepage": "https://www.drupal.org/user/61114" + }, + { + "name": "elachlan", + "homepage": "https://www.drupal.org/user/1021502" + }, + { + "name": "naveenvalecha", + "homepage": "https://www.drupal.org/user/2665733" + }, + { + "name": "podarok", + "homepage": "https://www.drupal.org/user/116002" + }, + { + "name": "soxofaan", + "homepage": "https://www.drupal.org/user/41478" + }, + { + "name": "wundo", + "homepage": "https://www.drupal.org/user/25523" + } + ], + "description": "The CAPTCHA module provides this feature to virtually any user facing web form on a Drupal site.", + "homepage": "https://www.drupal.org/project/captcha", + "support": { + "source": "https://git.drupalcode.org/project/captcha" + } + }, { "name": "drupal/ckeditor_indentblock", "version": "1.0.0-beta1", @@ -6643,6 +6712,73 @@ "issues": "https://www.drupal.org/project/issues/realname" } }, + { + "name": "drupal/recaptcha", + "version": "2.4.0", + "version_normalized": "2.4.0.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/recaptcha.git", + "reference": "8.x-2.4" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/recaptcha-8.x-2.4.zip", + "reference": "8.x-2.4", + "shasum": "ee68020dc33f880313b83d8bbcb159ef85285c7a" + }, + "require": { + "drupal/captcha": "^1.0.0-alpha1", + "drupal/core": "~8.0" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + }, + "drupal": { + "version": "8.x-2.4", + "datestamp": "1548967980", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "hass", + "homepage": "https://www.drupal.org/u/hass" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/147903/committers" + }, + { + "name": "diolan", + "homepage": "https://www.drupal.org/user/2336786" + }, + { + "name": "hass", + "homepage": "https://www.drupal.org/user/85918" + }, + { + "name": "id.medion", + "homepage": "https://www.drupal.org/user/2542592" + } + ], + "description": "Protect your website from spam and abuse while letting real people pass through with ease.", + "homepage": "https://www.drupal.org/project/recaptcha", + "support": { + "source": "https://git.drupal.org/project/recaptcha.git", + "issues": "https://www.drupal.org/project/issues/recaptcha" + } + }, { "name": "drupal/redirect", "version": "1.3.0", diff --git a/web/modules/captcha/.travis.yml b/web/modules/captcha/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..0ca22fed5ade83d1e1ea6f3d3d30f32252629959 --- /dev/null +++ b/web/modules/captcha/.travis.yml @@ -0,0 +1,113 @@ +# @file +# .travis.yml - Drupal for Travis CI Integration +# +# Template provided by https://github.com/LionsAd/drupal_ti. +# +# Based for simpletest upon: +# https://github.com/sonnym/travis-ci-drupal-module-example + +language: php + +sudo: false + +php: + - 5.5 + - 5.6 + - 7 + - hhvm + +matrix: + fast_finish: true + allow_failures: + - php: 7 + - php: hhvm + +env: + global: + # add composer's global bin directory to the path + # see: https://github.com/drush-ops/drush#install---composer + - PATH="$PATH:$HOME/.composer/vendor/bin" + + # Configuration variables. + - DRUPAL_TI_MODULE_NAME="captcha" + - DRUPAL_TI_SIMPLETEST_GROUP="captcha" + + # Define runners and environment vars to include before and after the + # main runners / environment vars. + #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./.drupal_ti/before" + #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after" + + # The environment to use, supported are: drupal-7, drupal-8 + - DRUPAL_TI_ENVIRONMENT="drupal-8" + + # Drupal specific variables. + - DRUPAL_TI_DB="drupal_travis_db" + - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db" + # Note: Do not add a trailing slash here. + - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1" + - DRUPAL_TI_WEBSERVER_PORT="8080" + + # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end. + - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT" + + # === Behat specific variables. + # This is relative to $TRAVIS_BUILD_DIR + - DRUPAL_TI_BEHAT_DIR="./tests/behat" + # These arguments are passed to the bin/behat command. + - DRUPAL_TI_BEHAT_ARGS="" + # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables. + - DRUPAL_TI_BEHAT_YML="behat.yml.dist" + # This is used to setup Xvfb. + - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16" + # The version of seleniumthat should be used. + - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44" + # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here. + - DRUPAL_TI_BEHAT_DRIVER="phantomjs" + - DRUPAL_TI_BEHAT_BROWSER="firefox" + + # PHPUnit specific commandline arguments. + - DRUPAL_TI_PHPUNIT_ARGS="--verbose --debug" + # Specifying the phpunit-core src/ directory is useful when e.g. a vendor/ + # directory is present in the module directory, which phpunit would then + # try to find tests in. This option is relative to $TRAVIS_BUILD_DIR. + #- DRUPAL_TI_PHPUNIT_CORE_SRC_DIRECTORY="./tests/src" + + # Code coverage via coveralls.io + - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*" + # This needs to match your .coveralls.yml file. + - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml" + + # Debug options + #- DRUPAL_TI_DEBUG="-x -v" + # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium", + # etc. to only output those channels. + #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver" + + matrix: + # [[[ SELECT ANY OR MORE OPTIONS ]]] + #- DRUPAL_TI_RUNNERS="phpunit" + #- DRUPAL_TI_RUNNERS="simpletest" + #- DRUPAL_TI_RUNNERS="behat" + - DRUPAL_TI_RUNNERS="phpunit-core simpletest" + +mysql: + database: drupal_travis_db + username: root + encoding: utf8 + +before_install: + - composer self-update + - composer global require "lionsad/drupal_ti:1.*" + - drupal-ti before_install + +install: + - drupal-ti install + +before_script: + - drupal-ti before_script + +script: + - drupal-ti script + +after_script: + - drupal-ti after_script diff --git a/web/modules/captcha/LICENSE.txt b/web/modules/captcha/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1 --- /dev/null +++ b/web/modules/captcha/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/modules/captcha/README.md b/web/modules/captcha/README.md new file mode 100755 index 0000000000000000000000000000000000000000..306ac310cadd96d5f48a76ceb0ca5ca7f4bd95d6 --- /dev/null +++ b/web/modules/captcha/README.md @@ -0,0 +1,60 @@ +CAPTCHA module for Drupal +--------------------------- +[![Build Status] +(https://travis-ci.org/chuva-inc/captcha.svg?branch=8.x-1.x)] +(https://travis-ci.org/chuva-inc/captcha) + +[![Code Climate] +(https://codeclimate.com/github/chuva-inc/captcha/badges/gpa.svg)] +(https://codeclimate.com/github/chuva-inc/captcha) + +DESCRIPTION +----------- + captcha.module is the basic CAPTCHA module, offering general CAPTCHA + administration and a simple maths challenge. + +SUB MODULE +---------- + image_captcha.module offers an image based challenge. + +INSTALLATION: +------------- + 1. Extract the tar.gz into your 'modules' or directory and copy to modules + folder. + 2. Go to "Extend" after successfully login into admin. + 3. Enable the module at 'administer >> modules'. + +DEPENDENCIES +------------ + The basic CAPTCHA module has no dependencies, nothing special is required. + +CONFLICTS/KNOWN ISSUES +---------------------- + CAPTCHA and page caching do not work together currently. + However, the CAPTCHA module does support the Drupal core page + caching mechanism: it just disables the caching of the pages + where it has to put its challenges. + If you use other caching mechanisms, it is possible that CAPTCHA's + won't work, and you get error messages like 'CAPTCHA validation + error: unknown CAPTCHA session ID'. + +CONFIGURATION +------------- + The configuration page is at admin/config/people/captcha, + where you can configure the CAPTCHA module + and enable challenges for the desired forms. + You can also tweak the image CAPTCHA to your liking. + +UNINSTALLATION +-------------- + 1. Disable the module from 'administer >> modules'. + 2. Uninstall the module + +MAINTAINERS +----------- + Current maintainers: + * Fabiano Sant'Ana (wundo) - https://www.drupal.org/u/wundo + * Andrii Podanenko (podarok) - https://www.drupal.org/u/podarok + * soxofaan - https://www.drupal.org/u/soxofaan + * Lachlan Ennis (elachlan) - https://www.drupal.org/u/elachlan + * Rob Loach (RobLoach) - https://www.drupal.org/u/robloach diff --git a/web/modules/captcha/captcha.admin.inc b/web/modules/captcha/captcha.admin.inc new file mode 100755 index 0000000000000000000000000000000000000000..ae69bd343f30cb99deee4d0c5f74a03892900d34 --- /dev/null +++ b/web/modules/captcha/captcha.admin.inc @@ -0,0 +1,52 @@ +<?php + +/** + * @file + * Functionality and helper functions for CAPTCHA administration. + */ + +/** + * Return an array with the available CAPTCHA types. + * + * For use as options array for a select form elements. + * + * @param bool $add_special_options + * If true: also add the 'default' option. + * + * @return array + * An associative array mapping "$module/$type" to + * "$type (from module $module)" with $module the module name + * implementing the CAPTCHA and $type the name of the CAPTCHA type. + */ +function _captcha_available_challenge_types($add_special_options = TRUE) { + $captcha_types = []; + if ($add_special_options) { + $captcha_types['default'] = t('Default challenge type'); + } + + // We do our own version of Drupal's module_invoke_all() here because + // we want to build an array with custom keys and values. + foreach (\Drupal::moduleHandler()->getImplementations('captcha') as $module) { + $result = call_user_func_array($module . '_captcha', ['list']); + if (is_array($result)) { + foreach ($result as $type) { + $captcha_types["$module/$type"] = t('@type (from module @module)', [ + '@type' => $type, + '@module' => $module, + ]); + } + } + } + return $captcha_types; +} + +/** + * Helper function for generating an example challenge. + */ +function _captcha_generate_example_challenge($module, $type) { + return [ + '#type' => 'captcha', + '#captcha_type' => $module . '/' . $type, + '#captcha_admin_mode' => TRUE, + ]; +} diff --git a/web/modules/captcha/captcha.api.php b/web/modules/captcha/captcha.api.php new file mode 100644 index 0000000000000000000000000000000000000000..23ace70e46714971ccaf154ae9133027d2fe8265 --- /dev/null +++ b/web/modules/captcha/captcha.api.php @@ -0,0 +1,180 @@ +<?php + +/** + * @file + * Hooks for the captcha module. + */ + +/** + * Implements hook_captcha(). + * + * This documentation is for developers that want to implement their own + * challenge type and integrate it with the base CAPTCHA module. + * === Required: hook_captcha($op, $captcha_type='') === + * The hook_captcha() hook is the only required function if you want to + * integrate with the base CAPTCHA module. + * Functionality depends on the first argument $op: + * 'list': you should return an array of possible challenge types that + * your module implements. + * 'generate': generate a challenge. + * You should return an array that offers form elements and the solution + * of your challenge, defined by the second argument $captcha_type. + * The returned array $captcha should have the following items: + * $captcha['solution']: this is the solution of your challenge + * $captcha['form']: an array of the form elements you want to add to the form. + * There should be a key 'captcha_response' in this array, which points to + * the form element where the user enters his answer. + * An optional additional argument $captcha_sid with the captcha session ID is + * available for more advanced challenges (e.g. the image CAPTCHA uses this + * argument, see image_captcha_captcha()) and it is used for every session. + * Let's give a simple example to make this more clear. + * We create the challenge 'Foo CAPTCHA', which requires the user to + * enter "foo" in a textfield. + */ +function foo_captcha_captcha($op, $captcha_type = '') { + switch ($op) { + case 'list': + return ['Foo CAPTCHA']; + + case 'generate': + if ($captcha_type == 'Foo CAPTCHA') { + $captcha = []; + $captcha['solution'] = 'foo'; + $captcha['form']['captcha_response'] = [ + '#type' => 'textfield', + '#title' => t('Enter "foo"'), + '#required' => TRUE, + ]; + // The CAPTCHA module provides an option for case sensitive and case + // insensitve validation of the responses. If this is not sufficient, + // you can provide your own validation function with the + // 'captcha_validate' field, illustrated by the following example: + $captcha['captcha_validate'] = 'foo_captcha_custom_validation'; + return $captcha; + } + break; + } +} + +/** + * Implements hook_menu(). + * + * Validation of the answer against the solution and other stuff is done by the + * base CAPTCHA module. + * === Recommended: hook_menu($may_cache) === + * More advanced CAPTCHA modules probably want some configuration page. + * To integrate nicely with the base CAPTCHA module you should offer your + * configuration page as a MENU_LOCAL_TASK menu entry under + * 'admin/config/people/captcha/'. + * For our simple foo CAPTCHA module this would mean: + */ +function foo_captcha_menu($may_cache) { + $items = []; + if ($may_cache) { + $items['admin/config/people/captcha/foo_captcha'] = [ + 'title' => t('Foo CAPTCHA'), + 'page callback' => 'drupal_get_form', + 'page arguments' => ['foo_captcha_settings_form'], + 'type' => MENU_LOCAL_TASK, + ]; + } + return $items; +} + +/** + * Implements hook_help(). + * + * You should of course implement a function foo_captcha_settings_form() which + * returns the form of your configuration page. + * === Optional: hook_help($section) === + * To offer a description/explanation of your challenge, you can use the + * normal hook_help() system. + * For our simple foo CAPTCHA module this would mean: + */ +function foo_captcha_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'foo_captcha.settings': + return '<p>' . t('This is a very simple challenge, which requires users to + enter "foo" in a textfield.') . '</p>'; + } +} + +/** + * Custom CAPTCHA validation function. + * + * Previous example shows the basic usage for custom validation with only a + * $solution and $response argument, which should be sufficient for most CAPTCHA + * modules. More advanced CAPTCHA modules can also use extra provided arguments + * $element and $form_state: + * + * @param $solution + * the solution for the challenge as reported by hook_captcha('generate',...). + * @param $response + * the answer given by the user. + * + * @return true + * on success and FALSE on failure. + */ +function foo_captcha_custom_validation($solution, $response) { + return $response == "foo" || $response == "bar"; +} + +/** + * Custom Advance CAPTCHA validation function. + * + * These extra arguments are the $element and $form_state arguments of the + * validation function of the #captcha element. See captcha_validate() in + * captcha.module for more info about this. + * + * @param $solution + * the solution for the challenge as reported by hook_captcha('generate',...). + * @param $response + * the answer given by the user. + * @param $element + * element argument. + * @param $form_state + * form_state argument. + * + * @return true + * on success and FALSE on failure. + */ +function foo_captcha_custom_advance_validation($solution, $response, $element, $form_state) { + return $form_state['foo']['#bar'] = 'baz'; +} + +/** + * Implements hook_captcha_placement_map(). + * + * === Hook into CAPTCHA placement === + * The CAPTCHA module attempts to place the CAPTCHA element in an appropriate + * spot at the bottom of the targeted form, but this automatic detection may be + * insufficient for complex forms. + * The hook_captcha_placement_map hook allows to define the placement of the + * CAPTCHA element as desired. The hook should return an array, mapping form IDs + * to placement arrays, which are associative arrays with the following fields: + * 'path': path (array of path items) of the form's container element in which + * the CAPTCHA element should be inserted. + * 'key': the key of the element before which the CAPTCHA element + * should be inserted. If the field 'key' is undefined or NULL, the CAPTCHA + * will just be appended in the container. + * 'weight': if 'key' is not NULL: should be the weight of the element defined + * by 'key'. If 'key' is NULL and weight is not NULL/unset: set the weight + * property of the CAPTCHA element to this value. + * For example: + * This will place the CAPTCHA element + * in the 'my_fancy_form' form inside the container $form['items']['buttons'], + * just before the element $form['items']['buttons']['sacebutton']. + * in the 'another_form' form at the toplevel of the form, with a weight 34. + */ +function hook_captcha_placement_map() { + return [ + 'my_fancy_form' => [ + 'path' => ['items', 'buttons'], + 'key' => 'savebutton', + ], + 'another_form' => [ + 'path' => [], + 'weight' => 34, + ], + ]; +} diff --git a/web/modules/captcha/captcha.inc b/web/modules/captcha/captcha.inc new file mode 100755 index 0000000000000000000000000000000000000000..f4f34fd148f2967b10bbaa27024d3cbbef78edf8 --- /dev/null +++ b/web/modules/captcha/captcha.inc @@ -0,0 +1,371 @@ +<?php + +/** + * @file + * General CAPTCHA functionality and helper functions. + */ + +use Drupal\captcha\Entity\CaptchaPoint; +use Drupal\Component\Utility\Xss; +use Drupal\Core\Render\Element; + +/** + * Helper function for adding/updating a CAPTCHA point. + * + * @param string $form_id + * the form ID to configure. + * @param string $captcha_type + * The setting for the given form_id, can be: + * - 'default' to use the default challenge type + * - NULL to remove the entry for the CAPTCHA type + * - something of the form 'image_captcha/Image' + * - an object with attributes $captcha_type->module + * and $captcha_type->captcha_type. + */ +function captcha_set_form_id_setting($form_id, $captcha_type) { + /* @var CaptchaPoint $captcha_point */ + $captcha_point = CaptchaPoint::load($form_id); + + if ($captcha_point) { + $captcha_point->setCaptchaType($captcha_type); + } + else { + $captcha_point = new CaptchaPoint([ + 'formId' => $form_id, + 'captchaType' => $captcha_type, + ], 'captcha_point'); + } + $captcha_point->enable(); + + $captcha_point->save(); +} + +/** + * Get the CAPTCHA setting for a given form_id. + * + * @param string $form_id + * The form_id to query for. + * @param bool $symbolic + * Flag to return as (symbolic) strings instead of object. + * + * @return null|CaptchaPoint + * NULL if no setting is known + * captcha point object with fields 'module' and 'captcha_type'. + * If argument $symbolic is true, returns 'default' or in the + * form 'captcha/Math'. + */ +function captcha_get_form_id_setting($form_id, $symbolic = FALSE) { + /* @var CaptchaPoint $captchaPoint */ + $captcha_point = CaptchaPoint::load($form_id); + + if ($symbolic) { + $captcha_point = $captcha_point->getCaptchaType(); + } + + return $captcha_point; +} + +/** + * Helper function for generating a new CAPTCHA session. + * + * @param string $form_id + * The form_id of the form to add a CAPTCHA to. + * @param int $status + * The initial status of the CAPTHCA session. + * + * @return string + * The session ID of the new CAPTCHA session. + */ +function _captcha_generate_captcha_session($form_id = NULL, $status = CAPTCHA_STATUS_UNSOLVED) { + $user = \Drupal::currentUser(); + + // Initialize solution with random data. + $solution = hash('sha256', mt_rand()); + + // Insert an entry and thankfully receive the value + // of the autoincrement field 'csid'. + $captcha_sid = db_insert('captcha_sessions') + ->fields([ + 'uid' => $user->id(), + 'sid' => session_id(), + 'ip_address' => Drupal::request()->getClientIp(), + 'timestamp' => REQUEST_TIME, + 'form_id' => $form_id, + 'solution' => $solution, + 'status' => $status, + 'attempts' => 0, + ]) + ->execute(); + return $captcha_sid; +} + +/** + * Helper function for updating the solution in the CAPTCHA session table. + * + * @param string $captcha_sid + * The CAPTCHA session ID to update. + * @param string $solution + * The new solution to associate with the given CAPTCHA session. + */ +function _captcha_update_captcha_session($captcha_sid, $solution) { + db_update('captcha_sessions') + ->condition('csid', $captcha_sid) + ->fields([ + 'timestamp' => REQUEST_TIME, + 'solution' => $solution, + ]) + ->execute(); +} + +/** + * Helper function for checking if CAPTCHA is required for user. + * + * Based on the CAPTCHA persistence setting, the CAPTCHA session + * ID and user session info. + */ +function _captcha_required_for_user($captcha_sid, $form_id) { + // Get the CAPTCHA persistence setting. + $captcha_persistence = \Drupal::config('captcha.settings') + ->get('persistence'); + + // First check: should we always add a CAPTCHA? + if ($captcha_persistence == CAPTCHA_PERSISTENCE_SHOW_ALWAYS) { + return TRUE; + } + + // Get the status of the current CAPTCHA session. + $captcha_session_status = db_query('SELECT status FROM {captcha_sessions} WHERE csid = :csid', [':csid' => $captcha_sid])->fetchField(); + // Second check: if the current session is already + // solved: omit further CAPTCHAs. + if ($captcha_session_status == CAPTCHA_STATUS_SOLVED) { + return FALSE; + } + + // Third check: look at the persistence level + // (per form instance, per form or per user). + if ($captcha_persistence == CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE) { + return TRUE; + } + else { + $captcha_success_form_ids = isset($_SESSION['captcha_success_form_ids']) ? (array) ($_SESSION['captcha_success_form_ids']) : []; + switch ($captcha_persistence) { + case CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL: + return (count($captcha_success_form_ids) == 0); + + case CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE: + return !isset($captcha_success_form_ids[$form_id]); + } + } + + // We should never get to this point, but to be sure, we return TRUE. + return TRUE; +} + +/** + * Get the CAPTCHA description. + * + * @return string + * CAPTCHA description. + */ +function _captcha_get_description() { + $description = \Drupal::config('captcha.settings')->get('description'); + return Xss::filter($description); +} + +/** + * Parse or interpret the given captcha_type. + * + * @param string $captcha_type + * representation of the CAPTCHA type, + * e.g. 'default', 'captcha/Math', 'image_captcha/Image'. + * + * @return array + * list($captcha_module, $captcha_type). + */ +function _captcha_parse_captcha_type($captcha_type) { + if ($captcha_type == 'default') { + $captcha_type = \Drupal::config('captcha.settings') + ->get('default_challenge', 'captcha/Math'); + } + return explode('/', $captcha_type); +} + +/** + * Helper function to get placement information for a given form_id. + */ +function _captcha_get_captcha_placement($form_id, $form) { + // Get CAPTCHA placement map from cache. Two levels of cache: + // static variable in this function and storage in the variables table. + static $placement_map = NULL; + + $write_cache = FALSE; + + // Try first level cache. + if ($placement_map === NULL) { + // If first level cache missed: try second level cache. + if ($cache = \Drupal::cache()->get('captcha_placement_map_cache')) { + $placement_map = $cache->data; + } + else { + // If second level cache missed: initialize the placement map + // and let other modules hook into this with the + // hook_captcha_placement_map hook. + // By default however, probably all Drupal core forms + // are already correctly handled with the best effort guess + // based on the 'actions' element (see below). + $placement_map = \Drupal::moduleHandler() + ->invokeAll('captcha_placement_map'); + $write_cache = TRUE; + } + } + + // Query the placement map. + if (array_key_exists($form_id, $placement_map)) { + $placement = $placement_map[$form_id]; + } + // If no placement info is available in placement map: + // make a best effort guess. + else { + // If there is an "actions" button group, a good placement + // is just before that. + if (isset($form['actions']) && isset($form['actions']['#type']) && $form['actions']['#type'] === 'actions') { + $placement = [ + 'path' => [], + 'key' => 'actions', + // #type 'actions' defaults to 100. + 'weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99), + ]; + } + else { + // Search the form for buttons and guess placement from it. + $buttons = _captcha_search_buttons($form); + if (count($buttons)) { + // Pick first button. + // TODO: make this more sofisticated? Use cases needed. + $placement = $buttons[0]; + } + else { + // Use NULL when no buttons were found. + $placement = NULL; + } + } + + // Store calculated placement in cache. + $placement_map[$form_id] = $placement; + $write_cache = TRUE; + } + + if ($write_cache) { + \Drupal::cache()->set('captcha_placement_map_cache', $placement_map); + } + return $placement; +} + +/** + * Helper function for searching the buttons in a form. + * + * @param array $form + * The form to search button elements in. + * + * @return array + * Array of paths to the buttons. + * A path is an array of keys leading to the button, the last + * item in the path is the weight of the button element + * (or NULL if undefined). + */ +function _captcha_search_buttons(array $form) { + $buttons = []; + + foreach (Element::children($form, FALSE) as $key) { + // Look for submit or button type elements. + if (isset($form[$key]['#type']) && ($form[$key]['#type'] == 'submit' || $form[$key]['#type'] == 'button')) { + $weight = isset($form[$key]['#weight']) ? $form[$key]['#weight'] : NULL; + $buttons[] = [ + 'path' => [], + 'key' => $key, + 'weight' => $weight, + ]; + } + // Process children recursively. + $children_buttons = _captcha_search_buttons($form[$key]); + foreach ($children_buttons as $b) { + $b['path'] = array_merge([$key], $b['path']); + $buttons[] = $b; + } + } + + return $buttons; +} + +/** + * Helper function to insert a CAPTCHA element before a given form element. + * + * @param array $form + * the form to add the CAPTCHA element to. + * @param array $placement + * information where the CAPTCHA element should be inserted. + * $placement should be an associative array with fields: + * - 'path': path (array of path items) of the container in + * the form where the CAPTCHA element should be inserted. + * - 'key': the key of the element before which the CAPTCHA element + * should be inserted. If the field 'key' is undefined or NULL, + * the CAPTCHA will just be appended in the container. + * - 'weight': if 'key' is not NULL: should be the weight of the + * element defined by 'key'. If 'key' is NULL and weight is not NULL: + * set the weight property of the CAPTCHA element to this value. + * @param array $captcha_element + * the CAPTCHA element to insert. + */ +function _captcha_insert_captcha_element(array &$form, array $placement, array $captcha_element) { + // Get path, target and target weight or use defaults if not available. + $target_key = isset($placement['key']) ? $placement['key'] : NULL; + $target_weight = isset($placement['weight']) ? $placement['weight'] : NULL; + $path = isset($placement['path']) ? $placement['path'] : []; + + // Walk through the form along the path. + $form_stepper = &$form; + foreach ($path as $step) { + if (isset($form_stepper[$step])) { + $form_stepper = &$form_stepper[$step]; + } + else { + // Given path is invalid: stop stepping and + // continue in best effort (append instead of insert). + $target_key = NULL; + break; + } + } + + // If no target is available: just append the CAPTCHA element + // to the container. + if ($target_key == NULL || !array_key_exists($target_key, $form_stepper)) { + // Optionally, set weight of CAPTCHA element. + if ($target_weight != NULL) { + $captcha_element['#weight'] = $target_weight; + } + $form_stepper['captcha'] = $captcha_element; + } + // If there is a target available: make sure the CAPTCHA element + // comes right before it. + else { + // If target has a weight: set weight of CAPTCHA element a bit smaller + // and just append the CAPTCHA: sorting will fix the ordering anyway. + if ($target_weight != NULL) { + $captcha_element['#weight'] = $target_weight - .1; + $form_stepper['captcha'] = $captcha_element; + } + else { + // If we can't play with weights: insert the CAPTCHA element + // at the right position. Because PHP lacks a function for + // this (array_splice() comes close, but it does not preserve + // the key of the inserted element), we do it by hand: chop of + // the end, append the CAPTCHA element and put the end back. + $offset = array_search($target_key, array_keys($form_stepper)); + $end = array_splice($form_stepper, $offset); + $form_stepper['captcha'] = $captcha_element; + foreach ($end as $k => $v) { + $form_stepper[$k] = $v; + } + } + } +} diff --git a/web/modules/captcha/captcha.info.yml b/web/modules/captcha/captcha.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..48af451b28470a29b60e6c6ea3c747282a611618 --- /dev/null +++ b/web/modules/captcha/captcha.info.yml @@ -0,0 +1,15 @@ +name: CAPTCHA +type: module +description: Provides the CAPTCHA API for adding challenges to arbitrary forms. +package: Spam control +# core: 8.x +configure: captcha_settings + +dependencies: + - node + +# Information added by Drupal.org packaging script on 2017-02-15 +version: '8.x-1.0-beta1' +core: '8.x' +project: 'captcha' +datestamp: 1487198589 diff --git a/web/modules/captcha/captcha.install b/web/modules/captcha/captcha.install new file mode 100755 index 0000000000000000000000000000000000000000..a0d672d23b69ad222b327765090dd649eabf1be0 --- /dev/null +++ b/web/modules/captcha/captcha.install @@ -0,0 +1,132 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the CAPTCHA module. + */ + +/** + * Implements hook_schema(). + */ +function captcha_schema() { + $schema['captcha_sessions'] = [ + 'description' => 'Stores the data about CAPTCHA sessions (solution, IP address, timestamp, ...).', + 'fields' => [ + 'csid' => [ + 'description' => 'CAPTCHA session ID.', + 'type' => 'serial', + 'not null' => TRUE, + ], + 'token' => [ + 'description' => 'One time CAPTCHA token.', + 'type' => 'varchar', + 'length' => 64, + 'not null' => FALSE, + ], + 'uid' => [ + 'description' => "User's {users}.uid.", + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ], + 'sid' => [ + 'description' => "Session ID of the user.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ], + 'ip_address' => [ + 'description' => 'IP address of the visitor.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => FALSE, + ], + 'timestamp' => [ + 'description' => 'A Unix timestamp indicating when the challenge was generated.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ], + 'form_id' => [ + 'description' => 'The form_id of the form where the CAPTCHA is added to.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ], + 'solution' => [ + 'description' => 'Solution of the challenge.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ], + 'status' => [ + 'description' => 'Status of the CAPTCHA session (unsolved, solved, ...)', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ], + 'attempts' => [ + 'description' => 'The number of attempts.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ], + ], + 'primary key' => ['csid'], + 'indexes' => [ + 'csid_ip' => ['csid', 'ip_address'], + ], + ]; + + return $schema; +} + +/** + * Implements hook_requirements(). + */ +function captcha_requirements($phase) { + $requirements = []; + $config = \Drupal::config('captcha.settings'); + + if ($phase == 'runtime' && $config->get('enable_stats')) { + // Show the wrong response counter in the status report. + $requirements['captcha_wrong_response_counter'] = [ + 'title' => \Drupal::translation()->translate('CAPTCHA'), + 'value' => \Drupal::translation()->formatPlural( + \Drupal::state()->get('captcha.wrong_response_counter'), + 'Already 1 blocked form submission', + 'Already @count blocked form submissions' + ), + 'severity' => REQUIREMENT_INFO, + ]; + } + return $requirements; +} + +/** + * Implements hook_install(). + */ +function captcha_install() { + + if (!\Drupal::service('config.installer')->isSyncing()) { + $form_ids = []; + // Add form_ids of all currently known node types too. + foreach (node_type_get_names() as $type => $name) { + $form_ids[] = 'node_' . $type . '_form'; + } + + $captcha_storage = \Drupal::entityTypeManager() + ->getStorage('captcha_point'); + foreach ($form_ids as $form_id) { + $values = [ + 'formId' => $form_id, + 'captchaType' => 'default', + 'status' => FALSE, + ]; + $captcha_storage->create($values)->save(); + } + } + +} diff --git a/web/modules/captcha/captcha.links.action.yml b/web/modules/captcha/captcha.links.action.yml new file mode 100755 index 0000000000000000000000000000000000000000..03125f574018fb554cd3a6c5f4a9a1b84433f359 --- /dev/null +++ b/web/modules/captcha/captcha.links.action.yml @@ -0,0 +1,5 @@ +captcha_point.add: + route_name: 'captcha_point.add' + title: 'Add captcha point' + appears_on: + - captcha_point.list diff --git a/web/modules/captcha/captcha.links.menu.yml b/web/modules/captcha/captcha.links.menu.yml new file mode 100755 index 0000000000000000000000000000000000000000..e4ec77659143cbe18e26040838434b18e5bf6a12 --- /dev/null +++ b/web/modules/captcha/captcha.links.menu.yml @@ -0,0 +1,13 @@ +captcha.settings: + title: 'CAPTCHA module settings' + description: 'Administer how and where CAPTCHA is used.' + route_name: captcha_settings + parent: user.admin_index + weight: -1 + +captcha.examples: + title: 'CAPTCHA examples' + description: An overview of the available challenge types with examples.' + route_name: captcha_examples + parent: captcha.settings + weight: 0 diff --git a/web/modules/captcha/captcha.links.task.yml b/web/modules/captcha/captcha.links.task.yml new file mode 100755 index 0000000000000000000000000000000000000000..838cc19d8671c06adbc4069b01fdb127a5d3b05a --- /dev/null +++ b/web/modules/captcha/captcha.links.task.yml @@ -0,0 +1,14 @@ +captcha_settings: + route_name: captcha_settings + title: 'CAPTCHA Settings' + base_route: captcha_settings + +captcha_examples: + route_name: captcha_examples + title: 'CAPTCHA examples' + base_route: captcha_settings + +captcha_points.list: + route_name: captcha_point.list + title: 'CAPTCHA Points' + base_route: captcha_settings diff --git a/web/modules/captcha/captcha.module b/web/modules/captcha/captcha.module new file mode 100755 index 0000000000000000000000000000000000000000..fb940e9a8d6f29ec5a9124cac0dacc45d7424fee --- /dev/null +++ b/web/modules/captcha/captcha.module @@ -0,0 +1,576 @@ +<?php + +/** + * @file + * This module enables basic CAPTCHA functionality. + * + * Administrators can add a CAPTCHA to desired forms that users without + * the 'skip CAPTCHA' permission (typically anonymous visitors) have + * to solve. + */ + +use Drupal\captcha\Entity\CaptchaPoint; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Database\Database; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Link; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; + +/** + * Constants for CAPTCHA persistence. + * + * TODO: change these integers to strings because the CAPTCHA settings + * form saves them as strings in the variables table anyway? + */ + +// @TODO: move all constants to some class. +// Always add a CAPTCHA (even on every page of a multipage workflow). +define('CAPTCHA_PERSISTENCE_SHOW_ALWAYS', 0); +// Only one CAPTCHA has to be solved per form instance/multi-step workflow. +define('CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE', 1); +// Once the user answered correctly for a CAPTCHA on a certain form type, +// no more CAPTCHAs will be offered anymore for that form. +define('CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE', 2); +// Once the user answered correctly for a CAPTCHA on the site, +// no more CAPTCHAs will be offered anymore. +define('CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL', 3); + +define('CAPTCHA_STATUS_UNSOLVED', 0); +define('CAPTCHA_STATUS_SOLVED', 1); +define('CAPTCHA_STATUS_EXAMPLE', 2); + +define('CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE', 0); +define('CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE', 1); + +/** + * Implements hook_help(). + */ +function captcha_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.captcha': + $output = '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('"CAPTCHA" is an acronym for "Completely Automated Public Turing test to tell Computers and Humans Apart". It is typically a challenge-response test to determine whether the user is human. The CAPTCHA module is a tool to fight automated submission by malicious users (spamming) of for example comments forms, user registration forms, guestbook forms, etc. You can extend the desired forms with an additional challenge, which should be easy for a human to solve correctly, but hard enough to keep automated scripts and spam bots out.') . '</p>'; + $output .= '<p>' . t('Note that the CAPTCHA module interacts with page caching (see <a href=":performancesettings">performance settings</a>). Because the challenge should be unique for each generated form, the caching of the page it appears on is prevented. Make sure that these forms do not appear on too many pages or you will lose much caching efficiency. For example, if you put a CAPTCHA on the user login block, which typically appears on each page for anonymous visitors, caching will practically be disabled. The comment submission forms are another example. In this case you should set the <em>Location of comment submission form</em> to <em>Display on separate page</em> in the comment settings of the relevant <a href=":contenttypes">content types</a> for better caching efficiency.', [ + ':performancesettings' => \Drupal::url('system.performance_settings'), + ':contenttypes' => \Drupal::url('entity.node_type.collection'), + ]) . '</p>'; + $output .= '<p>' . t('CAPTCHA is a trademark of Carnegie Mellon University.') . '</p>'; + return ['#markup' => $output]; + + case 'captcha_settings': + $output = '<p>' . t('A CAPTCHA can be added to virtually each Drupal form. Some default forms are already provided in the form list, but arbitrary forms can be easily added and managed when the option <em>Add CAPTCHA administration links to forms</em> is enabled.') . '</p>'; + $output .= '<p>' . t('Users with the <em>Skip CAPTCHA</em> <a href=":perm">permission</a> won\'t be offered a challenge. Be sure to grant this permission to the trusted users (e.g. site administrators). If you want to test a protected form, be sure to do it as a user without the <em>Skip CAPTCHA</em> permission (e.g. as anonymous user).', [ + ':perm' => \Drupal::url('user.admin_permissions'), + ]) . '</p>'; + $output .= '<p><b>' . t('Note that the CAPTCHA module disables <a href=":performancesettings">page caching</a> of pages that include a CAPTCHA challenge.', [ + ':performancesettings' => \Drupal::url('system.performance_settings'), + ]) . '</b></p>'; + return ['#markup' => $output]; + } +} + +/** + * Loader for Captcha Point entity. + * + * @param string $id + * Form id string. + * + * @return \Drupal\Core\Entity\EntityInterface + * An instance of an captcha_point entity. + */ +function captcha_point_load($id) { + return CaptchaPoint::load($id); +} + +/** + * Implements hook_theme(). + */ +function captcha_theme() { + return [ + 'captcha' => [ + 'render element' => 'element', + 'template' => 'captcha', + 'path' => drupal_get_path('module', 'captcha') . '/templates', + ], + ]; +} + +/** + * Implements hook_cron(). + * + * Remove old entries from captcha_sessions table. + */ +function captcha_cron() { + // Remove challenges older than 1 day. + $connection = Database::getConnection(); + $connection->delete('captcha_sessions') + ->condition('timestamp', REQUEST_TIME - 60 * 60 * 24, '<') + ->execute(); +} + +/** + * Theme function for a CAPTCHA element. + * + * Render it in a section element if a description of the CAPTCHA + * is available. Render it as is otherwise. + */ +function template_preprocess_captcha(&$variables) { + $element = $variables['element']; + + if (!empty($element['#description']) && isset($element['captcha_widgets'])) { + $variables['details'] = [ + '#type' => 'details', + '#title' => t('CAPTCHA'), + '#description' => $element['#description'], + '#children' => drupal_render_children($element), + '#attributes' => [ + 'class' => ['captcha'], + 'open' => [''], + ], + '#open' => TRUE, + ]; + } +} + +/** + * Implements hook_form_alter(). + * + * This function adds a CAPTCHA to forms for untrusted users + * if needed and adds. CAPTCHA administration links for site + * administrators if this option is enabled. + */ +function captcha_form_alter(array &$form, FormStateInterface $form_state, $form_id) { + $account = \Drupal::currentUser(); + $config = \Drupal::config('captcha.settings'); + // Visitor does not have permission to skip CAPTCHAs. + module_load_include('inc', 'captcha'); + if (!$account->hasPermission('skip CAPTCHA')) { + + /* @var CaptchaPoint $captcha_point */ + $captcha_point = \Drupal::entityTypeManager() + ->getStorage('captcha_point') + ->load($form_id); + + if ($captcha_point && $captcha_point->status()) { + // Build CAPTCHA form element. + $captcha_element = [ + '#type' => 'captcha', + '#captcha_type' => $captcha_point->getCaptchaType(), + ]; + + // Add a CAPTCHA description if required. + if ($config->get('add_captcha_description')) { + $captcha_element['#description'] = _captcha_get_description(); + } + + // Get placement in form and insert in form. + $captcha_placement = _captcha_get_captcha_placement($form_id, $form); + _captcha_insert_captcha_element($form, $captcha_placement, $captcha_element); + + } + } + elseif ($config->get('administration_mode') && $account->hasPermission('administer CAPTCHA settings') + && (!\Drupal::service('router.admin_context') + ->isAdminRoute() || $config->get('allow_on_admin_pages')) + ) { + // Add CAPTCHA administration tools. + /* @var \Drupal\captcha\Entity\CaptchaPoint $captcha_point */ + $captcha_point = CaptchaPoint::load($form_id); + + // For administrators: show CAPTCHA info and offer link to configure it. + $captcha_element = [ + '#type' => 'details', + '#title' => t('CAPTCHA'), + '#attributes' => [ + 'class' => ['captcha-admin-links'], + ], + '#open' => TRUE, + ]; + + if ($captcha_point !== NULL && $captcha_point->getCaptchaType()) { + $captcha_element['#title'] = $captcha_point->status() ? t('CAPTCHA: challenge "@type" enabled', ['@type' => $captcha_point->getCaptchaType()]) : t('CAPTCHA: challenge "@type" disabled', ['@type' => $captcha_point->getCaptchaType()]); + $captcha_point->status() ? $captcha_element['#description'] = t('Untrusted users will see a CAPTCHA here (<a href="@settings">general CAPTCHA settings</a>).', + [ + '@settings' => Url::fromRoute('captcha_settings') + ->toString(), + ]) : $captcha_element['#description'] = t('CAPTCHA disabled, Untrusted users won\'t see the captcha (<a href="@settings">general CAPTCHA settings</a>).', + ['@settings' => Url::fromRoute('captcha_settings')->toString()] + ); + $captcha_element['challenge'] = [ + '#type' => 'item', + '#title' => t('Enabled challenge'), + '#markup' => t('<a href="@change">change</a>', [ + '@change' => $captcha_point->url('edit-form', [ + 'query' => Drupal::destination() + ->getAsArray(), + ]), + ]), + ]; + } + else { + $captcha_element['#title'] = t('CAPTCHA: no challenge enabled'); + $captcha_element['add_captcha'] = [ + '#markup' => Link::fromTextAndUrl( + t('Place a CAPTCHA here for untrusted users.'), + Url::fromRoute('captcha_point.add', [], [ + 'query' => Drupal::destination() + ->getAsArray() + ['form_id' => $form_id], + ]) + )->toString(), + ]; + } + + // Get placement in form and insert in form. + if ($captcha_placement = _captcha_get_captcha_placement($form_id, $form)) { + _captcha_insert_captcha_element($form, $captcha_placement, $captcha_element); + }; + } + + // Add a warning about caching on the Performance settings page. + if ($form_id == 'system_performance_settings') { + $form['caching']['captcha'] = [ + '#type' => 'item', + '#title' => t('CAPTCHA'), + '#markup' => '<div class="messages messages--warning">' . t('The CAPTCHA module will disable the caching of pages that contain a CAPTCHA element.') . '</div>', + ]; + } +} + +/** + * CAPTCHA validation function to tests strict equality. + * + * @param string $solution + * The solution of the test. + * @param string $response + * The response to the test. + * + * @return bool + * TRUE when case insensitive equal, FALSE otherwise. + */ +function captcha_validate_strict_equality($solution, $response) { + return $solution === $response; +} + +/** + * CAPTCHA validation function to tests case insensitive equality. + * + * @param string $solution + * The solution of the test. + * @param string $response + * The response to the test. + * + * @return bool + * TRUE when case insensitive equal, FALSE otherwise. + */ +function captcha_validate_case_insensitive_equality($solution, $response) { + return Unicode::strtolower($solution) === Unicode::strtolower($response); +} + +/** + * CAPTCHA validation function to tests equality while ignoring spaces. + * + * @param string $solution + * The solution of the test. + * @param string $response + * The response to the test. + * + * @return bool + * TRUE when equal (ignoring spaces), FALSE otherwise. + */ +function captcha_validate_ignore_spaces($solution, $response) { + return preg_replace('/\s/', '', $solution) === preg_replace('/\s/', '', $response); +} + +/** + * Validation function to tests case insensitive equality while ignoring spaces. + * + * @param string $solution + * The solution of the test. + * @param string $response + * The response to the test. + * + * @return bool + * TRUE when equal (ignoring spaces), FALSE otherwise. + */ +function captcha_validate_case_insensitive_ignore_spaces($solution, $response) { + return preg_replace('/\s/', '', Unicode::strtolower($solution)) === preg_replace('/\s/', '', Unicode::strtolower($response)); +} + +/** + * Helper function for getting the posted CAPTCHA info. + * + * This function hides the form processing mess for several use cases an + * browser bug workarounds. + * For example: $element['#post'] can typically be used to get the posted + * form_id and captcha_sid, but in the case of node preview situations + * (with correct CAPTCHA response) that does not work and we can get them from + * $form_state['clicked_button']['#post']. + * However with Internet Explorer 7, the latter does not work either when + * submitting the forms (with only one text field) with the enter key + * (see http://drupal.org/node/534168), in which case we also have to check + * $form_state['buttons']['button']['0']['#post']. + * + * @param array $element + * The CAPTCHA element. + * @param FormStateInterface $form_state + * The form state structure to extract the info from. + * @param string $this_form_id + * The form ID of the form we are currently processing + * (which is not necessarily the form that was posted). + * + * @return array + * Array with $posted_form_id and $post_captcha_sid (with NULL values + * if the values could not be found, e.g. for a fresh form). + */ +function _captcha_get_posted_captcha_info(array $element, FormStateInterface $form_state, $this_form_id) { + if ($form_state->isSubmitted() && $form_state->has('captcha_info')) { + // We are handling (or rebuilding) an already submitted form, + // so we already determined the posted form ID and CAPTCHA session ID + // for this form (from before submitting). Reuse this info. + $posted_form_id = $form_state->get('captcha_info')['posted_form_id']; + $posted_captcha_sid = $form_state->get('captcha_info')['captcha_sid']; + } + else { + // We have to determine the posted form ID and CAPTCHA session ID + // from the post data. + // Because we possibly use raw post data here, + // we should be extra cautious and filter this data. + $input = &$form_state->getUserInput(); + $posted_form_id = isset($input['form_id']) ? + preg_replace("/[^a-z0-9_]/", "", (string) $input['form_id']) + : NULL; + $posted_captcha_sid = isset($input['captcha_sid']) ? + (int) $input['captcha_sid'] + : NULL; + $posted_captcha_token = isset($input['captcha_token']) ? + preg_replace("/[^a-zA-Z0-9]/", "", (string) $input['captcha_token']) + : NULL; + + if ($posted_form_id == $this_form_id) { + // Check if the posted CAPTCHA token is valid for the posted CAPTCHA + // session ID. Note that we could just check the validity of the CAPTCHA + // token and extract the CAPTCHA session ID from that (without looking at + // the actual posted CAPTCHA session ID). However, here we check both + // the posted CAPTCHA token and session ID: it is a bit more stringent + // and the database query should also be more efficient (because there is + // an index on the CAPTCHA session ID). + if ($posted_captcha_sid != NULL) { + $expected_captcha_token = db_query( + "SELECT token FROM {captcha_sessions} WHERE csid = :csid", + [':csid' => $posted_captcha_sid] + )->fetchField(); + if ($expected_captcha_token !== $posted_captcha_token) { + drupal_set_message(t('CAPTCHA session reuse attack detected.'), 'error'); + // Invalidate the CAPTCHA session. + $posted_captcha_sid = NULL; + } + // Invalidate CAPTCHA token to avoid reuse. + db_update('captcha_sessions') + ->fields(['token' => NULL]) + ->condition('csid', $posted_captcha_sid); + } + } + else { + // The CAPTCHA session ID is specific to the posted form. + // Return NULL, so a new session will be generated for this other form. + $posted_captcha_sid = NULL; + } + } + return [$posted_form_id, $posted_captcha_sid]; +} + +/** + * CAPTCHA validation handler. + * + * This function is placed in the main captcha.module file to make sure that + * it is available (even for cached forms, which don't fire + * captcha_form_alter(), and subsequently don't include additional include + * files). + */ +function captcha_validate($element, FormStateInterface &$form_state) { + + $captcha_info = $form_state->get('captcha_info'); + $form_id = $captcha_info['this_form_id']; + + // Get CAPTCHA response. + $captcha_response = $form_state->getValue('captcha_response'); + + // Get CAPTCHA session from CAPTCHA info + // TODO: is this correct in all cases: see comments in previous revisions? + $csid = $captcha_info['captcha_sid']; + + $solution = db_query( + 'SELECT solution FROM {captcha_sessions} WHERE csid = :csid', + [':csid' => $csid] + ) + ->fetchField(); + + // @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. + $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)) { + // Correct answer. + $_SESSION['captcha_success_form_ids'][$form_id] = $form_id; + // Record success. + db_update('captcha_sessions') + ->condition('csid', $csid) + ->fields(['status' => CAPTCHA_STATUS_SOLVED]) + ->expression('attempts', 'attempts + 1') + ->execute(); + } + else { + // Wrong answer. + db_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.')); + // 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'], + ]); + } + } + } +} + +/** + * Pre-render callback for additional processing of a CAPTCHA form element. + * + * This encompasses tasks that should happen after the general FAPI processing + * (building, submission and validation) but before rendering + * (e.g. storing the solution). + * + * @param array $element + * The CAPTCHA form element. + * + * @return array + * The manipulated element. + */ +function captcha_pre_render_process(array $element) { + module_load_include('inc', 'captcha'); + + // Get form and CAPTCHA information. + $captcha_info = $element['#captcha_info']; + $form_id = $captcha_info['form_id']; + $captcha_sid = (int) ($captcha_info['captcha_sid']); + // Check if CAPTCHA is still required. + // This check is done in a first phase during the element processing + // (@see captcha_process), but it is also done here for better support + // of multi-page forms. Take previewing a node submission for example: + // when the challenge is solved correctely on preview, the form is still + // not completely submitted, but the CAPTCHA can be skipped. + if (_captcha_required_for_user($captcha_sid, $form_id) || $element['#captcha_admin_mode']) { + // Update captcha_sessions table: store the solution + // of the generated CAPTCHA. + _captcha_update_captcha_session($captcha_sid, $captcha_info['solution']); + + // Handle the response field if it is available and if it is a textfield. + if (isset($element['captcha_widgets']['captcha_response']['#type']) && $element['captcha_widgets']['captcha_response']['#type'] == 'textfield') { + // Before rendering: presolve an admin mode challenge or + // empty the value of the captcha_response form item. + $value = $element['#captcha_admin_mode'] ? $captcha_info['solution'] : ''; + $element['captcha_widgets']['captcha_response']['#value'] = $value; + } + } + else { + // Remove CAPTCHA widgets from form. + unset($element['captcha_widgets']); + } + + return $element; +} + +/** + * Default implementation of hook_captcha(). + */ +function captcha_captcha($op, $captcha_type = '') { + switch ($op) { + case 'list': + return ['Math']; + + case 'generate': + if ($captcha_type == 'Math') { + $result = []; + $answer = mt_rand(1, 20); + $x = mt_rand(1, $answer); + $y = $answer - $x; + $result['solution'] = "$answer"; + // Build challenge widget. + // Note that we also use t() for the math challenge itself. This makes + // it possible to 'rephrase' the challenge a bit through localization + // or string overrides. + $result['form']['captcha_response'] = [ + '#type' => 'textfield', + '#title' => t('Math question'), + '#description' => t('Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.'), + '#field_prefix' => t('@x + @y =', ['@x' => $x, '@y' => $y]), + '#size' => 4, + '#maxlength' => 2, + '#required' => TRUE, + '#attributes' => [ + 'autocomplete' => 'off', + ], + '#cache' => ['max-age' => 0], + ]; + \Drupal::service('page_cache_kill_switch')->trigger(); + + return $result; + } + elseif ($captcha_type == 'Test') { + // This challenge is not visible through the administrative interface + // as it is not listed in captcha_captcha('list'), + // but it is meant for debugging and testing purposes. + // TODO for Drupal 7 version: This should be done with a mock module, + // but Drupal 6 does not support this (mock modules can not be hidden). + $result = [ + 'solution' => 'Test 123', + 'form' => [], + ]; + $result['form']['captcha_response'] = [ + '#type' => 'textfield', + '#title' => t('Test one two three'), + '#required' => TRUE, + '#cache' => ['max-age' => 0], + ]; + \Drupal::service('page_cache_kill_switch')->trigger(); + + return $result; + } + break; + } +} diff --git a/web/modules/captcha/captcha.permissions.yml b/web/modules/captcha/captcha.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..f745e29349d540cc75d3d022a0cfd340de9d1810 --- /dev/null +++ b/web/modules/captcha/captcha.permissions.yml @@ -0,0 +1,5 @@ +administer CAPTCHA settings: + title: 'Administer CAPTCHA settings' +skip CAPTCHA: + title: 'Skip CAPTCHA' + description: 'Users with this permission will not be offered a CAPTCHA.' diff --git a/web/modules/captcha/captcha.routing.yml b/web/modules/captcha/captcha.routing.yml new file mode 100755 index 0000000000000000000000000000000000000000..f653505242c487b35faf21413c39f6c660ccd4a0 --- /dev/null +++ b/web/modules/captcha/captcha.routing.yml @@ -0,0 +1,72 @@ +captcha_settings: + path: '/admin/config/people/captcha' + defaults: + _form: '\Drupal\captcha\Form\CaptchaSettingsForm' + _title: 'CAPTCHA settings' + requirements: + _permission: 'administer CAPTCHA settings' + +captcha_examples: + path: '/admin/config/people/captcha/examples/{module}/{challenge}' + defaults: + _form: '\Drupal\captcha\Form\CaptchaExamplesForm' + module: '' + challenge: '' + requirements: + _permission: 'administer CAPTCHA settings' + +captcha_point.list: + path: '/admin/config/people/captcha/captcha-points' + defaults: + _entity_list: 'captcha_point' + _title: 'CAPTCHA configuration' + requirements: + _permission: 'administer CAPTCHA settings' + +captcha_point.add: + path: '/admin/config/people/captcha/captcha-points/add' + defaults: + _entity_form: 'captcha_point.add' + _title: 'Add CAPTCHA point' + requirements: + _permission: 'administer CAPTCHA settings' + +entity.captcha_point.edit_form: + path: '/admin/config/people/captcha/captcha-points/{captcha_point}' + defaults: + _entity_form: 'captcha_point.edit' + _title: 'Edit CAPTCHA point' + options: + _admin_route: TRUE + requirements: + _permission: 'administer CAPTCHA settings' + +entity.captcha_point.disable: + path: '/admin/config/people/captcha/captcha-points/{captcha_point}/disable' + defaults: + _entity_form: 'captcha_point.disable' + _title: 'Disable CAPTCHA point' + options: + _admin_route: TRUE + requirements: + _entity_access: 'captcha_point.update' + +entity.captcha_point.enable: + path: '/admin/config/people/captcha/captcha-points/{captcha_point}/enable' + defaults: + _entity_form: 'captcha_point.enable' + _title: 'Enable CAPTCHA point' + options: + _admin_route: TRUE + requirements: + _entity_access: 'captcha_point.update' + +entity.captcha_point.delete_form: + path: '/admin/config/people/captcha/captcha-points/{captcha_point}/delete' + defaults: + _entity_form: 'captcha_point.delete' + _title: 'Delete CAPTCHA point' + options: + _admin_route: TRUE + requirements: + _permission: 'administer CAPTCHA settings' diff --git a/web/modules/captcha/captcha.services.yml b/web/modules/captcha/captcha.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..53103203f0e91bc333928238c491b9241165fc22 --- /dev/null +++ b/web/modules/captcha/captcha.services.yml @@ -0,0 +1,5 @@ +services: + captcha.config_subscriber: + class: Drupal\captcha\EventSubscriber\CaptchaCachedSettingsSubscriber + tags: + - { name: event_subscriber } diff --git a/web/modules/captcha/composer.json b/web/modules/captcha/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..91e10588ca0cd99e1f80350062795d0d8a1e53af --- /dev/null +++ b/web/modules/captcha/composer.json @@ -0,0 +1,5 @@ +{ + "name": "drupal/captcha", + "description": "The CAPTCHA module provides this feature to virtually any user facing web form on a Drupal site.", + "type": "drupal-module" +} diff --git a/web/modules/captcha/config/install/captcha.captcha_point.contact_message_feedback_form.yml b/web/modules/captcha/config/install/captcha.captcha_point.contact_message_feedback_form.yml new file mode 100644 index 0000000000000000000000000000000000000000..a78a314f7fccd2f6db80fd3f838697c259b18769 --- /dev/null +++ b/web/modules/captcha/config/install/captcha.captcha_point.contact_message_feedback_form.yml @@ -0,0 +1,6 @@ +langcode: en +status: false +dependencies: { } +formId: contact_message_feedback_form +captchaType: default +label: null diff --git a/web/modules/captcha/config/install/captcha.captcha_point.contact_message_personal_form.yml b/web/modules/captcha/config/install/captcha.captcha_point.contact_message_personal_form.yml new file mode 100644 index 0000000000000000000000000000000000000000..29c1fab1dfdb6ab327dde3826358865bbdedbcf7 --- /dev/null +++ b/web/modules/captcha/config/install/captcha.captcha_point.contact_message_personal_form.yml @@ -0,0 +1,6 @@ +langcode: en +status: false +dependencies: { } +formId: contact_message_personal_form +captchaType: default +label: null diff --git a/web/modules/captcha/config/install/captcha.captcha_point.user_login_form.yml b/web/modules/captcha/config/install/captcha.captcha_point.user_login_form.yml new file mode 100644 index 0000000000000000000000000000000000000000..5e060c131c0cc4e5e7c05c1ee33757a751d9d33b --- /dev/null +++ b/web/modules/captcha/config/install/captcha.captcha_point.user_login_form.yml @@ -0,0 +1,6 @@ +langcode: en +status: false +dependencies: { } +formId: user_login_form +captchaType: default +label: null diff --git a/web/modules/captcha/config/install/captcha.captcha_point.user_pass.yml b/web/modules/captcha/config/install/captcha.captcha_point.user_pass.yml new file mode 100644 index 0000000000000000000000000000000000000000..c1feba4c3f677dc4e84cad599f19e50f5bd850e8 --- /dev/null +++ b/web/modules/captcha/config/install/captcha.captcha_point.user_pass.yml @@ -0,0 +1,6 @@ +langcode: en +status: false +dependencies: { } +formId: user_pass +captchaType: default +label: null diff --git a/web/modules/captcha/config/install/captcha.captcha_point.user_register_form.yml b/web/modules/captcha/config/install/captcha.captcha_point.user_register_form.yml new file mode 100644 index 0000000000000000000000000000000000000000..1d0ad7a738dd87d7a8a52b7db3c52b51b5a0c945 --- /dev/null +++ b/web/modules/captcha/config/install/captcha.captcha_point.user_register_form.yml @@ -0,0 +1,6 @@ +langcode: en +status: false +dependencies: { } +formId: user_register_form +captchaType: default +label: null diff --git a/web/modules/captcha/config/install/captcha.settings.yml b/web/modules/captcha/config/install/captcha.settings.yml new file mode 100755 index 0000000000000000000000000000000000000000..ac797b0a86f57cd0c1ffd22f23e12ead43910c98 --- /dev/null +++ b/web/modules/captcha/config/install/captcha.settings.yml @@ -0,0 +1,9 @@ +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: false +allow_on_admin_pages: false +add_captcha_description: true +default_validation: 1 +persistence: 1 +enable_stats: false +log_wrong_responses: false diff --git a/web/modules/captcha/config/schema/captcha.schema.yml b/web/modules/captcha/config/schema/captcha.schema.yml new file mode 100755 index 0000000000000000000000000000000000000000..33a92d80a418edde06eb0ae97e02596f4327b505 --- /dev/null +++ b/web/modules/captcha/config/schema/captcha.schema.yml @@ -0,0 +1,16 @@ +captcha.captcha_point.*: + type: config_entity + label: 'CAPTCHA point' + mapping: + formId: + type: string + label: 'Form ID' + captchaType: + type: string + label: 'Captcha Type' + label: + type: label + label: 'Label' + uuid: + type: string + label: 'UUID' diff --git a/web/modules/captcha/config/schema/captcha.settings.yml b/web/modules/captcha/config/schema/captcha.settings.yml new file mode 100755 index 0000000000000000000000000000000000000000..3cdcddb10c1d33e16ebefcf4caa741c53f4b738c --- /dev/null +++ b/web/modules/captcha/config/schema/captcha.settings.yml @@ -0,0 +1,34 @@ +# Schema for the configuration files of the captcha module. +#wrong_response_counter + +captcha.settings: + type: config_object + label: 'External Links Settings' + mapping: + default_challenge: + type: string + label: 'The default challenge for captcha.' + description: + type: label + label: 'The default captcha description.' + administration_mode: + type: boolean + label: 'Add CAPTCHA administration links to forms' + allow_on_admin_pages: + type: boolean + label: 'Allow CAPTCHAs and CAPTCHA administration links on administrative pages' + add_captcha_description: + type: boolean + label: 'Add a description to the CAPTCHA' + default_validation: + type: integer + label: 'Default CAPTCHA validation' + persistence: + type: integer + label: 'CAPTCHA Persistence' + enable_stats: + type: boolean + label: 'Enable statistics' + log_wrong_responses: + type: boolean + label: 'Log wrong responses' diff --git a/web/modules/captcha/image_captcha/config/install/image_captcha.settings.yml b/web/modules/captcha/image_captcha/config/install/image_captcha.settings.yml new file mode 100755 index 0000000000000000000000000000000000000000..dad05116334fd2e0cad5d2d61b352d3fd0405975 --- /dev/null +++ b/web/modules/captcha/image_captcha/config/install/image_captcha.settings.yml @@ -0,0 +1,16 @@ +image_captcha_fonts_preview_map_cache: [] +image_captcha_fonts: ['BUILTIN'] +image_captcha_font_size: 30 +image_captcha_character_spacing: '1.2' +image_captcha_image_allowed_chars: 'aAbBCdEeFfGHhijKLMmNPQRrSTtWXYZ23456789' +image_captcha_code_length: 5 +image_captcha_rtl_support: 0 +image_captcha_background_color: '#ffffff' +image_captcha_foreground_color: '#000000' +image_captcha_foreground_color_randomness: 100 +image_captcha_file_format: 1 +image_captcha_distortion_amplitude: 0 +image_captcha_bilinear_interpolation: 0 +image_captcha_dot_noise: 0 +image_captcha_line_noise: 0 +image_captcha_noise_level: 5 diff --git a/web/modules/captcha/image_captcha/config/schema/image_captcha.settings.yml b/web/modules/captcha/image_captcha/config/schema/image_captcha.settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..7951e5fe1b805da7fce066191eb0348a069d6c9c --- /dev/null +++ b/web/modules/captcha/image_captcha/config/schema/image_captcha.settings.yml @@ -0,0 +1,55 @@ +image_captcha.settings: + type: config_object + label: 'Image Captcha settings' + mapping: + image_captcha_fonts_preview_map_cache: + type: sequence + label: 'Font preview map cache' + image_captcha_fonts: + type: sequence + label: Fonts + sequence: + type: string + label: 'Font filepath' + image_captcha_font_size: + type: integer + label: 'Font Size in Image Captcha' + image_captcha_character_spacing: + type: string + label: 'Spacing between characters' + image_captcha_image_allowed_chars: + type: string + label: 'String with allowed characters' + image_captcha_code_length: + type: integer + label: 'Captcha code length' + image_captcha_rtl_support: + type: integer + label: 'Right to left support' + image_captcha_background_color: + type: string + label: 'Hexadecimal background color code' + image_captcha_foreground_color: + type: string + label: 'Hexadecimal foreground color code' + image_captcha_foreground_color_randomness: + type: integer + label: 'Background color randomness' + image_captcha_file_format: + type: integer + label: 'File format' + image_captcha_distortion_amplitude: + type: integer + label: 'Distortion amplitude' + image_captcha_bilinear_interpolation: + type: integer + label: 'Bilinear interpolation' + image_captcha_dot_noise: + type: integer + label: 'Dot noise' + image_captcha_line_noise: + type: integer + label: 'Line noise' + image_captcha_noise_level: + type: integer + label: 'Noise level' diff --git a/web/modules/captcha/image_captcha/fonts/README.txt b/web/modules/captcha/image_captcha/fonts/README.txt new file mode 100755 index 0000000000000000000000000000000000000000..452586d7478ed71de2832187fd00131cb58d2b75 --- /dev/null +++ b/web/modules/captcha/image_captcha/fonts/README.txt @@ -0,0 +1,7 @@ + +It possible to put your own fonts for the Image CAPTCHA in this folder. +However, this is not the recommended way, as they can get lost easily during +a module update. The recommended way to provide your own fonts is putting them +in the files directory of your Drupal setup or, just like with contributed +modules and themes, in the "libraries" folders sites/all/libraries/fonts +or sites/<site>/libraries/fonts. diff --git a/web/modules/captcha/image_captcha/fonts/Tesox/tesox.ttf b/web/modules/captcha/image_captcha/fonts/Tesox/tesox.ttf new file mode 100755 index 0000000000000000000000000000000000000000..31f91d34913e2e7ba43e3af1468a494371024b82 --- /dev/null +++ b/web/modules/captcha/image_captcha/fonts/Tesox/tesox.ttf @@ -0,0 +1,462 @@ +���� ����PFFTMS�;��������OS/2����X���Vcmapv�������2cvt �D�������gasp����������glyfX�+�����Dhead����������6hhea�r�����$hmtx�������loca&I,����maxp��-��8��� name�������X��@post�G9�������=������$��M_<�����������������������- ������������� ���ZP����-��������������������������7���������@�.�������������������1�����������������������fc���@� �%�%�Z f�����������D�����������M��D��<�#����.��\ ���[ ���>��<��<��<��?��C`����.Y�E���(z�V�"��&��>��B��<��F��Q=�`�� s�#b�MJ�j�V����)-�H����� k�������=]�h�!�,��#��K�(3����!P���!�� ����9��n��8E�<��9��&q���<s� ��=����%p�n�'��&�5� ��x��5��&��!�*�*�\,�V��0��(��,� '�}�4������;�,�� ���2�-���� ����#7��'3�k���;��7��8W���=~�-��z�8C�1�=N�.,�(��1O�&r���D�%���� K� ��5b���*N�3��\��DA�5��������������,���������@�@����~������������������������������������������������������ ������ ������������������������������������������������������ ������������������������������������������������������������k�i�������������������������������������������������������������������������������������������������� + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a�lm�oqsvx��y�z�|���}���~����������de���w���h��nt�g������������{�jc�����fi�b����������������������������k��p���r���u����������������D����,�,�,�,�����~��0\��,Pp�6��� D +P�� B x �Z����R +�L�b����0����N� �!�"�#�$�%�%@%�%�%�&&&�&�'<'�((�)8)�)�*\*�+8+�,,f,�- -f-�.6.�.�/�/�0�0�1P1�1�262>2�33�4>4�4�545�6N6�7�8~99�:d;;�<v<�=v> +>�?H?�@�@�A\BBdB�CLC�DVD^DfE"��D��d%���.��/<���2��<���2���/<���2��<���23!%!!D �$��h%��D�����M��� ��:��76#"&7>766!2'.76&'.'&'.� S"@)"#��/.'!!* +8 +9F $!a/.2oP:"�%.)8/Lo<iB2g8oECl/�?62-?�m���D�[��6��6'.4&'.'&.6%>32'.54&'&6�L@&4 + +&PJ��*) + +&�BJH�+�:&/9$R^zI" 4- +�P& [57^ ���<���7�n�z����627>32+&'&6?+#"&76?#"&'.54676'.7>7632654'&47>32>&'&66&#"7>%7+7>�Z <Id'y`S2+UF s@9 yOO,!% > �XD6#0Cj.[ +P %�v(!)8"PD�`a\ +7S/�K&& %f,&3/S=&dcj.K�ZCLL�!.Thj,, # AeN%" ,NN� G*eo,!%\��?%B14389 ���#�'� �k�u�����>32?;32#""&54676&#"&'.764'&'.7>67>7>.'.5467>546'&6'6'&76.#"32>76&|&> +rq +"'H +(/ 6 + +:4*"JM<")Z ��[ 5bY�!60[D%� ��1 + + (* +�$334G9<!&�]�k]/K@;%K(g/*qQ6"BAG"@)K!%'.D^+'I3& 1$�8{#!cMV� +��� ��� �N qMi;+FKVA���5Zq��"�_��6#".546?>&32662'.7>7>'&#"&'.546763276?J@�=�,/(&1`GQ�Z$>1J��I K%'/+44>TZOW,+;'!�6%x5/1F<6&+�+ �"(+�!$m + e2';j/,�%07w./)0?L8Ee�̞QY98 L+�;(�/.;e{�':'�����.�.M��J�W�u��6327>#"'&'.'.#"&'&767>4&'.5467>6&67>.'&'"&6767>'&�(q`n (9� +=8]�4N'B%:Anm +NxM[�'&- b?B"-<% � +@4�8>(�I8��0-) !�\1a!:N"�0$N3. !=:?\2VLbWD!=�` + +z#P��<%%'J>!/����\5D���>32&54&'&�4IC +T�#G�:&WA�����,�`�)��6&'&/767>32U89!.-#! )-e[&o|YY_#%C + >, +Sl�JV=�t + !]Lҟ��+���[�H�����6 +'.76'.'&^<;KZ1>(Y8B?m:9"$B�7EZ�X�����UKOc���wk���9�R�6��67632'.#"&7676&'.5467>'.u"0889 " !5#5=47 424 :9 *11190 + ++Cf{H;&'<""$&��>��-��-��6'.57'.'&67>54>�K (@8�8Mf?(0 +E4� 2[� �.X:}(/ +(>*M/:V� <�-)% ���<�5�`���625467>'.&546�X5 '$'�&YF4-5IA`��1 X?*!U=) .)u�����<}M`���>".54676]� ��>L+;[E (4# "L����<�6����6&'.546�]Abx* 3 c&"&#'9/�����?�/0�%��>'&>764?>7>�<F_@H1P+(' +K 4b/3Yl(42�*} �V`_�o#u/V��IN)��c��C��-�B�W��>3267>5&67>32&'.'&67>326.3267>�$$A_^-K ,-]&+2�-128(5DWN�0)Kr>8(2{�, X16A") <^U9 +:S$_�##ac� +�]>R]fPc9ROr.8���F?�#I'8#fު[ʆ�## *&��:�{ҽ�������JC��]�g�q�y����636#+ +&'&'.676?567>7>7>?#"'&'.&54676$6'&766'&72'4"264&#"6�!+:fK \+m"S ��"0;<(= +)+F|W(( /�P<4SH + +�;O:���E + ##|{ːXL,[==71U�_;# # +&$�L:��[+,9�� $ �����������!�g����%6&'.&'&676$67>6&6?>32676?'.'.#"'&'.5467>76.#"3265467>76�%"$�UrjIC;\�F)z`F�F7)4��T?G(&L=5�*�_Ix 6D + ;,# !'"-4L#@D>Hg/ + % &(� U9')%> + X! 9�!&�|cH(�]R) !"1�17yx1$ 7wr3&7@D$��4L.).@<Z1IWBj�����.��~�P�_�l�t����62#"#"&'.'.'.7>327>7>54&'&'&67>7>&'&23676&&?'&&"266%$'.'&'.7>_.'%%"�_EM�.F<��U2)4�S4/80:IN:I/�$!4RhW " ]3`N_�/� Yz%,/0�� + +��D<6*":�b*��B9. +AU�O:|620�&$�/ "�A:�(&T #bbX> 3'�[��7J0\9��V)���K�� ) � ",;%1( +4!���E��%E�_�o��62'&+76?�76'.'.7>764&'&67>5467>7>7>6&7>�6I##FEOO + + +���}��aY 33~t (, +F�� " .��j!-6%�G.�A>l(�H F(!D��?MM + CDno���'/ + "o}$� + FY3� =)#.~FDa +T�> +7%;������b��M�\�h������6232&'&#".5463232$7>'4&'.#"&7>326.56&67676>#"&'.67>76?'.76767276&'&'&326�i.4#I �s&8N7D�T .uc#/,'4�:i*+)="J# +�!�+ !D&2��3!7onno�9�P#$FE9�__ +0<* +1+ +�g�� + �: @:KK8D�7 x�,'6Z;<K735#�� +B.^��� �>�'_(.! +%"0Y&��(%��+0 !���(�j�[�v��6&#"7>7>454&'.'&7>7>3276#*.'.'&>767>7>7>0�@j| O0vf@�;0�:'1 + &,-);�GH' 7'g2W,A'%:�,<B 52 &Q4=!(B[7%JS.CBf 0!�@8$�Z( Z�W -�TPCM]S, +."G/)! #GEQH-F15MI;0Ga 1 !H ���5G?8� ')�����V�=b�8�p�|��632#"#"&54&&67>5466?&'.546672#&'67654&54&#"'&767676'&&'&6$763264&"26�.�#- + <.4>B�&Z uS :#&k�_' + +9> JU<5[3ڙ��. �!>8 .@`r�kWed O ;?�Z7t!* ��#= +(�(8<L-b>�[:�N + +&H �����"�~���x������>32#""'&>7>'&7674/7632&'.&'&'.7>7>7>7>54&/&&'&7>546�6'&765%476OpAIJJ2+GHjc +05=]I$+T�� +56#k&1#LGV)2NcfY�M CfHгiH*$P,8%&?(#G2e� .��' �p )-Izyq.* +56Y�- hS9�Y#c#b)=d8Y��9# ;A= +1S�0<Yn|cDJ #, + x",)((#72."(��6��i$ +:h +* ���&���p�w��62&'.'&"'.76'&6323232676'.32676#"./54>7>&_J?�f $*0'.N:Ge�#HV5+(?D2O Vv� +#+9=.' + +/L)F + *# 1?800 [/6'MoDL���xI':IC(( +Z72*#F0H6A +�i5)w;YH." ,?#( 8- +,%grlnS9)A���>�C����6'.7>6&'&6�8E &*H$0%EV >D<' +@0(R4* *AFa�.F1? A76�����B�;bc��*��>&'&67632654&#"'&6632'.>�-J('8(�%=+!04i+/�22Fw�� +H1-lj"RjfKRR����<�U�B�4��6#"#"$'&#"&'&67654>7>76�!'<L, + #7�o_)!��B3�"-A_%5PDqA='#(>* +KR+2\k $)P&52'�����F��f���-��6$'&#.7>7676;2#"&'&6�k(s[F0dn��%(0OO) $� +��B GkE�?�"� !)CX8� > + +& 2�����Q�KRS�4��>32"'&'&767676?'&'.'.>76{�J�0._1;�VB@?/ "2=f G32RRP!J 7]'F! X!"}9+AOH/++98"4 �����`������H��6&'.6>32#&'&67>467>7>'.#"'.>&2#!p$.*L>_q23&''",!*|KM2;#8PF((*!*V_.4�!b_*4&AU��B25E(PDBTkN'j/N�]+6>q�.:C"7#2'}�y����� ���v�x����6'&'.4&'.767>7667676&'&54&'./3267>7667>32#"&'&'./&676?6&326߈}FX8A|""%<M+/1�!!4,% +W:�t.+-/?,CWR�SC1 *5T�iOK,M !/K1'.s8�N����94-I O#1wx[ &5%(Cv (_4(�u +2$ ` )5~*#E\-q�*' +-+B36("\Jp +*oZA@,F%2 ++D5/(�7 N)c 1. +R0t��OJf{{��-'p]���#�MK�s������6#!'&67>'&'.'&/.'&67>7>4'&'./&'&67>54647>&#"&'&67>327>&'&'.4'&#&3� U+j*u[K8Nqs��#68L7�_. /D)0!.-*=2p �%j�!#01"G[1 #2# (.F�� K�V�#e:�K.& +&%�?`1 + _(aG'&�i��k!%(% +H!4 %�8,F$I/ +���`G:<FA'$Eqe��2 3$"3�����M�4@�b�|����6&76&'&6762"+&'&/"&'&67>'&'&.#"&67>&267>'.&32654&�M�M;iH/!:T1 @>8cR� *��0_,�='!(]? +%+��̇, + ' +*# &"#H��;�"7�+6�#'I8!%E[ln6" +&D>n$:*�>3& !n'2?9[>GA1q +!4"��c<: ;_.b��* O.�� ++FBIUO(+} +�V-C9=g���~��f�u��6#"'.'.'"'&3676>7>7>#"&'.'.7>7>766'&"#"7674�!3 '7Xe\ZRB (1.\bT= +E=+!/LY���QWng0 OUO���tsmE8��& �+)")N/; +! +:"+8�h?IB +" )%$!1+(D.*5ddCPL��jdm:1@6�����, ������V�GFB�b�{��6$"#"&'&5463267>'.'&&'.54676'&54&'.>327>327>.'&3276'��L:?3>� #%?(2b@C84* +J5�4PL�TD�= K)<�Z"&% !."9 +$9%3>��E3�{#4E*>(8l7U�bg�-$"6#HY�H(odgZ7 + Y_�ET) (( +��=-/-�lfZD )+�#����M� ��h�v��>3276'.'.546676'.$7>7>'&&'.'&6?54&/#"&5463266&'&2656$"+@J0</*E)^orm,�08C�q'(����'" +{�2& + 1NE��qpXOOKB +*-2(SC6-7C� + +� + + (8C,)('2k03S$>Vy�� + U: + 6��(�� )3� �a�dH������)�J�K�]��6&'."'.54#"&'.67'.&'&7>327>7$4 H%:`�� + �=Io# 9&*y +#W #�c�J O6 ii9-! +@`�eG�& '( H) j���c " +��H�8��(�:����>'.'&'.54&'.546.76'&>#"'&76&'&#"676'.'&7>76$764Ad�l?)* #H[" hE7^e\8'_ 3, )�/#�*%"! + F4q3O85"6���j7<2c3�P�@N:� #VV�;8)$Y�$R3DX O $$�) " G,7/Tt�%�!9 !#"`6.G,OX85&<S+7yQ"vc�L�%;*&& �������6������632'.'&67>'.'.'&'&'"#&5432%676#"'.6767645'&&7>76767>76&5466&32�i/]= +, ;%[_rI"S 3=`7�1F=+O&2B $-5%[J% po�wQC�f-- '-� �.+ +KK���\qQR>!>78 6K�T僆�% >#&1 +�����14M% + ++:( F/ CP�3�Y ! .�����`�H��6676#"7>676&'&'&767632?676&'.5466e+�%"?5 +�H&E *B743 +"����(9 BL./�mO]T*X` +2k*'"#���jH�= + + +<(&;-3 ')%�� �<���O�\��6$'&'.'.'&632763276767>32/.546&326�a�@36 +.SsB6`��>W.D"-$*928�� +1-wU�EN +�-## &��+�� +/,QU +F1I<8UF + (p:B ���%85��'.�����R������L��0�:�B����>7>#"#"#"&76&'.54&'.7>32./32&"266332'&"#"'&#"&7>7676'&'&676'.#"&'&67>762767676767654'&.6�$(8I GI�+?; =�$(. !+)@t-* +)5X�&A +��3(% �P%"): ,O�O +/!7 + 1DpiGt;'#4�S1.JR�JB6+ (\b:7{X�TP `G?YL7%�!0�#2(%g�q �L5 "C&%/c��3<-H;7 $ )2c.;=]"!6"J��w�Q"8OW,i^<1 +),����v���f��>3267>546"'&'&'.'.5467267>76'&'.676&76'&/54767;>�LZ53?A +&:9l|o? H +b<WDU$91�1*- +8* + J Tl +Vop�{|� + +! '� ++1[$+=>j7!b; *)�x�"0! "������_��q�������62#"'.'.#&3++&'&7>76767'&'&'.5463267>7%>32'&"&7>?&'&5463267>7>'&;'&S �)1J; +(#[^Z, 7FB% +$�`� A`83�7 -"p��0&+)</ +5�j7iA 4 /+1 !5� 9"<//�)= 5:�'^a0MRCW[ABk sDV?2!+ !��4" C/# ���x1z$b�=$<% / ��&," 'DC�;/�n}C���lBB����=�si��[����>232/32'.#"&'476'&'&'.'.7>327654'&676&'.'&67>26%676&.'&7676;76&'&&4'.7676%37;7;7;78&*t #;X 33,0t + -g h1?�7_&K( "$*"nE�B&$' % + + +$�b�$ ?$V +$3A*b^0O 9 +0( y +4!��0����.?8Dq5C�Tr%Ki=*:"oU84�:!%' �4X�� N0$.gg�j�/r &)�`R#k�H6=���K@��(�S���6'& '.'.'&67>76.#"7>76'.54&#"&�8&n%Y4$B%+";&M%\��=��(&Lp/X)r�0T#1!2- +�Q0Tub\.) %���]\*M�;DT"D G 4�lh����v1I �0 Hs}'1�$%!S$S!����!�Jv�p�{��63267>7654&'.'&'.76#""'&#".'.632>&'&76&'&'.&7>4&'&76DK +"ZZ�)�6.� + �DsvBK63 $ =/05C<\I*)02 %, +��t)+D-g��K +#&E#'{ +=J -MG%%D� + + +\� />( �\M�H2=)`�)O|!0\���,����������6326'&67>76&'.'.'&7>76#"267676&'.'&#"76&/.'.67654>7>766#"6>C3V \X(�IMVRA, #� f'0.�T($'0 +0$"#5-Y&] +|j�#bW #I�rC="0" LO`�a+? c�"3�� +�+Y>$UP���K*$' }GTO$=0� $�.�S)(-/ Y%4%?8S�^)< +&��5+_9D.aD)3" g-X@���O;"`��� ��#������������>'.'&676'&'.54.'.'.'&.6767>74&'&$#"76'&6;'&'&7>.&'&4>36.#"765676."7274'&�=��!\3R�' +*Hd(A%3LɀO=Xm3,J4G?D+ .0#a��X7��), -$&Hbb* `��=1& + +# +=-)=(�2 + �K@B��q!0Q3F+*} ),6GV`7H + %-+�?"Z2>\)DHQ�)D� +! +3*:��oNc + ) )�7:%i%Z��&7&%f��W�0 �����5��c�p��>#'&232'.'.'.'.'&67>7>7>54&'.'.'.676>176* + "*��$ i�<M|�G�}c[(=�*",�Z:.)-4'>[��i^9/'@gqdh:<L�Rw�� �% <x{�:L?!%, + �o���u- �:.T/ #$$DE4$# + +D=aaE/0''!,?6RjgL�s)�'4/0���(��!��_�p�y����62&'.'.32'&'&6327>32654 +'&'&&'.'.7>7>32676$764&7236326.'&6'&76� ) : +W��8 +@0"%y��$ + +* +1 :$56!40 O :EbAn^6LL&��$%<o5ADV[=�@)� Z :4� )����WV D9(%Fl^� +(+[7PM$4 + 3� +��,.� $�����������S�g��63267>7>76/.5476$".'&76&#".>."326&'&4�u� '5*6:3# K>�3"Oi9!;-_ .'! $..[�u�����9B':8 "2 +0� )\w�`�Hm/ &(��Vpvp 4 F +#���}~�p@K(|����go%,(��"2�_��L) +K�^�V��!��I��H�{����>&'.'&'.4&'./.#.7>267>%6$'.7>7676'&#".'.6&#"327>76%t8!:F\0 .F$@.-!-$W: *O5OE> -�)+(,O +)&C f(Id2%g"I�J $$�#) 58 +4l*�#,+ vi�07|�+,<4iL 7-2�(00K&]�w�,(.(: +N\�W]-�N��2$3X$9<��C� Dw +6W{�2+�u�������z-��I�V�������632'&".'.'&67>5463232676?67&'"&5&/7>67>>7>32'&'.54&'&'.'&'.&546763267>4767>E�Z?. +�4 S#;.g$"$# '(U0.+ 5cW�./ +#�?/K +0H% �lr'� �#< */0& ,;,5N &3h %- +�`&#="=�O�%Z~��^.(#'79FqU�q$% +)N3&m�x ����#66: :(=*�?>6%-A�1%Fh"�- +�x$* 38" �/VC"#j'a���@P (% , ��!Tg.9���!��<�$����>32#".'&67>54#".6%676&'&676'&#"&'.'.'.7>7>7>7>'.'.'.'.#"&7>�e � $47r\%%-(c-A&)��((!+1W) +1�F5cl>* + 'UjZ�$ +75#*1'Z5#��k.<;%)R9 ')ME!( +�K +��<H6/��Iw + ' '5$�79+)1^�g,k9#(4 + =):<J)-L)d<!M04,H; �x +6U1y$$i&6':���� ��W:�U�d����632#"&'&632>767>7676547>76&'&64&76767>%62&'&'.'.#"&7>7>-8g##�G1(#4K GP�1i K<)" +Yf}F$/ |J0 +� $=tv$ ;$�b.(" ( %/$ / +"()Q!!8,;�6 )$!$ >&TP��[� �* : 4')'db&Q*&� #!2P� H/ +�%)""6JU2B*2$!)M#>9 &&A7IQmG@�������^��*����������627654632&'.'$'&'.#"54>6; 7>32#"#"'&""'&67676#"&'&'&67>326747>46764$/'.7>766'"6#327>4"26�_é�@X:+(A�����RC&9�,SD;�k W#+�9�#!{F�*%:-M ViU 4Le^1D;=+ Z.$n<&!3:�0e *H�˸� + +[$ � TYD4-� + t"1 7AIF+# +&$ ]d +!'2U(^d[ + +^ #�% K(. 3JC_(%M-n 00VB&T.o��,e*F7 ,� ��9�����4��676+32'.'.7'.>7>26766�e#)0&ezVv�DS=*L{Y�e /&'?3(-� CS3!<ː�����ms_+ +����n�9|�&��>32�.'&'&'.'&'&7632�J S$�?>>8X��+ �8~8���/Z&P2Na�Q� &# +��8�����9��6&'.67>7>7>76'&76/&'46�C! + ! +8B?�`p�S 7 +qr`"�@C<���BB�, + %"' $X 1</�"883B"'��<������>'.'&#"&'&767>�D>r[<9;;)E1(2-�1� +1��#NgV +)<W= (�A�����9���K���62'&67>���ZVLH^���#.#61QK2! ? + ��&2�����676'.#.'&67>e3*,�@ DHED +�6*�$BN94"%"(�����MR��L��>76'.'&&'.'&67>7627667676&'&"#"&546�X�;/e +GF + !Q!K&{D'$>NE +*�^;2-MA4,("-C2/5 +-5�5"5B� +l4&�џN + +@ + # )]O9-'6N ++ $2N+ +30�]TR!!(%,"@GF���<�Z���8��67>76&&54632#"&'&67>'&76'.sK+ 874*2<#,]c;.H}jU" ) +�%V�0i>=#S + %/A[J7&��F7&; S( 'E]#�O{���� �ZVh�'��>'&76#"&'.7>7>e0ZHMYj"\@W_AO +P0D:=9&8$8!SZ)2 p(<6.*\Q<135���=�b���F��>76'.'.#"76&'&67>767674546546�!"7 8S#3>2=&Z 7X, Z32o"%FY0 +�("=�q;=,'�)#= ��'B +d2_/Y'+�NC9+ A�O A���\�R�?��>'&547>7>76.'&76.'&6767>�(+ ,+!0ev�?a75$ .>+94^2N$4�Rg E$Y��jC qQ*5,!9$" ) +Z�N&y"'-.E�a)�$ 1(;�����%��o*��P��32#"&'.7>67>6&76&'&#'&6?54?6cD' "%51.[< + A_-!�P�)7) +"'!!8 + +554$ ?B>� 2<54/�� O���QB-/, +U�&)8G' %4I&�ewii +# +&C��^������"S��#�<�c�r��63667>?&'.546&#"327>'&67>'&>+=&'.'.67>7>726.#;'&�R,8>&88$KF�7&J>6EQQD��=X<=A$S +�<W + +* 56+,�&7 soMZS +" .G$-Ik ��&Y + +9); 5=749z\<Fb#U/# + ; o{Au�rsWW8!$0!)8b*$�.= GH���'�C4U�?��>3276'.'.'&#"76&7>76'&'"'&'&326'&6�*P/7!,O+ 2D# Z! % -7A�UII���# + %-3C6!-}h_1�_)]&�f�;%2��&�K���$�0��>76&'&'&.7>54>'&6�$& +o� k�,8Q )! +�FF#52T8) ��VN�Kb4 j] +uu^ +�9&*$z�����5�����;�E���>&'.67>7>7>7'&'.>7>546&54c)% + +0CI�M9' #5;."+D�%,> %&"-G,# R*-�%<OJ )�t,5cclKRz Y_^ 24& �R>`~0 ++ 8 W9�(/L�� ���-�O��>7632#"&7>'.76327>32&'.&'&76d )(X9/) ?4*V#0/6? +" \ +%0,9 :_5D9*&+!- 1Qu��){[-+ + \&#( +!# + *-. @$& !�������x���V�<��>'&67>&'&76#"&/'&76?'&7>QE.:9 +!%&!/I#3='=*.1jRVDe* 5Z L#1I`N��� |QB̩!E@h��d5D# $3>@&%-��:k,%����5�����R���>766'.'&'.+&'&76'&'&'&676&'.'&67>76�9; 3�LK- &%0*%#'# +*=: .+*?2<1 ,,7]O<"� 'G)A+3 +;#AGy +�%7/ ߗ �e;0 +�N"T,7 ���&�����>��67>76'.'&'&'&'&76&'.&'.7>�S�:((&r-\ 3G2ZD./)6=40,Q "-2 9�5E0zE%J@a^t +T/ENf�Dv!93* 2@�����b����,��6&'.5467>."676&�"� +ȶQ:7!'" +F�5E/>23 Y�(�(u��.0'3b'O�&.$-M50a����*����;��6&'&>7>54.#"'&6?>=#"&547>G�,[D +#&A*J"3"(>@(7Db'"!#8q2 ( -t�ku)>$' + *65!?+E���2A| U��0�P�1G-L���*�]���:��672'&7476&'.&'.'.7>7>?7>7>� +$,"!�%3/2)G5pG7������ `)xo�"/,*,;V17!\(1*0?>h]���\�E�;�*���>32&'&#"&'.76&'&763276u/<)�6*C)*" + ! +%LX -4P +�#]&- (Fn�� &��blO\3-*#���V�p ��7��>32#"&'&6326?'&'.'.54#76767>+&%8(4L4=F _}6<15Y8TJT6= + + N�z<V$��J- (#.BFQ�%5iC )$%8A,+%& +:v +"8Ek��0�X���L��>#"&54>676'.'&76'&'&767>7>7>76e b+lRrL|'%^ 4", + �-^ �HR� + � + +H z "3D��!Z2"' +�=7KE@5=�-&,:�����(�����N��>3232>746327>"'.'.'.'.76'&'&#"&76X"'@". '#nS: 2* 52,1 +%�- -&#� 6>9 (!T<u�1$FLRaT*) + +'0(!V/3<\l7������B���1��66?>76&/#"&'.'4&#"'&6�mH7 +5V4\Q&'"3>F$+IIwJB '".%A W�-S>wa-T +&�n`n #)CD$>�61%1�n9M/&����� ���i�)�\��632654676'.'.&'&6%67>&'.#"'&'&67>546n8o#N "' +vA%9 &# (� ! �,(/Ag"G$5"$,�#'V[-I* ("$BBaw�2F4#%G`2 $S�&'G�_JNO,�.7,�����������?��%>&546662#"6'+/.'&/&547676� )<0|W9C)uCB) 7)#�;[uU$F#K6uD`/,OO@1&4)�H(':&B+/T#[G&&1* *� +.@=SS��4�#_s�"�5�g��>32'.'.>7>&'&67>56&6#"&54&'.54&/'&67>3267>54E*O <(6BM):]�n#ICl�J�+0O�@-'-(' +% &#!�:'' !4;� +�.1-i�;&2*=EF4RNf + _03�'(,sT&� +o�NE2 +)'?�E3-$+" : fE:a�&C>�����gT��A��6767>327>#"&'&'&7>767>?'.'&546�[ P1B1'} $$ 6%3�')J+7!&*<@)R5D�F %79R0D2U`>5#j{gE�6.?1'. +,4)).5 + YL#'$ + +P/) [TF\-:�������>��676#"#"'.'.5.'.54632>7>�1X60 +!I1)5- '# +-3B'QOF\, $$,W/M�C4"=���W%#6Ei�8% !E*$B@*H{O]O4-!;60wx����;��]f�&��676'.#"&'&767>�1# ,f #���k�o� 0�#�$&�����,����>��>#"&'.7>326546?6&'&54.#"&54�w/. KDB-<D[8E5YO!0'R[ +) +#- ^�!.�Օ�$00&8ѵ�b;0$"�{0s �!#/#�EJ/ ]!�� �;�*��>'&&'."#"'&67>766�*[;t$ 6%3M-S6?< +8;O�<C�;+02� 8!// �(()-���������������-�b��-��>&'.76767676726632.7>�! +# ,.3O) 0�T/gT��XcE 6Єg~�x4GH3(,n�����y�I�I�X��>#"'&3276'.?'.546767>767>7>4&#"76E 8 + R`!(#)0]��EF9, @;V 4#/,\4,A� � @S,#�#% 1@/IF�,0W + ,D!@`'$�I(/ 52$+$>�� [e4.72��� �-�a� �r�z��6'&6�>32.'&676&#"327632'.'&'&67>767>'.'&67>7>4#"7,#" bU4;::,.)0 834# +@'�]-C7bO]/*% &��`JJE�[@R`=15@ +)/! +AW � �H !'-+#; + +-2(/Hi[O +/ + +O/nx;*)# IE1"*C>a'\c0#Antj�� +�������"�K��>32#"&'.54&'.546>32#".546767>7>�)!JR� %L'6! +G@"0W'~n"* FBBsF, 0$��B.o3;)r1D�##k>VI(K)Ck +� /6-OZK�F97��U.rB2���#J�#��=��6&'&7>36>76#"'.'.#"&'.7626'&6���Tf�2+�")Db-? +N0*+GL3" + &$ 1O3&�-)( 1"<sr +#06\L3 " +>S79����� ?���>#"'&7>54q ?,3>*>*16*@q\8?4K*CA@ %67+hUaLv����'���� �B��676"'&546?'&'.546%6&5467>4&'.'.�S�XzER!22L��Xc� AmQF@$4&l;%�A� )��:4$%�26WW + +�<j�"� �1'qA%> +]<>��������6�D��>67>&'.7467>7>5467>>&546� + ^ "�q2Pd�{�2D6&H%;/ GP� R^�(&!&�I:0 IIRB'l0 ��4,0A�eMXV6�%%C a�XBM-�5*Z8*���JL �X�p���6;67>2'.'.'&/.#"'&67>'&67>7>?'&5467.'.>&�.ONG# +(:A + +%4!*$ +*3E3"&^UU� +};1&O59D +!$!S 5(-#. +#%+" �.""D0 69#+I!0GEoW�:�N/"�^wOO%�X%],�[`�)O�0<52��$Nz *5����;��� �<�L�X�b��676#"&'44'.&'&'&767>76'.6767676.'&"76632#".%&7632� +e'i'A_%.&OZ�,o+/3( % .3�!$ .� +� +\= 0E%�� ]1 :;��@`C��L(BC>/ #'��"( �-&F( `#���zk_y2R� +6'2��7��� ��g�z��&632'&%>32&'&7>546'.'&'.&'&'&6767>'&676767676&'.&326'&'.E +"& ��b@� +30$.7C+&_-EQ3% + +0N x? 9,?3+G!'P7 8 - +z+Q�;#! , + N C ,%6fU(M��97�E)+�rZ + Q�p`Y +R.6�rB$/5�#I g��*2$� )�Cu����8�toU�y������632762'&'&?&54&#"7676&'.7'�'.7>765467647>7>4'&#"67646�$)#2NsG1$(©" 6Mx67b37 +& + +��!# ,0�eG), + O#.$$e@$ t:"%?/8+f. Vu�" 6U& 1!7=%$7630 + + + +/fh�F<o . &�}Z�a9;H;g0i��u!� w (�����_7 �T�_��66'.'&&'&'&32'&3276'&$'.7>;'.N= +(+.:k&% + +*A)I 2' ;<����1 *#Nb�OIT�xr4[�A/+:L~%( " +>Wcc&%)� ,2.� ��6' ��=� +� ��R�Y�d��62"&>'&'&'&32&'.#"&7>??'&'&632>32656&76632#"� + [&"b9k8r4> +&2) +>750-37 N.'+$),"% +,�)�7./T + +�& %$.!':63"-H�����7)"1 +�� "2�&/-���-�QD �_�i�t��6'./.'&&'.7>547>4&'&"'.7>76767>7>.'&6'&7632%4632'&,0�)$1="CxJ��f& + 1[D'+ +$! �!&oTxC + +:� $�g*G0j? ����tP0@2G �o�À ���98@ (/!#E + X^j��Ї;J i?Cc��oh<��aaJ!5U������� �*�O�X�`�l��67>'.'.'&6?>6'&"676$7>'&'.'.%6.64#"74632#"&�/#c.DI+*sJD����LO�(:VV.`�;+*Lg('�d��.42?E-�D"$'�� 0 �-%>s +0-. >��%;�W��',.�R9^b��HC�� `n*��4J8ϒI`75]47 + +�" :�����8��: �!�W�d�o��6'&&'.'.76."".#"326767>'&67654&#"54&#"&632&'.&7632&�5SqSLII1 +.,clPu �)/� (��P( +$ �(Hw#!((ZD|+(S(7XS7(/1� +SkF51 #1IKO_Ufh\VkO:7 �?>�\��5 �M/^h�%"% +"> +VQ0q &�y� +.D����1�R ��I�a���>#"&'&#"'&67>&'&'.>7>767632>&$326767>54&&#""67>7>7>&� '` ^-080&"PML0+�34#$,"01W�ho +n,(h$:BQ�0�� hZ + �g\��KYKG�Jgv)b� ! +�EC2Q&@%XX��ADE$&";$$ C4($ !.�GYa[H��6:%!-=UY�� + ��;1ZG3I�!sf� JKhJ L��&#I X:+R@�����=�I� �L�X��6'"3232>7>32676'&6"#"'.'&67>%4632#"&�%- +'$J;oe +1 4!! + + + G�&")�\UF#,22'*3 @%U-UU/!40��='H*ex�MH5W[ I�=�g2\ �!�"��Ep����� +Oj��.��R�K��67667>3267>?'&6767>'&'&76&'&'&67>6�$ ( DI '23-"4�;2 :697?&�\7#J�L^7�."1%*+%3Q + 5B��[ZX�5* + +�zh[~�51< L-N+/�F���0C��Ot9,! +�����(��|�_��>.'&'&765>7>54.#"&'&&5467>54&'&&76767>�0�DNK +)D*8(!�W#N#77`SOA6K9'1_ENR[ 'QIB%_, �v=9--&:%@3V5g3s$(K +I(H=6*jK +�-,4!4 #&N�_��F5pVs}W��b������1���C�=�H�W�g��766'.'.'.54>76'&'&#"'.7>4'&3&67>54&6#"&>=jm%T[ Z"<;Q:�P53Ue&4 + &!!�C +>m#*++ +'6�8��?)]�Q,.( ;)8B*#' *&�� + 17;e� �$)�!).8B"&<<T6L�����&!��7�B�S�c��676/"'&67>3276'&'&'."&'&47>4"326>&546%6'&'.'&6��S /4%.5+*DD� +�&5 +#, + <�$*,>5=� +A-��+/$P I +A�$'251#!!!#*,9v0.+$ + 4��+3.5�"%)*"h +&E, +.* +�����T��8�L�a�j��67676&'&#"&'&>76'.'&'.7>&7>54&>'&'.767>476}#�UE.;W '/ �#,:Cs9>"(;[ 0�%-5'!Z@)� !"{ � " & ,m-.�# +?* w*G)9@9;}Q) &l! ��#O*/?%�u#)+!> 1)*Q{&#�����D�����H�S�_��67>"67>67>".'.7>?654&'&'&76&32>.676�BtCD9('6EH"q7,0 + +;3*&'-QeY 4N7FODC3- +�&R+N*% ?�� +jM��BJ'81y..9(#''@HZ70< ,X1#^1,� !P" +43@ +1"@J��K5A���%�����$�:�F��6#"'.'&67>4&#"76'&'&6546�Dm9A ! _5)v51%'2�o+ � '"-%Q$8S`@,uH$18:'!+ '(-2I09�L?.f"!!&'$_p,��������'�A��>767>&'.76&#""'&27>22&5467>x.<0! .|-P#He& %= )#B+�<6!+{.- &;�`;!b+<) 2%�=I\(!!!O! +J%" Z *S��� ����� �G�o��>32&'&7>767>&'.'&476'.'&7654667>3'.'.&546 $ +�,kK&/-UK8 6#8Z0XP + 6*!(%455E +#�>A;>>'�O/E4 + + +&- +*M�F)'�""(-Ee8Q!T-I�rq"T`7f/)'��/$%;><.<)K(& +#&��� �_���%�:��6#"&'&7>&#"67>6'&'&'.7>�$G%5+>6f:"."�R^�h3*#:*?51�"(9I+��# 08e`TN&��/_�;}6'JF]�@ g� -����5��p���&�4�F��6'.7>.'&67676&>"'&%6'&54&'&6�f 30R[[)13=9 #/"!5 -7/ *a��%<+# �[7 +LfX5%'C"NWt�'E�%Ug #2 5�5 = " +$"7����88�5�?�J��6&'&76&'.5467>7>7>6&767&#"3276�# + +(#�L@)$?.!2? +��",.0!� $#A2x7+$ (*j.C�"/D %M?>#L0�5% + 6�0D.`1;(r")?�&\���*��W��B�R��>3267>7>3267>&'&/'.'&#"&'&67>>2'&7676�&(t 0 "/ & GLK;JB(-, = )55&F{;3';��~��2TA=�6B#* +"'$$-;gor %-�$(��'" #@B�����3���r�8�F�U��>2>763276'.54'.'.'&'&6>&547>%6&'.'.|"<)�9',#(: ) #^I3+'&"%% "u/ H>8&�K` 8 �#1 ,6��!^OH� .0 SG3 +"DGK= !N��#&#%�-'A�����\5D� +�����D�[�����5�C!M�}��6'&'&327>763267>&'.'&&'&67>323:>4.'&'.&7>327>7>7>�*�dL!/ .�0k�J/(K*O��+9:�;-&3�_I" 5V-�� +G7-@GoG'5(; + /?q��s�eE9G+S>W ;))3,H-#*D?VC $ )-E ('! + +- 24 4 ?" 4&B7^@ +!-,, + 'A7843��������������:�v������������������������� �������G�������o���������� ���t���� �� +���� ������ ��@���� �� +;�� �� M�� �� +��C�o�p�y�r�i�g�h�t� �2�0�0�9� �S�o�x�o�f�a�a�n�,� �c�r�e�a�t�e�d� �u�s�i�n�g� �w�w�w�.�f�o�n�t�c�a�p�t�u�r�e�.�c�o�m��Copyright 2009 Soxofaan, created using www.fontcapture.com��T�e�s�o�x��Tesox��M�e�d�i�u�m��Medium��F�o�n�t�F�o�r�g�e� �2�.�0� �:� �T�e�s�o�x� �:� �6�-�9�-�2�0�0�9��FontForge 2.0 : Tesox : 6-9-2009��T�e�s�o�x��Tesox��V�e�r�s�i�o�n� �0�0�1�.�0�0�0� ��Version 001.000 ��T�e�s�o�x��Tesox�����������2��������������������������������� � +��� ������������������� �!�"�#�$�%�&�'�(�)�*�+�,�-�.�/�0�1�2�3�4�5�6�7�8�9�:�;�<�=�>�?�@�A�B�C�D�E�F�G�H�I�J�K�L�M�N�O�P�Q�R�S�T�U�V�W�X�Y�Z�[�\�]�^�_�`�a�������������������b�c���e���f���g�����h���i�l�n���p�t�x�y�|���~������uni00A0Euro�����������������e������������������ \ No newline at end of file diff --git a/web/modules/captcha/image_captcha/fonts/Tesox/tesox_readme.txt b/web/modules/captcha/image_captcha/fonts/Tesox/tesox_readme.txt new file mode 100755 index 0000000000000000000000000000000000000000..97e2b6286fe12b1af31c20232b559a9dc214ab14 --- /dev/null +++ b/web/modules/captcha/image_captcha/fonts/Tesox/tesox_readme.txt @@ -0,0 +1,24 @@ + +The Tesox typeface +================== + +The Tesox typeface is created by Stefaan Lippens (also known as soxofaan on +drupal.org, http://drupal.org/user/41478). +It is based on hand drawn characters, converted to a TrueType font with the +FontCapture web service (http://www.fontcapture.com). + +Background +---------- +The Tesox typeface is created specifically for the image CAPTCHA module +for Drupal (http://drupal.org/project/captcha). For a better out-of-the-box +experience it was desired to include one or more typefaces with the CAPTCHA +module package by default. However, this redistribution raised licensing issues. +For example, according the code hosting policy of drupal.org, only GPL licensed +code and resources are allowed in the drupal.org code repository (CVS). +To avoid licensing and redistribution issues, it was decided to create a +dedicated typeface for the image CAPTCHA module from scratch. + +Licencing +--------- +The Tesox typeface is GPLv2 licensed to be compatible with the drupal.org code +hosting and packaging policies, as explained above. diff --git a/web/modules/captcha/image_captcha/fonts/Tuffy/README.txt b/web/modules/captcha/image_captcha/fonts/Tuffy/README.txt new file mode 100755 index 0000000000000000000000000000000000000000..205343fb5b0aa84f185b08d7c5897f0f8df5b9c8 --- /dev/null +++ b/web/modules/captcha/image_captcha/fonts/Tuffy/README.txt @@ -0,0 +1,23 @@ + +This directory contains a subset (Regular and Bold) of the Tuffy typeface +created by Thatcher Ulrich (http://tulrich.com/fonts) and released in the +public domain. + +Original licensing statement of the creator +------------------------------------------- +Here are my dabblings in font design. I have placed them in the Public Domain. +This is all 100% my own work. Usage is totally unrestricted. +If you want to make derivative works for any purpose, please go ahead. + +I welcome comments & constructive criticism. + +Put another way, a la PD-self (http://en.wikipedia.org/wiki/Template:PD-self): + I, the copyright holder of this work, hereby release it into the public + domain. This applies worldwide. + + In case this is not legally possible, + + I grant any entity the right to use this work for any purpose, + without any conditions, unless such conditions are required by law. + +-Thatcher Ulrich <tu@tulrich.com> http://tulrich.com diff --git a/web/modules/captcha/image_captcha/fonts/Tuffy/Tuffy.ttf b/web/modules/captcha/image_captcha/fonts/Tuffy/Tuffy.ttf new file mode 100755 index 0000000000000000000000000000000000000000..8ea647090f75e7d34bee4c511921299d1f3c1055 --- /dev/null +++ b/web/modules/captcha/image_captcha/fonts/Tuffy/Tuffy.ttf @@ -0,0 +1,131 @@ +��������FFTMEQ�\��G����GDEF�)����FP���GPOS����F���^GSUBl�t���Fp��� OS/2��k������Vcmap�+�������Bcvt �!y��8���gasp�����FH���glyf�Һ�����4�head��B�����6hhea Ύ���D���$hmtxw�J������loca���R��<���maxp����h��� name/d����=����posteH����Dh�������� 4�_<���������/�������/����?����������������?�]����������������������������b��T���������@�����_��������P��������0����������������������PfEd��� �=�=������������������������h����g���5����z�h��d~��$�)�Z��!~�N��gb�m��g(��p�dp�p�mf�qp�Xp��p��p�op�hp����q��qK�T��BX�\��9X�)�H��y��d��yv�yz�yS�d��y���t�q��yQ�y�y��y5�\K�yZ�\M�y7�-��X��y��P(�T��J��L-�dr��+��~�������X�`��T����L�I��b��}��I(�����������������(���f���I�����`��f(�x��R^�D��5(�x��Z��Zp�}�����?5�(�P`�TX�?V�J1�Z��+�L��)h�;����m��^��?V�1���&�a���~�y�����u��b���R�R�R�R�R�Rl�S��xz��z��z��z�������������������1�f1�f1�f1�f1�fA�l p�`����������V1����\�^�^�^�^�^�^��T��V�l�l�l�l������x�s���R��$�p&�p&�p&�p$�p��w��V��(��(��(��(�����(��?����� +����S��_��c��j��Q��a��q��[A����������������<������ ����������������� ������ ���������� ��������������������������������������������� � +��� ������������������� �!�"�#�$�%�&�'�(�)�*�+�,�-�.�/�0�1�2�3�4�5�6�7�8�9�:�;�<�=�>�?�@�A�B�C�D�E�F�G�H�I�J�K�L�M�N�O�P�Q�R�S�T�U�V�W�X�Y�Z�[�\�]�^�_�`�a��������������������������������� + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a�}~�������������������������������pcdh�t�nj�ri���q��fs����������xbl����kw��y|��������������������u���{�z����������������o���v������!y�����������0�p����d���Zr����b��DV��$8L� <���*n������^�� ` t � � � � + + +0 +@ +R +d +p +� +�@v��$Ln��� : p � � �Jf����*���"V��p��(2����@��������$LXdp|�������(4B�������fr~����6BNZfr���~������� ,8DP\���������$j��g��B�� ���#.546324632#"&'&#");'(=�@,':?,':�"!�'77%��-?1$,?0% +���;��&��46;2#"&'4&%46;2#"&'&�+'"��+)"Dv$(��v$(���������5�{����!#!#!5!!5!3!3!!!���������\��\�V�i��i#R�����a��a�R�{��{����������e��*�3�:��3.'#5.'7.5467>54&ŁВ}#wK:oeL-ӴQ~fL-� ;5O-S|k9ԝ]t<E�lyr~�PAe +�$;IyO����*P\GP*.D$ +6W�V�� ���uZ9Q4���jT�����h�hD���/�?�C��32654'.#"4632#"&'&32654'.#"4632#"&'{I5=WO6:S��qb��rb���J6=VP5:V�rb��qb�@�ѐu3@XB 3CY<p�z\p�x\�3@WB2DZ<q�z]p�x\�q���d�����4��.#";#"3265#5!##".54>7.54>32+'n_c��[ea���w|����!Jg�[Z�L6RY(Ro_�t��3�LL�gi�������v��4su]:Hy�^P�]; #�u`�l�c������46;2#"&'&�+)"Dv$(�����)������&5473�n�{s����uT���������I��U����Z���� ��#654'3 +���t�j�yT��������J���]���!��y��9��#"'&54?"&5463'&547632763272#'#"&'�q!&��0*"��(*om*(��"*0�&!�(�#)!)�'(��('�)!)#�(����NB����!3!!#!N3�4�̍���7�Ɏ��7��g�#>�����%'65"&'&54632>%8H':@,':^,XM/gK/% +-?1$�����m3����!!m��[������g��>�����74632#"&'&g@,':?,':^-?1$,?0% +������T���3#3��͞�����d�����&��4>32#"'&732>54'.#"dBy�o��.mܓ��.�"�mPI&!�kP�L(ך�|��ڈ����� ڄ��x��p��m����s�������������#73#F��\����P���m�����/��'6$32!!4>7>54&#"�'��AxlP/*@:M3C*b`?��u4PypG?"=#-�kd�V7�� Ge�Y3bOO8>#,\ny/�^���\2*0%96F%�������q����/��?32654.+532654&+"'>32�#"&q�&�S��9_s=Nba��_Py �5�{e�uBN'&TM3���}��/Rh��IwK(}�pd�UI@i�^�mR�M@]�G�������X�������3##!;!`�����^ ���}���t��s�#��V����������&��"'!!632#"&'732654'.\!;1%$�^���B[���# kч��.�T߄� �'K���T7¤?>�݉Ţ���,'r����������)��4'.#"326632#"&'&54>73o�q���i���EI@��! +Bs�e�� /";N��%g��u!f~�ɛ+3N�~N͓*-:snDd H�����o��7���� #!57�ն���P���h��/����@��%2654'.#"32654'.#"2#"$'&54>7&'&546F���s�����`o��ew�����U�������# +3SQ)� �q��" n���&o��Vb�qQa��� -�n'�l((��w͠4,N�Y9V�'0�����������*��32654'.#"#"&'&54>32#%�q���i���I@��! +)Pm�Q�� 1A���#f��v"f����ɛ2->~pW3Γ,+:rr>m������q��H�'�� +�� +����q�#H�&� +��� +���T�����% 3 P�����dX������Bfbb����!!!!B �� ��������\�����%# 3#�����eZ�������9�����-�=��#54>7> 54&#"'>324632#"&'&=�6(E + �|R�H���V�~KJ>I21�@,':?,':���-P6="9 $}�Z|9��2`�aa�?7'4G�9-?1$,?0% +����)����F�V��%# �&54$32#"&'#"&'&5463232654'.#"�32632654'.#"�M�����^7�`Ј����6Tb-7x(,�Oz�Ȏv�R38fo��s���c.[�]��uKZmqBT��f�WRP�_�]��wC@K�^89+<E�u '�șq"(:X�\15_��Ph��AA���D;K]|[Gg����H������ +��!!#3#���%�����������P����y��=�� ��;��!2>54'.#32>54'.#"&'!2#2#!?FqK4�����S}F+ %HU˱�v�o?�WHvK-*Kc�Z�#���!7KR,#^ud�'!0C9#<9!�4Ye:*#u�5X^3$$"BWKL5!��d�����)��#".54>32.+"3267�Lp�_wȋa-.a��wL�bN1��hh�i8<m�cj� %@m_6R���vw�ƒV%<QQ*)[nl��wyݫe�\���y��������4.+32>7#!!2�*Jc�U���i0�O���D���O�A��s[4�jT�χ����p�z�����y������!!!!!!y����0��g��������y���� ��!!!!#y����0������\����d�����0��".54>32.#"32>5!5!#*���P-b��w��I�:�jg�i99i�gFvP<!�w%5Xq�[��i�Ț^xsDQQu��ipٴo*IYg],�VK��|^6���y��J����3!3#!#y�����k�����P��^������7����33����P��q������332>53�#"&'&q��o\�F���ͥ�&45��}�g��g����G���y�������33 ##y����x���ϼ�����s���������y������3!!y���e��ݍ���y�������33 3##y���ϙ�T��T��!��P��\��L��y����� ��333#y�䝑���w��P��Z��\������3��4'.#"32>%4>32#".'&H=^�V]�\==^�U]�\>�&Z��{yƆV'Z��{xƆV�vmS�tAP��VokR�q@Mz��Ue�Ξc\��ltmd�ʚ`Z��kj���y�������3 4'.+';2+#�� ,S�bΞ��� B�ՊΞ#)E?#���-(W�sB����\�����9��4'.#"3267'764>32'#".'&PAa�Vd�e5=a�[6{"�X�g�([��yb�|b=U?�Z�L�nd�}b=�rjV�uAi��{mdR�vC)"�t��e�Ξc?h��Vok���Ryu<A<e��Ui����y��P�����32>54&+ ##;2�^�R2�����Sמ���3Ʀ�"5LF(|���w��������x���-�� +��<��732654.'.54632.#"#"&-��s��,G=Z7S%5H26!����)��ps�2-G4S(@T?B*�ۋ��GW��r)L:9'* ))=CZ3�ê�Hq|^$>.+!! +13HNf8��T�����X�������!!#!XD�7��#����#�����y�������3�#"$'&5332>5�������-�#�zK�e9���������g�+��rV��?~Ї�����P�������3#3���G��B����P�����T�������333# #T����������������+��P��3��J�������3 3 # #J�jc��;˾���������^�)�'J��������L��d����3 3#L�dd��B����X�<�������d�������!!!!�P��l��i����ݍ#�����������!!#3�u���`Z��������\���!3��1����������������53#5!����`�@��������}5����#'#35�����}��V��X�������5!XB獍��`w�!���&'&547632#"c� "��$ ����T��bB��+��"32654&'254&#"'632#=#"&546�o��l`��{h�uEz"N���ۋ+Dh6���F�cg��cf��m1`��+"hm��{��=>,Χ��������������"��>32#".'#�&"320Fd>e�\..\�f>dF-���蠠ts���-GA#]��poŤ`#@E-�������������L��lF�!��.#"32>7#"&54>32�xF���+O6! +z@Um;��u(Pm�Tr�6)2c���#60M)HA&��V��j=za����I������#��3#5#".54>3232654&"���.Ed?R�Z>.\�e>dF0��st�����R�-E@#@l��Xpš]#AG-��p��������b���F��"��!.#"3267#".>32 + �pLw<<uLH� �3�v��ab��a�c4\��l��`�kM<7cv��>��[��eI����}�������##5354>32.#"3����;X`':M+m((9���V���<jD'))i4-�������I�?�N�+�5��#".'3326=#".54>3253�32654&"�Fs�M[�e<��\h�-Fd>R�Z>.\�e>dF0��L�st���c�k;Agt8Wu��!�-E@#@l��Xpš]#AG-�����p�����������������!#3>32#4."���rR�d7�<f�g?���YyB�ِ��Z�vBDw�X��������?�����4632#"&'&3#�8&"38&!4��5&8+ &8+ �������Tf���#��4632#"&732=3#".�8&%98&%9��kf�.DQB*H 5&87'&87��f +{�1�ϋ?g?+������������ ##33�ͮ��j����h����������������������%3"&'&53-dq��� K�JY% �������N�*��!#4.#"#3>32>32#4.#"V�+K3ژ�/1L.t��m���+A+�#HnrK/�a��1�&/@$�ih����s;^dH<�g�������N���!#3>32#4."���rR�d7�<f�g?1�YyB�ِ��Z�vBDw�X��f���F��"��4>32#".732>54.#"f9k�i��a_Ɍ��a� BoJIoB 7�ab�7wʗU���������Z�yDDy�Zy�uu��������T�N��"��>32#".'#�&"320Fd>e�\..\�f>dF-���蠠ts1�-GA#]��poŤ`#@E-����"���������I�? N��'��3"&=".54>3253�32654&"�4B��5�Ȝ[..\�e>dF0��L�st����=5�uy\a��npš]#AG-�����p�����������N���&#"#3>32�P1>Y{5���qR*�x�w��1�Yy���`��HJ�6��.54672&#" #"&'732654.�<Re>+ȔQ�R0�6�Pu-52?"?%6!&ؗ��&�h\et&G?� :Ab;��-JG%zOG$<$ " .0="���pCH`U(@-���f��P+���#53533#;#"&5��uFF�x������-uo���x���1���3#5#".532>5���rR�d7�<f�g?1�ϵYyB�ِ��Z�vBDw�X����R���1���!3 3�������1��n�������D��11���#333#��u���ї�����z^��1��/��7�����5��f1���!## 33f���:�Ȫ������H�N�����x�?�1�&��#"'732>=#".532>53�L{�MՖjg�1^R2�rR�d7�<f�g?�b�k;�U{#AoE�YyB�ِ��Z�vBDw�X��Z���1���%!!!5!V��5���������Z��{��=��>&67>7&#"327.'&'&'&Z@T! +,9F'+Z<.<"Y,'F:, +(�f}��w" � P-M�����+> � "'@W�����}������33}���R��������=��532676=4>7&'&=4'&'.#"5�@T! +,:F',Y<.?"Z+'F9, +'�i���|" � T+Q�����-9 � " %��R����?3�\���>3232>7#".#"?1H$800: D7!!5D" ?403$B1�+!-,+�,--+������������^�������������P�N���!��.'&54>753&#"327#}k� +8]`4�cdZPkU~pPRZ<�� �w,9L|M. ��W_Pz[!&S]jZ`4���T��'��(��#5332.#"!!!>7!>7��j�^I: g�� ^�.P &&(�; +TBT�kt��Fd��Q}N����! s *����?w-w��1��32654'.#""'#53&'&547#536253#3# cAKi bALidP�P�+;y�P�P�y&B��>QjK>Qj��77��9F$gQ�{77{�4B%hW�����J��u���!5!3 33!!!#!5!������DX������1�Ϙ��7��D��6���ׇ��L���Z���������#53鏏��{����z����+�h��,�;��632#"&'7;26754"#"&'&54>32.+"4'.#";2�&�� +��]�;�S Ga +- +��"N�[avF�F��^!�Q%� Ö0,���Wd +#pX ��7:n�xH\��S{Q���L�������4632#"&'&%4632#"&'&!48&!4�)8&!48&!4F&8+ '8+!&8+ '8+!�����)7����(�8��#"&'&54632&"327%32654'.#"4�32�#"&'&uCSJqyV[;N"L84!.�s�s���s��u��� +����� +bC\GUy;R8& ,D%o���&n����Ŗ2)���Ŗ+���;^����3#3#������ч����^����"#����"������!�}���#5!5!�����!͏����m�m���������^��X���,�:�M��32>54'.#"4>32#".'&%32>54&5.+'32#'#� Qlv;D�tH Vpu5E�tGu?g��E�&��kʡ�3"/0]KRRq� -�d�AR�2,JuG$5a�Z.(KzH&1^�_f�tR&߰96��}U�{8q"N:> +,�������?��j���!!?]��j�����1�=�����32654'.#"4632#"&'&�N4=SN4=Sv�m^��m^��3AW?3BXBk�tY l�tY������'��H��P������a�+���632#"'&'&54i�$��:!�� �����ufh���%#332>53#5#".)��$�.N5%��.u:,E*(J�+��6L8�'@QQ&���b9D$�����y���u����,��23"3"&#.'&54>;����0GM*$)A/%����=^^A0s������� !!6O2!5! ������ ,7O1*)b�`3��������neE��'{����u��q�J���>54'.'7u'�@8*J +� " +.8>H 81,��b;^����#!#�����������^����"#����"#������}��"�{�����R���}'�C'\�$ +����R����'�r�X�$ +����R���q'�A����$ +����R���J'�a��$ +����R����'�i/�$ +����R����'�pf1�$ +���S��g�����!#!!!!!!53�����O��F��F�������{��������J�������x�X��'�v����&�������'�C��^�( +��������'�r`^�( +�������Z'�A����( +��������'�i��1�( +�������A�'�C�js�, +��������'�r�X��, +�������If'�A���, +�������9�'�i�Z!�, +�����������#53!2�+324'.+!F��}�82���⛝���(��,������bh�������E^S�����������5'�a+��1 +����f����'�CP��2 +����f����'�r +��2 +����f���s'�A����2 +����f���J'�a3��2 +����f����'�iB�2 +����lJ����7��,�-��c,�����`������,�9��%#7&'&54>3273#"'32>54'&' &#"��J|/6[x�cza�E�55\w�c�5VoY�Z=#O���IXY�Z<)F���okR���vD2E����mtR���sB�;Lz��Tto�d�`�&O}��Txd�����������'�C=s�8 +���������'�r�s�8 +��������w'�A����8 +���������'�i��B�8 +����V��n�'�r�^�< +�������� +���2>54'&!3 ##/W��lC.�����E7g���c��&;b@������&(S�X>!����\��]��G��>32!"'732>54'.+"5;2>54'&#" +#6�7����,DXQ'+^eN�Pj[( ?d_?&��$*Y��)�JsO3! +������*-8cF:" +,GnA'�h5{ $;`@o��;xJ'%�#CP7b�z��Y� +�����^��l#'�C����D +����^��l'�r;���D +����^��l�&�A��D +������^��l�'�a�yL�D +����^��l^'�i�^��D +����^��lV'�p�����D +���T��`F��W�a��"32654'.>32!32>7#".'#"&'&546326&74'&#"'>32!&'&#"�i_"�\mf�:zd��!��aV�8�m'@5$,,5U8�� +ȘWR$ +"�AP'N55Q3^�t� /�:X/Z�l1$��h(Ob4_Y��^}-IG]~ +7nk9 %71&�70��35,6+�&'f(Y�sG9�Bmu���V�]vF'�v���F +����l���+'�C��� +�H +����l���5'�r9� +�H +����l����&�A5�H +������l���`'�i���H +������-/����3#&'&547632#"���!� "�1���$ �������#����3#632#"'&'&54����$�1���:!�� ��s��#��� +��3##'#3���������1��^��V�������R���#��3#4632#"&'&%4632#"&'&���8&!48&!4�)8&!48&!41���&8+ '8+!&8+ '8+!����R������@��32654'.#"#"&'&54324'.''7&#"'6327� +�Iq��g;h9�C����� ݠ)G/30 h8�c�GY)J7���`�.2u�׆%s�j��M�o$!8�B��Ҹ�HP�$. )^�1�d��F�m���������'�a��X�Q +����p���''�C����R +����p���1'�rh��R +����p����&�A3��R +������p����'�a��/�R +����p���Z'�i�o��R +����w��[&� +��������V���J���1�� 32>54'&'&#"7&'&54>3273#"'���ATEj<ZASAh>! �s7_���_1�q9_���c0��DN��NVD2�?L{�LWG+%���YwZV���KO�ZuQ\���NN���������'�C�����X +��������'�rG���X +��������&�A1/�X +����������b'�i�u��X +������?�'�rK���\ +����������+��!#3>32#"./4'.#"326��/DU4��":[�^8d6�fD5hU5�Jr����)54ȢXeZ��kA.7�]Rp�<i�]QH����������?�'�i�u��\ +���������!!�����������X����!!���R������������!!����������SD*��������_>6���������c�4:������������jDA�������QDr�'��H��������a>��'��J�������q�2�������������[>|��� +���������0��#53>32.#"!!!!3267#"&'#53&54��'�}q}M`2XQ<_��,��"i0E\>`<9[7��&������:K`3+ZJ� �MW+Da/���!!���z�����������������s���������������)�������� +�������[�������x������ ����������������������� �D������� ������� �� +g�� ��y�� ��R��� ���� �� 9�� �� +l�� � �~�� ��&��� ��&��� � �&�� +��k������������������"-��������������S�C�r�e�a�t�e�d� �b�y� �T�h�a�t�c�h�e�r� �U�l�r�i�c�h� �(�h�t�t�p�:�/�/�t�u�l�r�i�c�h�.�c�o�m�)� �w�i�t�h� �F�o�n�t�F�o�r�g�e� �1�.�0� �(�h�t�t�p�:�/�/�f�o�n�t�f�o�r�g�e�.�s�f�.�n�e�t�)� +� +�T�h�i�s� �f�o�n�t�,� �i�n�c�l�u�d�i�n�g� �h�i�n�t� �i�n�s�t�r�u�c�t�i�o�n�s�,� �h�a�s� �b�e�e�n� �d�o�n�a�t�e�d� �t�o� �t�h�e� �P�u�b�l�i�c� �D�o�m�a�i�n�.� � �D�o� �w�h�a�t�e�v�e�r� �y�o�u� �w�a�n�t� �w�i�t�h� �i�t�.� +��Created by Thatcher Ulrich (http://tulrich.com) with FontForge 1.0 (http://fontforge.sf.net) + +This font, including hint instructions, has been donated to the Public Domain. Do whatever you want with it. +��T�u�f�f�y��Tuffy��R�e�g�u�l�a�r��Regular��F�o�n�t�F�o�r�g�e� �1�.�0� �:� �T�u�f�f�y� �R�e�g�u�l�a�r� �:� �1�1�-�2�-�2�0�0�7��FontForge 1.0 : Tuffy Regular : 11-2-2007��T�u�f�f�y� �R�e�g�u�l�a�r��Tuffy Regular��V�e�r�s�i�o�n� �0�0�1�.�1�0�0� ��Version 001.100 ��T�u�f�f�y��Tuffy��T�h�a�t�c�h�e�r� �U�l�r�i�c�h��Thatcher Ulrich��h�t�t�p�:�/�/�t�u�l�r�i�c�h�.�c�o�m� +��http://tulrich.com +��h�t�t�p�:�/�/�t�u�l�r�i�c�h�.�c�o�m� +��http://tulrich.com +��P�u�b�l�i�c� �D�o�m�a�i�n� +��Public Domain +��M�a�g�e�r�K�u�r�s�i�v���N�o�r�m�a�l�C�u�r�s�i�v�a��1KG=K9C@A82���V�a�n�l�i�g�K�u�r�s�i�v���N�o�r�m�a�l�e�C�u�r�s�i�v�o���N�o�r�m���l�DQ�l�t���S�t�a�n�d�a�r�d�K�u�r�s�i�v���N�o�r�m�a�l�I�t�a�l�i�q�u�e���R�e�g�e�l�m�a�t�i�g�C�u�r�s�i�e�f�������������2��������������������������������� � +��� ������������������� �!�"�#�$�%�&�'�(�)�*�+�,�-�.�/�0�1�2�3�4�5�6�7�8�9�:�;�<�=�>�?�@�A�B�C�D�E�F�G�H�I�J�K�L�M�N�O�P�Q�R�S�T�U�V�W�X�Y�Z�[�\�]�^�_�`�a�����������������������������������������������������b�c���d���e���������������f���������g�����������h�������j�i�k�m�l�n���o�q�p�r�s�u�t�v�w���x�z�y�{�}�|������~�������������������������� +softhyphen +figuredash quotereverseduni201FEuro������������������������������������ +���latn������������������ +��,�latn������������kern���������������������6�<�B�X�^�d�j�t�z���������������������������������q��&���7���9�+�Y�\�Z����5�)��7����2����$���7����$�w��$���X�y��$��(���H����$���H����$�B�D����?�q��Y����W����D���Q���R���W���X���\����M�u��M�P��[����M��H����M�u�����$�(�*�.�2�3�7�9�:�<�?�D�H�I�J�M�R�T�U�\�����������?���������������. \ No newline at end of file diff --git a/web/modules/captcha/image_captcha/fonts/Tuffy/Tuffy_Bold.ttf b/web/modules/captcha/image_captcha/fonts/Tuffy/Tuffy_Bold.ttf new file mode 100755 index 0000000000000000000000000000000000000000..9574aab6e8cdd0b8a1c003f80539b5b5bbac24e9 --- /dev/null +++ b/web/modules/captcha/image_captcha/fonts/Tuffy/Tuffy_Bold.ttf @@ -0,0 +1,67 @@ +��������FFTMEQ�#��C���GDEF�)����A����GPOS܊����A���DGSUBl�t���A���� OS/2i���������Vcmap�+�������Bcvt �!y��8���gasp�����A����glyf��2)�����1�head��������6hhea�����D���$hmtx��: �����locad�qH��<���maxp����h��� name������:L��Upostd���?���������漢�_<���������.U������.U��&|0������������0�&�� ����|�����������������������N��N���������@����������3����3������f��������������������PfEd� � �=�=��0���������������������h����Ej���w����^�;�������P��W��D��8��b��@O����P�����\��]��Z��^��m��b��D��k��@��@`�-��7`�7 +�/��O����w�Z��s��s��s��Z��s+����f5�s��s�B?�w��d|�y��b��y��SI�NC�o�F��J�5"�B��H��yX����w9�z��N�J8�Zb�w�Vb�JE�X�Jq�Ja�w�y���w1��'�wa�wQ�\l�wl�JZ�z��N��HX�l$�H��9�R�l��B��P��s��y��55��t�F��J��5p�-��P��!��HA����3`����X��T��5������A�gh���o��]�9�����������5� +��d��s~�s~�s~�s��������~�������wC�dC�dC�dC�dC�dA�! ��d�o�o�o�o��Bh����>�Z�Z�Z�Z�Z�Z��ZO�V��X��X��X��X&�� ��"��$��&�HA�w~�\��\~�\~�\~�\��v�5��l��l��l��l��lE�w��l������ +���� ��&�� �����$���}��������������<������ ����������������� ������ ���������� ��������������������������������������������� � +��� ������������������� �!�"�#�$�%�&�'�(�)�*�+�,�-�.�/�0�1�2�3�4�5�6�7�8�9�:�;�<�=�>�?�@�A�B�C�D�E�F�G�H�I�J�K�L�M�N�O�P�Q�R�S�T�U�V�W�X�Y�Z�[�\�]�^�_�`�a��������������������������������� + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a�}~�������������������������������pcdh�t�nj�ri���q��fs����������xbl����kw��y|��������������������u���{�z����������������o���v������!y�����������(�h����@����Nf�����<~��$t�����F��>f~����"@Pn���8h���� < T j | � � � � � +* +f +� +�.~��� +\��� ( J � � � �.j~��4B|��"6����r�����<Fh���������$0<HT`l��������FR^jv��*6BN������Lh����$0<��������*4>HP\hrz���E����� ���#"&54632462#"&LHG)`+,a��`�``DCa�$$�%99#�D``�`a������&��46;2#"&'4&%46;2#"&'&O"MGB��O"LFBDv"*��v"*�������w�/�{����5####!5!5!5!333!!!l�������>��>���7��7T�����5��5���J��J��������e��,�3�;��3.'#5.'7$467654.��}�@��"(GdwJ2�¨O�fG� �y�;86� <� +{tR. +��*KV�P����Imw?R �VF�� ��P$BE�����*95���^�?3m� +��!�-�1��2654&#"4632#"&2654&#"4632#"�2L03%#3װ}��}��`2L03%"4ױ~��~�����s.750,@@-�������A.640,AA+������������;�����1��.#";#"3265#5!##".57.54632�Z;CcgO��j��eon�J�"Kl�`���=>(����/�*ESMRZ�wd�cG��3moW7q�~Il-Fh;�ވn���h���46;2#"&'&�O"LFBDv"*�������)���&5!!�q����T����L��R���V���P��Z ���!654'!�T����~v`�����������g��W ;��M��72#'#"'&/#"/.5476?"&=46763'&546?6327632��"*��+', pn-+ /��"*��,&, pn.* /5O"L �9 �� 8�O"L � : ��7��D��]&���!3!!#!D +�������.�������8�3~*���%'>54'.54632~*>6J"/E`?OX�9vZ<+QUF<ee���bf����!!b��������@���'���7462"&@`�``�`�D``�`a��������q���3!!�������P��A��� ���32�#0#"&%32>5&#"P��u���u 5S8F^4]��g�Y��}�������`�n��U/E����������������5!?!!��� � �����P���\��%��&��'6$32!!4>7>54&#"f� +�@x{Z9G\.�zo��7,R@*r_cD+jVRe4G��@c�fy�\.zZs2�U�|@)[ARKc6fmV���]��L��-��%32654&+532654&#"'!2#".]%�Jf��\��KnmR;Y�l4Y�J�1FJ(L��jq��CF]V{\[��wG`gB-B-Z�g�h!>_~K[�}JE����Z��:�� +� ��3#!!5! !�������5!����OI���\����������^��R��#��"%!!632#".'%32654&Vn9��v��+7&m�懇�Z�oV0 + *4M+n���;Z����w����w5Ske0.16-�ik������m��+���!��4&#"326632�#".54>7!x^\szW^v��1� G|�h��w))L�`��Za��*���T��P��vQ�[������b������� !!5�� ��]����P�������D��X����7��%2654&#"32654&"2#"$5467.546Nq��ex��Sh]Yke�b��,8W�R��n���S5B(���jh��lj��WUUWNab=ެ$D/6+׃o�q;���*Hg;����k��)���!��32654&#"#"�54>32!xx^\szW^v1���G|�h��w))L����`��Za���� +�T��P��vQ�[���������@����'����������@�3��&� +�������-�����% ! ��^�L�]�dY���������75X�����!!!!7!��!��%�^����7�'����%! !�����ZS�d[�����/�����#�-��!54>54.#"%>32462#"&��%:FG:%>U3Og���J�{]5&=II=&��`�``DCa��1T>99=R08JiO)�� B]�MK~TF86A�KD``�`a�����O��|��:�F��#"$&546$32#"&'#"&5463232654$#"32632654&#"�ph֏����}}�'��#�z�v8n$U�������8%)=����xݠ__��xb��%lLM]fDKm�JHw����wr����@?�⟕�Ќ8bbF��^��xyݠ^L +NhrRE[_����C��� +��!!!!!��,K�@���3�����Y�����P�����w�������(��32654!32654&#%!2#!�㚃��㨉ww��L�]�jQ/ +p]`�:Dl����quZ�D��eGFd�-ACOC#m���?5i}]?���Z�����"��!".54>32.#"327П���_(*`�ۈ�9�(�@O|O3)R�[�q6��X���|v���R��YB]7\��So��X������s��������4+32%#!!2����ǒ�����-ӵ����/��������������s��I����!!!!!!s��6��u��*������}���s��I�� ��!!!!!s��6��u������������Z����,��".54>32.#"32>5!5!Ʉ`))`���=���1T�P**P�TGm?(��E#V��_���rj���Y��V4[Z��Xc��d0IdY.�e���R����s��q����!!!!!!s� ��������P�Pq�������������3!� ��P�����f��h����332>5!#".f�=nJEd4p�i�wQ&JG|m?>mzJf�����>o����s�� +����!! !!sO��I���:u�����o�x��t�����s��}����!!!s�����@���B�������3! !!!Bh]eg������������P����$��w����� ��3!!!w= ������T��P��3���d��1���-��4.#"32>%4> #".!H�\JuI10JuIJuI1�M(^��Պ]((^�Ԃ�Պ^)�\��Y<a��HN��jBBj��Nd�Õ\\���ek�ўbb�������y��-�����3 !#%!32+!��������ՅEK�Ҁ���7��Dx�[Z�xF�����b��8���2��4.#"327'7>%4>32'#".C�m[�J# 3Fg>^'���!�K']�ن}ω_*H5���:�jy͌c.�ݎV��a<w~kV1%vŃ+�O_�ȚbW���m��Mc�m4?]�����y��������3 !# #"&#!!32�������1B����օE��t7��?E���Dx�[z�9�����S��f��3��%32654.'.54>32&#"#"$S�noy&9_U~-/CM0 Q��_�,��1�]v6c_} 9VT5 �������+e�jU.K11 2'@Hh>b�a2��3tKV(D5'/4KUtE�����N�������!!!!N��7���#���?�����o�������!�! �!! ���������������~U������F�������!!!��_���U!'��P������J��5����!!3!!J�������������j{���P��Y��5�������! ! !!;5 6�X������������'�)��4����B�������! !!B65�<�������?���H������!!!!�����4 �#��@������y��o����!!#3o� +���b^����������!!I��$�������w��k����53#5!w���b��������z|�����!'!3���|���|��U����N��������5!NA���������Jc�1���&5467632#"G� +!*/�".l < ��6�����Z���N��,���"3264254.#"'>32!5#"&6o�srYXh��c�!1F=".Y*'g-�PR��^7���c���e�ba�=e@R;X3 +�$:Jo�k���ψ@e�J����w��'���&��)!>32#"&'4."32>y��4�W`�Z-.Z�_V�4�&@GNJD))DJ'$I@'���Re[���ǛYgR|X�J#!G�Z[�H!*N�����V���L���&#"32>7#"�4�32�Asc��m&7 �H]t>����t�:�r���� (o-PD(2���<i�����J������&��!5#".4>32!32>54."�4�V_�Z.-Z�`W�4�T'@I$'JD))DJNG@&�RgY���ɟ[eR�RP�N*!H�[Z�G!#J������X��J����!.#"27#".54�32!k�nZYo�d6�=�jc��NɄ�t�h-G@�Z���Bb]iV��x�A����DQq5��J������!#5354>32.#"3�����7[�G�[�5+'�L��L�P2ufCY�*-d������J�3�L�#�6��5#".4>325!#".'72632>54."�4�V_�Z.-Z�`W�4<p�pY�cG#� e�s�V'@I$'JD))DJNG@&F�RgY���ɟ[eR���Q�tD0Kb^/CRY�P�N*!H�[Z�G!#J�����w�������)!>32!4."y��-�g7m]9��/3F75#���Rl9i�f�dQvH+,HwP��y��������462"!!yWxWWxE����__�_e��������P������462"73265!#"&yWxWWx�D�42Bo�_Sq��__�_��?0���d�p(��w������� !!!!B�������;U����D��e������������3"&5!�]��W߆������w���N�)��)4&#"!!>32>32!4&"���@QR>��1DY.0P1'9K^.����?�?������1�;=%"/@/&NL0���d���������w���N���)!>32!4."y��-�g7m]9��/3F75#2�Rl9i�f�dQvH+,HwP���\��L���� #"X��D����������KI��/������.�����w�V'L��&��!!>32#"&'4."32>y��4�W`�Z-.Z�_V�4�&@GNJD))DJ'$I@'�VۜRe[���ǛYgR|X�J#!G�Z[�H!*N����J�?eL��,��#".4>325!3"&32>54."�4�V_�Z.-Z�`W�4)B���V'@I$'JD))DJNG@&�*RgY���ɟ[eR��F,�FP�N*!H�[Z�G!#J���z��AN���&#"!!>32�l07R+��*�=2o+'=:ah8��1�Pn5)����N��vL�5��.54632.#"#"&'732>54.V1;; ɩQ�^: �C:)&;?f*sin�k�� �b@(+!ACa�%6=U8��5Vi7+A/$6&(8�~V�K��'<D$-$)��H��q+���#535!3#;#".5Ս���o''k�T#L������_�%OoQ����l���1���)5#"!2>5!���-�c��#1/526)�Qm��OyG,,GyO��H���1���!!3��������1�R�����9��i5���#!33#ˎ�������肪������h1�c��b�������1���)! !!��;���U��"��:��O����J����l�/�1�'��#"&'7326=#"!2>5!�P~�O��/�kOTp-�c��#1/526)O�yL�ep6C^P�Qm��OyG,,GyO�����B���1���%!!!5!������}&��B����P����+��>7>3"3".'.PET +"(,149VH��HV.+)&$ +T�����" +�@X�g:8�eX?�������s��f����33s���R��y����+�� #52657&44�ET + $&)+.VH��HV941,(" +T������?X�8:�X@� +"���������5������>3232>7#"."5+*+:33< M!$3!<'S<F648(& --+"��+080. +����������T�������������F�N���'��.54>7530#".#"32>7#9h�-HQ'�*t%�18OH=.$�*f0�%��F|T8�� P3�P3=Y(�4C���J�����%��#5332.#"!!!267#!>&����}�7�36 0 e��'�3 �F��� +P?%�]\��=F:\a0jD�E�91��&�����51����,��32654&#"#"'!3&547#!6325!#3!�=(&::&*;�(BD'��okN*AJNos���+6;&+98��uN3<>.Nll��B*+D����-��-u���!53!!3!!!!!5!�����������������{������y������P��D�����#53D����f�f��f���!�9�=�)�8��632#"&'7;267#".54>32.#"4.#"3232 B�d>V�X[�:�3 ,@yb;L�Z_uD�05v8! +#8� Fx�a��x�+'A4<p�q~ދk�2"��6iF6nE����H�g�'���������������� �*��4> .#"&4632&"327$326&#"N��&���������Sej��joIq"L88&, �Cؙ��ؚ������������S�ԔIu8&'8�����4����3'^����3#3#P���������^����##����#������^�����#!5!����]^\����X�\����������T�q���'�2�C��2>54.#"4>32#".%232654&+'32#/#=bqrr_<Aem3:ra<�Al��Hj��T��K��i?�0'6��{_�G��U�Q*/U�OX�T,-U�Wo�Z)T��v���+Z~�{'&�Yf- ��������5������!!5\���������bj�� ���2654&"6 �TzSSzT�������?WW?>XX���������������E'��=��!������gc�1���#"&'.547632�.!�2! +l 68 < ������s�y���%#332653#5#"&���(??Cm��i)7�N�%�ZHl8�Y���T4=B���o��t�����(��23"3"&#.54>3!����0+,/���BbjC,_���e��V�M0(<��P��j%?OwJT�dG!�_���]Z����{����9����J���>54.'79-(�l(375�"+Z`o3*) +�����s_����#%#V�����7���_����"#����"#���������}��"���������C�'�C=j�$�������C�'�r�o�$�������Cp'�A����$�������C}'�aH��$�������C''�i��D�$�������C0'�p�^�$���� +��������!!!!!!!!33�����@w�|��{��r��T�����������A����d�&��'�v����& +����s��I�'�C +s�(�����s��I�'�r�s�(�����s��IY'�A�{��(�����s��I5'�i��R�(����������'�C��s�,��������.�'�r�7j�,�����~���Y'�A���,���������A'�i�B^�,�������M�����#53!2+32+3}}����_a��������߫��d�]r��������qd������������w���h'�a��1�����d��1�'�CD��2�����d��1�'�r���2�����d��1r'�A����2�����d��1�'�a`�2�����d��1h'�i����2�����!��,��,�-��c,�����d��1���)�6��!7&54>327!#"732>54' "&#"! ��Cer*_�Ճd<=yl#Z�ۈV @jF5 >��X&O{J/"�eX�s�ǓX�n���V�Ѥl�1Ugwk3Ȍ�/Af��L9�����o����'�Cs�8�����o����'�r�o�8�����o���v'�A����8�����o���C'�i��`�8�����B����'�r�o�<�������0�����2>54!!2#!�>llL0�n��[��y^2Lz��o��1Q4����'E`�Wp�d=�����>�����:��!2#"'732>54&+2>?2>54&#" +#6��[��hAJI2����2b!/Z�XȦ#6dc<efi~#���ɂ�<\�H��4�&^Hh��/_BrkQI,������5+=B������Z���H'�C����D�����Z���C'�r-��D�����Z����&�A��D�������Z���'�a��w�D�����Z����'�i�3��D�����Z����'�p�����D����Z��xN��@�G���"326427#"'!5#"&632<54.#"'>32>32!!.#"o�srYXh�d6�=�j{k���c��Җb�!1F=".Y*'g-�Pz�DB�_��t�h-G@��nZYoe�ba��Bb]i@#�@e�J�e?9;X3 +�$:JOGN����DQq5�Z�����V�+�L'�v���F�����X��H'�C����H�����X��E'�rT��H�����X���&�A;�H�������X���'�i�h��H��������?����!!&5467632#"� ��X� +!*/�".1��z < ��6������9=����!!#"&'.547632� ��.!�2! +1��x 68 < ������������ +��!!!'!3� ����|���1��z��U���������������!!462"&%462#"&� ���`�``�`�`�``DCa1��+D``�`aCD``�`a�����H������8��32654&#"�#".54324&''7.'#"'67327Jq;Lr�R;V�B{���\�dB�F~*_0Y�T8*<.&nU�,w�Ӄ��op���M��sNR+=���8`��T�\=7�-R�I +�8"o�����w���'�a����Q�����\��L'�C����R�����\��L'�rN��R�����\���&�A!��R�������\��'�a��}�R�����\���'�i�\��R�����v�uz�&�������5��%N���*�� 32>54'&#"7.546327!�#"&'���(:c6�Ej3 �ȉ*8sޕ*s /7���2r"{�bc�QB�h�S?�)�;�e���3�D�j���/���l���V'�C���%�X�����l���C'�r8��X�����l����&�A#�X�������l����'�i�Z��X�����l�/�3'�ra��\����w������"��)!>32#".'4&#"326y��~B��_Bc�a$E1{}@MyqKMz���=o����a��vG"3)��։������l�/��'�i�J��\�����������!!������������N����!!���R��������}����!!���������� WON��������&NlE�������� �Df;�����������W[N������W�P'����������N�G'��s��������$�B�;����������Q�J������}��H�1��#53>32&#"!!!!32>7#"&'#53'&54��1�j�R�S~7z ����~+40>!�!8VvL��!������]j�gL3� �%3 $�)44�����������������������s���������������&�������� +�������@������� +g������ ����������������������� �8�� ������� �� +g�� ��y�� ��L��� ����� �� �� ��Q�� � �r�� ��&��� ��&��� � ��C�r�e�a�t�e�d� �b�y� �T�h�a�t�c�h�e�r� �U�l�r�i�c�h� �(�h�t�t�p�:�/�/�t�u�l�r�i�c�h�.�c�o�m�)� �w�i�t�h� �F�o�n�t�F�o�r�g�e� �1�.�0� �(�h�t�t�p�:�/�/�f�o�n�t�f�o�r�g�e�.�s�f�.�n�e�t�)� +� +�T�h�i�s� �f�o�n�t�,� �i�n�c�l�u�d�i�n�g� �h�i�n�t� �i�n�s�t�r�u�c�t�i�o�n�s�,� �h�a�s� �b�e�e�n� �d�o�n�a�t�e�d� �t�o� �t�h�e� �P�u�b�l�i�c� �D�o�m�a�i�n�.� � �D�o� �w�h�a�t�e�v�e�r� �y�o�u� �w�a�n�t� �w�i�t�h� �i�t�.� +��Created by Thatcher Ulrich (http://tulrich.com) with FontForge 1.0 (http://fontforge.sf.net) + +This font, including hint instructions, has been donated to the Public Domain. Do whatever you want with it. +��T�u�f�f�y��Tuffy��B�o�l�d��Bold��F�o�n�t�F�o�r�g�e� �1�.�0� �:� �T�u�f�f�y� �B�o�l�d� �:� �1�1�-�2�-�2�0�0�7��FontForge 1.0 : Tuffy Bold : 11-2-2007��T�u�f�f�y� �B�o�l�d��Tuffy Bold��V�e�r�s�i�o�n� �0�0�1�.�1�0�0� ��Version 001.100 ��T�u�f�f�y�-�B�o�l�d��Tuffy-Bold��T�h�a�t�c�h�e�r� �U�l�r�i�c�h��Thatcher Ulrich��h�t�t�p�:�/�/�t�u�l�r�i�c�h�.�c�o�m� +��http://tulrich.com +��h�t�t�p�:�/�/�t�u�l�r�i�c�h�.�c�o�m� +��http://tulrich.com +��P�u�b�l�i�c� �D�o�m�a�i�n� +��Public Domain +������������4�f��������������������������������� � +��� ������������������� �!�"�#�$�%�&�'�(�)�*�+�,�-�.�/�0�1�2�3�4�5�6�7�8�9�:�;�<�=�>�?�@�A�B�C�D�E�F�G�H�I�J�K�L�M�N�O�P�Q�R�S�T�U�V�W�X�Y�Z�[�\�]�^�_�`�a�����������������������������������������������������b�c���d���e���������������f���������g�����������h�������j�i�k�m�l�n���o�q�p�r�s�u�t�v�w���x�z�y�{�}�|������~�������������������������� +softhyphen +figuredash quotereverseduni201FEuro������������������������������������ +���latn������������������ +��,�latn������������kern���������������������0�6�<�R�X�^�d�n�t�~�������������������������q��&���7���9�+�Y�\�Z����5�)��7����2����$���7����$�w��$���X�y��$��(���H����$���H����$�B�D����?�q��Y����W����D���Q���R���W���X���\����Y���[����H����H�������$�(�*�.�2�3�7�9�:�<�?�D�H�I�R�U�Y���������?���������������� \ No newline at end of file diff --git a/web/modules/captcha/image_captcha/image_captcha.admin.inc b/web/modules/captcha/image_captcha/image_captcha.admin.inc new file mode 100755 index 0000000000000000000000000000000000000000..6c3dd0bfc3ace9dbd56b209ef67292b768c2b22f --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.admin.inc @@ -0,0 +1,69 @@ +<?php + +/** + * @file + * Contains functions used in the backend forms. + */ + +/** + * Menu handler for font preview request. + * + * @param string $font_token + * Name of font to generate image from. + */ +function image_captcha_font_preview($font_token) { + // Get the font from the given font token. + if ($font_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[$font_token])) { + print 'Bad token'; + exit(); + } + // Get the font path. + $font = $fonts[$font_token]->uri; + // Some sanity checks if the given font is valid. + if (!is_file($font) || !is_readable($font)) { + print 'Bad font'; + exit(); + } + } + + // Settings of the font preview. + $width = 120; + $text = 'AaBbCc123'; + $font_size = 14; + $height = 2 * $font_size; + + // Allocate image resource. + $image = imagecreatetruecolor($width, $height); + if (!$image) { + exit(); + } + // White background and black foreground. + $background_color = imagecolorallocate($image, 255, 255, 255); + $color = imagecolorallocate($image, 0, 0, 0); + imagefilledrectangle($image, 0, 0, $width, $height, $background_color); + + // Draw preview text. + if ($font == 'BUILTIN') { + imagestring($image, 5, 1, .5 * $height - 10, $text, $color); + } + else { + imagettftext($image, $font_size, 0, 1, 1.5 * $font_size, $color, realpath($font), $text); + } + + // Set content type. + drupal_add_http_header('Content-Type', 'image/png'); + // Dump image data to client. + imagepng($image); + // Release image memory. + imagedestroy($image); + + // Close connection. + exit(); +} diff --git a/web/modules/captcha/image_captcha/image_captcha.css b/web/modules/captcha/image_captcha/image_captcha.css new file mode 100755 index 0000000000000000000000000000000000000000..dd7f4789e5f242e83326fa74dfebd77330ec2b5c --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.css @@ -0,0 +1,29 @@ +/** + * Styling of the font selection list (with previews) + * on the Image CAPTCHA settings page. + */ + +/** + * Float the fonts with preview (with a fixed width) + * to create a multi-column layout. + */ +.image_captcha_admin_fonts_selection .form-item { + float: left; + width: 160px; +} + +/** + * Stop floating with the item for the built in font. + */ +.image_captcha_admin_fonts_selection .form-item-image-captcha-fonts-BUILTIN { + clear: both; + float: none; + width: 100%; +} + +/** + * Center the font previews vertically to the text. + */ +.image_captcha_admin_fonts_selection img { + vertical-align: middle; +} diff --git a/web/modules/captcha/image_captcha/image_captcha.info.yml b/web/modules/captcha/image_captcha/image_captcha.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..049fe1ffa57d7dccef00ad0565dd57fe1387f339 --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.info.yml @@ -0,0 +1,14 @@ +name: Image CAPTCHA +type: module +description: Provides an image based CAPTCHA. +package: Spam control +# core: 8.x +dependencies: + - captcha +configure: admin/config/people/captcha/image_captcha + +# Information added by Drupal.org packaging script on 2017-02-15 +version: '8.x-1.0-beta1' +core: '8.x' +project: 'captcha' +datestamp: 1487198589 diff --git a/web/modules/captcha/image_captcha/image_captcha.install b/web/modules/captcha/image_captcha/image_captcha.install new file mode 100755 index 0000000000000000000000000000000000000000..86bde6800ee861caea1a3ac9b5416c8999c1748e --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.install @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Installation/uninstallation related functions for the image_captcha module. + */ + +/** + * Implements hook_requirements(). + */ +function image_captcha_requirements($phase) { + $requirements = []; + if ($phase == 'install') { + // _image_captcha_check_setup() is defined in image_captcha.module. + // Using 'module_load_include' returns FALSE so 'include_once' used instead. + include_once __DIR__ . '/image_captcha.module'; + // Check if the GD library is available and raise an error when not. + if (_image_captcha_check_setup(FALSE) & IMAGE_CAPTCHA_ERROR_NO_GDLIB) { + $requirements['image_captcha_requires_gd'] = [ + 'title' => \Drupal::translation() + ->translate('Image CAPTCHA requires GD library'), + 'description' => + \Drupal::translation() + ->translate('The Image CAPTCHA module can not be installed because your PHP setup does not provide the <a href="!gddoc">GD library</a>, which is required to generate images.', + ['!gddoc' => 'http://www.php.net/manual/en/book.image.php'] + ), + 'severity' => REQUIREMENT_ERROR, + ]; + } + } + return $requirements; +} + +/** + * Implements hook_install(). + */ +function image_captcha_install() { + $config = \Drupal::configFactory()->getEditable('image_captcha.settings'); + + $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', + ])->save(TRUE); +} diff --git a/web/modules/captcha/image_captcha/image_captcha.js b/web/modules/captcha/image_captcha/image_captcha.js new file mode 100755 index 0000000000000000000000000000000000000000..0bdaa9c8aab0b0f38f775268b7e5701922493db2 --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.js @@ -0,0 +1,52 @@ +/** + * @file + * Contains helper js for Captcha admin pages. + * + * @TODO remove and use native states. + */ + +(function ($) { + 'use strict'; + + Drupal.behaviors.captchaAdmin = { + attach: function (context) { + + // Helper function to show/hide noise level widget. + var noise_level_shower = function (speed) { + speed = (typeof speed == 'undefined') ? 'slow' : speed; + if ($('#edit-image-captcha-dot-noise').is(':checked') + || $('#edit-image-captcha-line-noise').is(':checked')) { + $('.form-item-image-captcha-noise-level').show(speed); + } + else { + $('.form-item-image-captcha-noise-level').hide(speed); + } + }; + + // Add onclick handler to the dot and line noise check boxes. + $('#edit-image-captcha-dot-noise').click(noise_level_shower); + $('#edit-image-captcha-line-noise').click(noise_level_shower); + // Show or hide appropriately on page load. + noise_level_shower(0); + + // Helper function to show/hide smooth distortion widget. + var smooth_distortion_shower = function (speed) { + speed = (typeof speed == 'undefined') ? 'slow' : speed; + if ($('#edit-image-captcha-distortion-amplitude').val() > 0) { + $('.form-item-image-captcha-bilinear-interpolation').show(speed); + } + else { + $('.form-item-image-captcha-bilinear-interpolation').hide(speed); + } + }; + + // Add onchange handler to the distortion level select widget. + $('#edit-image-captcha-distortion-amplitude').change( + smooth_distortion_shower); + // Show or hide appropriately on page load. + smooth_distortion_shower(0); + + } + }; + +})(jQuery); diff --git a/web/modules/captcha/image_captcha/image_captcha.libraries.yml b/web/modules/captcha/image_captcha/image_captcha.libraries.yml new file mode 100755 index 0000000000000000000000000000000000000000..44ff61d7b22b7c44639522b512ca779018ba9c38 --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.libraries.yml @@ -0,0 +1,12 @@ +base: + version: 1.0 + js: + image_captcha.js: {} + css: + all: + image_captcha.css: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings + - core/jquery.once diff --git a/web/modules/captcha/image_captcha/image_captcha.module b/web/modules/captcha/image_captcha/image_captcha.module new file mode 100755 index 0000000000000000000000000000000000000000..83e0214c82a9949884fb6032c8745cc75a21222f --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.module @@ -0,0 +1,278 @@ +<?php + +/** + * @file + * Implements image CAPTCHA for use with the CAPTCHA module. + */ + +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; + +define('IMAGE_CAPTCHA_ALLOWED_CHARACTERS', 'aAbBCdEeFfGHhijKLMmNPQRrSTtWXYZ23456789'); + +// Setup status flags. +define('IMAGE_CAPTCHA_ERROR_NO_GDLIB', 1); +define('IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT', 2); +define('IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM', 4); + +define('IMAGE_CAPTCHA_FILE_FORMAT_JPG', 1); +define('IMAGE_CAPTCHA_FILE_FORMAT_PNG', 2); +define('IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG', 3); + +/** + * Implements hook_help(). + */ +function image_captcha_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'image_captcha.settings': + $output = '<p>' . t('The image CAPTCHA is a popular challenge where a random textual code is obfuscated in an image. The image is generated on the fly for each request, which is rather CPU intensive for the server. Be careful with the size and computation related settings.') . '</p>'; + return $output; + } +} + +/** + * Getter for fonts to use in the image CAPTCHA. + * + * @return array + * List of font paths. + */ +function _image_captcha_get_enabled_fonts() { + if (IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT & _image_captcha_check_setup(FALSE)) { + return ['BUILTIN']; + } + else { + return \Drupal::config('image_captcha.settings') + ->get('image_captcha_fonts'); + } +} + +/** + * Helper function for checking if the specified fonts are available. + * + * @param array $fonts + * Paths of fonts to check. + * + * @return array + * List($readable_fonts, $problem_fonts). + */ +function _image_captcha_check_fonts($fonts) { + $readable_fonts = []; + $problem_fonts = []; + + foreach ($fonts as $font) { + if ($font != 'BUILTIN' && (!is_file($font) || !is_readable($font))) { + $problem_fonts[] = $font; + } + else { + $readable_fonts[] = $font; + } + } + + return [$readable_fonts, $problem_fonts]; +} + +/** + * Helper function for splitting an utf8 string correctly in characters. + * + * Assumes the given utf8 string is well formed. + * See http://en.wikipedia.org/wiki/Utf8 for more info. + * + * @param string $str + * UTF8 string to be split. + * + * @return array + * List of caracters given string consists of. + */ +function _image_captcha_utf8_split($str) { + $characters = []; + $len = strlen($str); + + for ($i = 0; $i < $len;) { + $chr = ord($str[$i]); + // One byte character (0zzzzzzz). + if (($chr & 0x80) == 0x00) { + $width = 1; + } + else { + // Two byte character (first byte: 110yyyyy). + if (($chr & 0xE0) == 0xC0) { + $width = 2; + } + // Three byte character (first byte: 1110xxxx). + elseif (($chr & 0xF0) == 0xE0) { + $width = 3; + } + // Four byte character (first byte: 11110www). + elseif (($chr & 0xF8) == 0xF0) { + $width = 4; + } + else { + \Drupal::logger('CAPTCHA') + ->error('Encountered an illegal byte while splitting an utf8 string in characters.'); + return $characters; + } + } + + $characters[] = substr($str, $i, $width); + $i += $width; + } + + return $characters; +} + +/** + * Helper function for checking the setup of the Image CAPTCHA. + * + * The image CAPTCHA requires at least the GD PHP library. + * Support for TTF is recommended and the enabled + * font files should be readable. + * This functions checks these things. + * + * @param bool $check_fonts + * Whether or not the enabled fonts should be checked. + * + * @return int + * Status code: bitwise 'OR' of status flags like + * IMAGE_CAPTCHA_ERROR_NO_GDLIB, IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT, + * IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM. + */ +function _image_captcha_check_setup($check_fonts = TRUE) { + $status = 0; + // Check if we can use the GD library. + // We need at least the imagepng function. + // Note that the imagejpg function is optionally also used, but not required. + if (!function_exists('imagepng')) { + $status = $status | IMAGE_CAPTCHA_ERROR_NO_GDLIB; + } + + if (!function_exists('imagettftext')) { + $status = $status | IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT; + } + + if ($check_fonts) { + // Check availability of enabled fonts. + $fonts = _image_captcha_get_enabled_fonts(); + $readable_fonts = []; + list($readable_fonts, $problem_fonts) = _image_captcha_check_fonts($fonts); + if (count($problem_fonts) != 0) { + $status = $status | IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM; + } + } + + return $status; +} + +/** + * Helper function for calculating image height and width. + * + * They are based on given code and current font/spacing settings. + * + * @param string $code + * The utf8 string which will be used to split in characters. + * + * @return array + * Array($width, $heigh). + */ +function _image_captcha_image_size($code) { + $config = \Drupal::config('image_captcha.settings'); + $font_size = (int) $config->get('image_captcha_font_size'); + $character_spacing = (float) $config->get('image_captcha_character_spacing'); + $characters = _image_captcha_utf8_split($code); + $character_quantity = count($characters); + + // Calculate height and width. + $width = $character_spacing * $font_size * $character_quantity; + $height = 2 * $font_size; + + return [$width, $height]; +} + +/** + * Implements hook_captcha(). + */ +function image_captcha_captcha($op, $captcha_type = '', $captcha_sid = NULL) { + $config = \Drupal::config('image_captcha.settings'); + + switch ($op) { + case 'list': + // Only offer the image CAPTCHA if it is possible to generate an image + // on this setup. + if (!(_image_captcha_check_setup() & IMAGE_CAPTCHA_ERROR_NO_GDLIB)) { + return ['Image']; + } + else { + return []; + } + break; + + case 'generate': + if ($captcha_type == 'Image') { + // In maintenance mode, the image CAPTCHA does not work because + // the request for the image itself won't succeed (only ?q=user + // is permitted for unauthenticated users). We fall back to the + // Math CAPTCHA in that case. + if (defined('MAINTENANCE_MODE') && \Drupal::currentUser() + ->isAnonymous() + ) { + return captcha_captcha('generate', 'Math'); + } + // Generate a CAPTCHA code. + $allowed_chars = _image_captcha_utf8_split($config->get('image_captcha_image_allowed_chars')); + $code_length = (int) $config->get('image_captcha_code_length'); + $code = ''; + + for ($i = 0; $i < $code_length; $i++) { + $code .= $allowed_chars[array_rand($allowed_chars)]; + } + + // Build the result to return. + $result = []; + + $result['solution'] = $code; + // Generate image source URL (add timestamp to avoid problems with + // client side caching: subsequent images of the same CAPTCHA session + // have the same URL, but should display a different code). + list($width, $height) = _image_captcha_image_size($code); + $result['form']['captcha_image'] = [ + '#theme' => 'image', + '#uri' => Url::fromRoute('image_captcha.generator', [ + 'session_id' => $captcha_sid, + 'timestamp' => REQUEST_TIME, + ])->toString(), + '#width' => $width, + '#height' => $height, + '#alt' => t('Image CAPTCHA'), + '#title' => t('Image CAPTCHA'), + '#weight' => -2, + ]; + + $result['form']['captcha_response'] = [ + '#type' => 'textfield', + '#title' => t('What code is in the image?'), + '#description' => t('Enter the characters shown in the image.'), + '#weight' => 0, + '#required' => TRUE, + '#size' => 15, + '#attributes' => ['autocomplete' => 'off'], + '#cache' => ['max-age' => 0], + ]; + + // Handle the case insensitive validation option combined with + // ignoring spaces. + switch (\Drupal::config('captcha.settings') + ->get('default_validation')) { + case CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE: + $result['captcha_validate'] = 'captcha_validate_ignore_spaces'; + break; + + case CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE: + $result['captcha_validate'] = 'captcha_validate_case_insensitive_ignore_spaces'; + break; + } + \Drupal::service('page_cache_kill_switch')->trigger(); + + return $result; + } + break; + } +} diff --git a/web/modules/captcha/image_captcha/image_captcha.routing.yml b/web/modules/captcha/image_captcha/image_captcha.routing.yml new file mode 100755 index 0000000000000000000000000000000000000000..4a8e52de07ec5611f62217c6d8a5b3dcdf33c931 --- /dev/null +++ b/web/modules/captcha/image_captcha/image_captcha.routing.yml @@ -0,0 +1,20 @@ +image_captcha.settings: + path: '/admin/config/people/captcha/image_captcha' + defaults: + _form: '\Drupal\image_captcha\Form\ImageCaptchaSettingsForm' + requirements: + _permission: 'administer CAPTCHA settings' + +image_captcha.font_preview: + path: '/admin/config/people/captcha/image_captcha/font_preview/{token}' + defaults: + _controller: '\Drupal\image_captcha\Controller\CaptchaFontPreview::content' + requirements: + _permission: 'administer CAPTCHA settings' + +image_captcha.generator: + path: '/image-captcha-generate/{session_id}/{timestamp}' + defaults: + _controller: '\Drupal\image_captcha\Controller\CaptchaImageGeneratorController::image' + requirements: + _access: 'TRUE' diff --git a/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreview.php b/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreview.php new file mode 100755 index 0000000000000000000000000000000000000000..c843153f3173f6901ba17dd85efbea7d28c712db --- /dev/null +++ b/web/modules/captcha/image_captcha/src/Controller/CaptchaFontPreview.php @@ -0,0 +1,73 @@ +<?php + +namespace Drupal\image_captcha\Controller; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * A Controller to preview the captcha font on the settings page. + */ +class CaptchaFontPreview extends StreamedResponse { + + /** + * {@inheritdoc} + */ + public function content(Request $request) { + $token = $request->get('token'); + // Get the font from the given font token. + if ($token == 'BUILTIN') { + $font = 'BUILTIN'; + } + else { + // Get the mapping of font tokens to font file objects. + $fonts = \Drupal::config('image_captcha.settings') + ->get('image_captcha_fonts_preview_map_cache'); + if (!isset($fonts[$token])) { + echo 'bad token'; + exit(); + } + // Get the font path. + $font = $fonts[$token]['uri']; + // Some sanity checks if the given font is valid. + if (!is_file($font) || !is_readable($font)) { + echo 'bad font'; + exit(); + } + } + + // Settings of the font preview. + $width = 120; + $text = 'AaBbCc123'; + $font_size = 14; + $height = 2 * $font_size; + + // Allocate image resource. + $image = imagecreatetruecolor($width, $height); + if (!$image) { + exit(); + } + // White background and black foreground. + $background_color = imagecolorallocate($image, 255, 255, 255); + $color = imagecolorallocate($image, 0, 0, 0); + imagefilledrectangle($image, 0, 0, $width, $height, $background_color); + + // Draw preview text. + if ($font == 'BUILTIN') { + imagestring($image, 5, 1, .5 * $height - 10, $text, $color); + } + else { + imagettftext($image, $font_size, 0, 1, 1.5 * $font_size, $color, realpath($font), $text); + } + // Set content type. + $this->headers->set('Content-Type', 'image/png'); + // Dump image data to client. + imagepng($image); + // Release image memory. + imagedestroy($image); + + // Close connection. + exit(); + } + +} diff --git a/web/modules/captcha/image_captcha/src/Controller/CaptchaImageGeneratorController.php b/web/modules/captcha/image_captcha/src/Controller/CaptchaImageGeneratorController.php new file mode 100644 index 0000000000000000000000000000000000000000..408c115efead14e3a0e687cd35daaad35e090867 --- /dev/null +++ b/web/modules/captcha/image_captcha/src/Controller/CaptchaImageGeneratorController.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\image_captcha\Controller; + +use Drupal\Core\Config\Config; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Logger\LoggerChannelInterface; +use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; +use Drupal\image_captcha\Response\CaptchaImageResponse; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Controller which generates the image from defined settings. + */ +class CaptchaImageGeneratorController implements ContainerInjectionInterface { + + /** + * Image Captcha config storage. + * + * @var Config + */ + protected $config; + + /** + * Watchdog logger channel for captcha. + * + * @var LoggerChannelInterface + */ + protected $logger; + + /** + * Kill Switch for page caching. + * + * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch + */ + protected $killSwitch; + + /** + * {@inheritdoc} + */ + public function __construct(Config $config, LoggerChannelInterface $logger, KillSwitch $kill_switch) { + $this->config = $config; + $this->logger = $logger; + $this->killSwitch = $kill_switch; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory')->get('image_captcha.settings'), + $container->get('logger.factory')->get('captcha'), + $container->get('page_cache_kill_switch') + ); + } + + /** + * Main method that throw ImageResponse object to generate image. + * + * @return CaptchaImageResponse + * Make a CaptchaImageResponse with the correct configuration and return it. + */ + public function image() { + $this->killSwitch->trigger(); + return new CaptchaImageResponse($this->config, $this->logger); + } + +} diff --git a/web/modules/captcha/image_captcha/src/Form/ImageCaptchaSettingsForm.php b/web/modules/captcha/image_captcha/src/Form/ImageCaptchaSettingsForm.php new file mode 100755 index 0000000000000000000000000000000000000000..22005181193e7c7531257e02c1b891a4b21d51c3 --- /dev/null +++ b/web/modules/captcha/image_captcha/src/Form/ImageCaptchaSettingsForm.php @@ -0,0 +1,451 @@ +<?php + +namespace Drupal\image_captcha\Form; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\DrupalKernel; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Template\Attribute; +use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Displays the pants settings form. + */ +class ImageCaptchaSettingsForm extends ConfigFormBase { + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Constructs a \Drupal\image_captcha\Form\ImageCaptchaSettingsForm object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + */ + public function __construct(ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) { + parent::__construct($config_factory); + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'image_captcha_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['image_captcha.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('image_captcha.settings'); + // Add CSS and JS for theming and added usability on admin form. + $form['#attached']['library'][] = 'captcha_image/base'; + + // First some error checking. + $setup_status = _image_captcha_check_setup(FALSE); + if ($setup_status & IMAGE_CAPTCHA_ERROR_NO_GDLIB) { + drupal_set_message($this->t( + 'The Image CAPTCHA module can not generate images because your PHP setup does not support it (no <a href="!gdlib" target="_blank">GD library</a> with JPEG support).', + ['!gdlib' => 'http://php.net/manual/en/book.image.php'] + ), 'error'); + // It is no use to continue building the rest of the settings form. + return $form; + } + + $form['image_captcha_example'] = [ + '#type' => 'details', + '#title' => $this->t('Example'), + '#description' => $this->t('Presolved image CAPTCHA example, generated with the current settings.'), + ]; + + $form['image_captcha_example']['image'] = [ + '#type' => 'captcha', + '#captcha_type' => 'image_captcha/Image', + '#captcha_admin_mode' => TRUE, + ]; + + // General code settings. + $form['image_captcha_code_settings'] = [ + '#type' => 'details', + '#title' => $this->t('Code settings'), + ]; + + $form['image_captcha_code_settings']['image_captcha_image_allowed_chars'] = [ + '#type' => 'textfield', + '#title' => $this->t('Characters to use in the code'), + '#default_value' => $config->get('image_captcha_image_allowed_chars'), + ]; + $form['image_captcha_code_settings']['image_captcha_code_length'] = [ + '#type' => 'select', + '#title' => $this->t('Code length'), + '#options' => [2 => 2, 3, 4, 5, 6, 7, 8, 9, 10], + '#default_value' => $config->get('image_captcha_code_length'), + '#description' => $this->t('The code length influences the size of the image. Note that larger values make the image generation more CPU intensive.'), + ]; + // RTL support option (only show this option when there are RTL languages). + $language = $this->languageManager->getCurrentLanguage(); + if ($language->getDirection() == Language::DIRECTION_RTL) { + $form['image_captcha_code_settings']['image_captcha_rtl_support'] = [ + '#title' => $this->t('RTL support'), + '#type' => 'checkbox', + '#default_value' => $config->get('image_captcha_rtl_support'), + '#description' => $this->t('Enable this option to render the code from right to left for right to left languages.'), + ]; + } + + // Font related stuff. + $form['image_captcha_font_settings'] = $this->settingsDotSection(); + + // Color and file format settings. + $form['image_captcha_color_settings'] = [ + '#type' => 'details', + '#title' => $this->t('Color and image settings'), + '#description' => $this->t('Configuration of the background, text colors and file format of the image CAPTCHA.'), + ]; + + $form['image_captcha_color_settings']['image_captcha_background_color'] = [ + '#type' => 'textfield', + '#title' => $this->t('Background color'), + '#description' => $this->t('Enter the hexadecimal code for the background color (e.g. #FFF or #FFCE90). When using the PNG file format with transparent background, it is recommended to set this close to the underlying background color.'), + '#default_value' => $config->get('image_captcha_background_color'), + '#maxlength' => 7, + '#size' => 8, + ]; + $form['image_captcha_color_settings']['image_captcha_foreground_color'] = [ + '#type' => 'textfield', + '#title' => $this->t('Text color'), + '#description' => $this->t('Enter the hexadecimal code for the text color (e.g. #000 or #004283).'), + '#default_value' => $config->get('image_captcha_foreground_color'), + '#maxlength' => 7, + '#size' => 8, + ]; + $form['image_captcha_color_settings']['image_captcha_foreground_color_randomness'] = [ + '#type' => 'select', + '#title' => $this->t('Additional variation of text color'), + '#options' => [ + 0 => $this->t('No variation'), + 50 => $this->t('Little variation'), + 100 => $this->t('Medium variation'), + 150 => $this->t('High variation'), + 200 => $this->t('Very high variation'), + ], + '#default_value' => $config->get('image_captcha_foreground_color_randomness'), + '#description' => $this->t('The different characters will have randomized colors in the specified range around the text color.'), + ]; + $form['image_captcha_color_settings']['image_captcha_file_format'] = [ + '#type' => 'select', + '#title' => $this->t('File format'), + '#description' => $this->t('Select the file format for the image. JPEG usually results in smaller files, PNG allows tranparency.'), + '#default_value' => $config->get('image_captcha_file_format'), + '#options' => [ + IMAGE_CAPTCHA_FILE_FORMAT_JPG => $this->t('JPEG'), + IMAGE_CAPTCHA_FILE_FORMAT_PNG => $this->t('PNG'), + IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG => $this->t('PNG with transparent background'), + ], + ]; + + // Distortion and noise settings. + $form['image_captcha_distortion_and_noise'] = [ + '#type' => 'details', + '#title' => $this->t('Distortion and noise'), + '#description' => $this->t('With these settings you can control the degree of obfuscation by distortion and added noise. Do not exaggerate the obfuscation and assure that the code in the image is reasonably readable. For example, do not combine high levels of distortion and noise.'), + ]; + + $form['image_captcha_distortion_and_noise']['image_captcha_distortion_amplitude'] = [ + '#type' => 'select', + '#title' => $this->t('Distortion level'), + '#options' => [ + 0 => $this->t('@level - no distortion', ['@level' => '0']), + 1 => $this->t('@level - low', ['@level' => '1']), + 2 => '2', + 3 => '3', + 4 => '4', + 5 => $this->t('@level - medium', ['@level' => '5']), + 6 => '6', + 7 => '7', + 8 => '8', + 9 => '9', + 10 => $this->t('@level - high', ['@level' => '10']), + ], + '#default_value' => $config->get('image_captcha_distortion_amplitude'), + '#description' => $this->t('Set the degree of wave distortion in the image.'), + ]; + $form['image_captcha_distortion_and_noise']['image_captcha_bilinear_interpolation'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Smooth distortion'), + '#default_value' => $config->get('image_captcha_bilinear_interpolation'), + '#description' => $this->t('This option enables bilinear interpolation of the distortion which makes the image look smoother, but it is more CPU intensive.'), + ]; + + $form['image_captcha_distortion_and_noise']['image_captcha_dot_noise'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Add salt and pepper noise'), + '#default_value' => $config->get('image_captcha_dot_noise'), + '#description' => $this->t('This option adds randomly colored point noise.'), + ]; + + $form['image_captcha_distortion_and_noise']['image_captcha_line_noise'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Add line noise'), + '#default_value' => $config->get('image_captcha_line_noise', 0), + '#description' => $this->t('This option enables lines randomly drawn on top of the text code.'), + ]; + + $form['image_captcha_distortion_and_noise']['image_captcha_noise_level'] = [ + '#type' => 'select', + '#title' => $this->t('Noise level'), + '#options' => [ + 1 => '1 - ' . $this->t('low'), + 2 => '2', + 3 => '3 - ' . $this->t('medium'), + 4 => '4', + 5 => '5 - ' . $this->t('high'), + 7 => '7', + 10 => '10 - ' . $this->t('severe'), + ], + '#default_value' => (int) $config->get('image_captcha_noise_level'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + + // Check image_captcha_image_allowed_chars for spaces. + if (preg_match('/\s/', $form_state->getValue('image_captcha_image_allowed_chars'))) { + $form_state->setErrorByName('image_captcha_image_allowed_chars', $this->t('The list of characters to use should not contain spaces.')); + } + + if (!isset($form['image_captcha_font_settings']['no_ttf_support'])) { + // Check the selected fonts. + // Filter the image_captcha fonts array to pick out the selected ones. + $fonts = array_filter($form_state->getValue('image_captcha_fonts')); + if (count($fonts) < 1) { + $form_state->setErrorByName('image_captcha_fonts', $this->t('You need to select at least one font.')); + } + if ($form_state->getValue('image_captcha_fonts')['BUILTIN']) { + // With the built in font, only latin2 characters should be used. + if (preg_match('/[^a-zA-Z0-9]/', $form_state->getValue('image_captcha_image_allowed_chars'))) { + $form_state->setErrorByName('image_captcha_image_allowed_chars', $this->t('The built-in font only supports Latin2 characters. Only use "a" to "z" and numbers.')); + } + } + + $readable_fonts = []; + 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)])); + } + } + + // Check color settings. + if (!preg_match('/^#([0-9a-fA-F]{3}){1,2}$/', $form_state->getValue('image_captcha_background_color'))) { + $form_state->setErrorByName('image_captcha_background_color', $this->t('Background color is not a valid hexadecimal color value.')); + } + if (!preg_match('/^#([0-9a-fA-F]{3}){1,2}$/', $form_state->getValue('image_captcha_foreground_color'))) { + $form_state->setErrorByName('image_captcha_foreground_color', $this->t('Text color is not a valid hexadecimal color value.')); + } + + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + 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(); + } + + parent::SubmitForm($form, $form_state); + } + + /** + * Form elements for the font specific setting. + * + * This is refactored to a separate function to avoid polluting the + * general form function image_captcha_settings_form with some + * specific logic. + * + * @return array + * The font settings specific form elements. + */ + protected function settingsDotSection() { + $config = $this->config('image_captcha.settings'); + // Put it all in a details element. + $form = [ + '#type' => 'details', + '#title' => $this->t('Font settings'), + ]; + + // First check if there is TrueType support. + $setup_status = _image_captcha_check_setup(FALSE); + if ($setup_status & IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT) { + // Show a warning that there is no TrueType support. + $form['no_ttf_support'] = [ + '#type' => 'item', + '#title' => $this->t('No TrueType support'), + '#markup' => $this->t('The Image CAPTCHA module can not use TrueType fonts because your PHP setup does not support it. You can only use a PHP built-in bitmap font of fixed size.'), + ]; + } + else { + // Build a list of all available fonts. + $available_fonts = []; + + // List of folders to search through for TrueType fonts. + $fonts = $this->getAvailableFontsFromDirectories(); + // Cache the list of previewable fonts. All the previews are done + // in separate requests, and we don't want to rescan the filesystem + // every time, so we cache the result. + $config->set('image_captcha_fonts_preview_map_cache', $fonts); + $config->save(); + // Put these fonts with preview image in the list. + foreach ($fonts as $token => $font) { + + $title = t('Font preview of @font (@file)', [ + '@font' => $font['name'], + '@file' => $font['uri'], + ]); + $attributes = [ + 'src' => Url::fromRoute('image_captcha.font_preview', ['token' => $token]) + ->toString(), + 'title' => $title, + 'alt' => $title, + ]; + $available_fonts[$font['uri']] = '<img' . new Attribute($attributes) . ' />'; + } + + // Append the PHP built-in font at the end. + $title = t('Preview of built-in font'); + $attributes = [ + 'src' => Url::fromRoute('image_captcha.font_preview', ['token' => 'BUILTIN']) + ->toString(), + 'alt' => $title, + 'title' => $title, + ]; + $available_fonts['BUILTIN'] = (string) t('PHP built-in font: font_preview', [ + 'font_preview' => '<img' . new Attribute($attributes) . ' />', + ]); + + $default_fonts = _image_captcha_get_enabled_fonts(); + $conf_path = DrupalKernel::findSitePath($this->getRequest()); + + $form['image_captcha_fonts'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Fonts'), + '#default_value' => $default_fonts, + '#description' => $this->t('Select the fonts to use for the text in the image CAPTCHA. Apart from the provided defaults, you can also use your own TrueType fonts (filename extension .ttf) by putting them in %fonts_library_general or %fonts_library_specific.', + [ + '%fonts_library_general' => 'sites/all/libraries/fonts', + '%fonts_library_specific' => $conf_path . '/libraries/fonts', + ] + ), + '#options' => $available_fonts, + '#attributes' => ['class' => ['image_captcha_admin_fonts_selection']], + ]; + + $form['image_captcha_font_size'] = [ + '#type' => 'select', + '#title' => $this->t('Font size'), + '#options' => [ + 9 => '9 pt - ' . $this->t('tiny'), + 12 => '12 pt - ' . $this->t('small'), + 18 => '18 pt', + 24 => '24 pt - ' . $this->t('normal'), + 30 => '30 pt', + 36 => '36 pt - ' . $this->t('large'), + 48 => '48 pt', + 64 => '64 pt - ' . $this->t('extra large'), + ], + '#default_value' => (int) $config->get('image_captcha_font_size'), + '#description' => $this->t('The font size influences the size of the image. Note that larger values make the image generation more CPU intensive.'), + ]; + } + + // Character spacing (available for both the TrueType + // fonts and the builtin font. + $form['image_captcha_font_settings']['image_captcha_character_spacing'] = [ + '#type' => 'select', + '#title' => $this->t('Character spacing'), + '#description' => $this->t('Define the average spacing between characters. Note that larger values make the image generation more CPU intensive.'), + '#default_value' => $config->get('image_captcha_character_spacing'), + '#options' => [ + '0.75' => $this->t('tight'), + '1' => $this->t('normal'), + '1.2' => $this->t('wide'), + '1.5' => $this->t('extra wide'), + ], + ]; + + return $form; + } + + /** + * 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 (file_scan_directory($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 new file mode 100755 index 0000000000000000000000000000000000000000..d4c81db0194b7cb5f21cb6d6273e102dee662343 --- /dev/null +++ b/web/modules/captcha/image_captcha/src/Response/CaptchaImageResponse.php @@ -0,0 +1,448 @@ +<?php + +namespace Drupal\image_captcha\Response; + +use Drupal\Core\Config\Config; +use Drupal\Core\Logger\LoggerChannelInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Response which is returned as the captcha for image_captcha. + * + * @package Drupal\image_captcha\Response + */ +class CaptchaImageResponse extends Response { + + /** + * Image Captcha config storage. + * + * @var Config + */ + protected $config; + + /** + * Watchdog logger channel for captcha. + * + * @var LoggerChannelInterface + */ + protected $logger; + + /** + * Recourse with generated image. + * + * @var resource + */ + protected $image; + + /** + * {@inheritdoc} + */ + public function __construct(Config $config, LoggerChannelInterface $logger, $callback = NULL, $status = 200, $headers = []) { + parent::__construct(NULL, $status, $headers); + + $this->config = $config; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function prepare(Request $request) { + $session_id = $request->get('session_id'); + + $code = db_query("SELECT solution FROM {captcha_sessions} WHERE csid = :csid", + [':csid' => $session_id] + )->fetchField(); + + if ($code !== FALSE) { + $this->image = @$this->generateImage($code); + + if (!$this->image) { + $this->logger->log(WATCHDOG_ERROR, 'Generation of image CAPTCHA failed. Check your image CAPTCHA configuration and especially the used font.', []); + } + } + + return parent::prepare($request); + } + + /** + * {@inheritdoc} + */ + public function sendHeaders() { + if ($this->config->get('image_captcha_file_format') == IMAGE_CAPTCHA_FILE_FORMAT_JPG) { + $this->headers->set('content-type', 'image/jpeg'); + } + else { + $this->headers->set('content-type', 'image/png'); + } + + return parent::sendHeaders(); + } + + /** + * {@inheritdoc} + */ + public function sendContent() { + if (!$this->image) { + return; + } + + // Begin capturing the byte stream. + ob_start(); + + $file_format = $this->config->get('image_captcha_file_format'); + if ($file_format == IMAGE_CAPTCHA_FILE_FORMAT_JPG) { + imagejpeg($this->image); + } + else { + imagepng($this->image); + } + // Clean up the image resource. + imagedestroy($this->image); + } + + /** + * Small helper function for parsing a hexadecimal color to a RGB tuple. + * + * @param string $hex + * String representation of HEX color value. + * + * @return array + * Array representation of RGB color value. + */ + protected function hexToRgb($hex) { + if (strlen($hex) == 4) { + $hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3]; + } + $c = hexdec($hex); + $rgb = []; + for ($i = 16; $i >= 0; $i -= 8) { + $rgb[] = ($c >> $i) & 0xFF; + } + return $rgb; + } + + /** + * Base function for generating a image CAPTCHA. + * + * @param string $code + * String code to be presented on image. + * + * @return resource + * Image to be outputted contained $code string. + */ + protected function generateImage($code) { + $fonts = _image_captcha_get_enabled_fonts(); + + $font_size = $this->config->get('image_captcha_font_size'); + list($width, $height) = _image_captcha_image_size($code); + + $image = imagecreatetruecolor($width, $height); + if (!$image) { + return FALSE; + } + + // Get the background color and paint the background. + $background_rgb = $this->hexToRGB($this->config->get('image_captcha_background_color')); + $background_color = imagecolorallocate($image, $background_rgb[0], $background_rgb[1], $background_rgb[2]); + // Set transparency if needed. + $file_format = $this->config->get('image_captcha_file_format'); + if ($file_format == IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG) { + imagecolortransparent($image, $background_color); + } + imagefilledrectangle($image, 0, 0, $width, $height, $background_color); + + // Do we need to draw in RTL mode? + global $language; + + $result = $this->printString($image, $width, $height, $fonts, $font_size, $code); + if (!$result) { + return FALSE; + } + + $noise_colors = []; + for ($i = 0; $i < 20; $i++) { + $noise_colors[] = imagecolorallocate($image, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255)); + } + + // Add additional noise. + if ($this->config->get('image_captcha_dot_noise')) { + $this->addDots($image, $width, $height, $noise_colors); + } + + if ($this->config->get('image_captcha_line_noise')) { + $this->addLines($image, $width, $height, $noise_colors); + } + + $distortion_amplitude = .25 * $font_size * $this->config->get('image_captcha_distortion_amplitude') / 10.0; + + if ($distortion_amplitude > 1) { + $wavelength_xr = (2 + 3 * lcg_value()) * $font_size; + $wavelength_yr = (2 + 3 * lcg_value()) * $font_size; + $freq_xr = 2 * 3.141592 / $wavelength_xr; + $freq_yr = 2 * 3.141592 / $wavelength_yr; + $wavelength_xt = (2 + 3 * lcg_value()) * $font_size; + $wavelength_yt = (2 + 3 * lcg_value()) * $font_size; + $freq_xt = 2 * 3.141592 / $wavelength_xt; + $freq_yt = 2 * 3.141592 / $wavelength_yt; + + $distorted_image = imagecreatetruecolor($width, $height); + + if ($file_format == IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG) { + imagecolortransparent($distorted_image, $background_color); + } + + if (!$distorted_image) { + return FALSE; + } + + if ($this->config->get('image_captcha_bilinear_interpolation')) { + // Distortion with bilinear interpolation. + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + // Get distorted sample point in source image. + $r = $distortion_amplitude * sin($x * $freq_xr + $y * $freq_yr); + $theta = $x * $freq_xt + $y * $freq_yt; + $sx = $x + $r * cos($theta); + $sy = $y + $r * sin($theta); + $sxf = (int) floor($sx); + $syf = (int) floor($sy); + if ($sxf < 0 || $syf < 0 || $sxf >= $width - 1 || $syf >= $height - 1) { + $color = $background_color; + } + else { + // Bilinear interpolation: sample at four corners. + $color_00 = imagecolorat($image, $sxf, $syf); + $color_00_r = ($color_00 >> 16) & 0xFF; + $color_00_g = ($color_00 >> 8) & 0xFF; + $color_00_b = $color_00 & 0xFF; + $color_10 = imagecolorat($image, $sxf + 1, $syf); + $color_10_r = ($color_10 >> 16) & 0xFF; + $color_10_g = ($color_10 >> 8) & 0xFF; + $color_10_b = $color_10 & 0xFF; + $color_01 = imagecolorat($image, $sxf, $syf + 1); + $color_01_r = ($color_01 >> 16) & 0xFF; + $color_01_g = ($color_01 >> 8) & 0xFF; + $color_01_b = $color_01 & 0xFF; + $color_11 = imagecolorat($image, $sxf + 1, $syf + 1); + $color_11_r = ($color_11 >> 16) & 0xFF; + $color_11_g = ($color_11 >> 8) & 0xFF; + $color_11_b = $color_11 & 0xFF; + // Interpolation factors. + $u = $sx - $sxf; + $v = $sy - $syf; + $r = (int) ((1 - $v) * ((1 - $u) * $color_00_r + $u * $color_10_r) + $v * ((1 - $u) * $color_01_r + $u * $color_11_r)); + $g = (int) ((1 - $v) * ((1 - $u) * $color_00_g + $u * $color_10_g) + $v * ((1 - $u) * $color_01_g + $u * $color_11_g)); + $b = (int) ((1 - $v) * ((1 - $u) * $color_00_b + $u * $color_10_b) + $v * ((1 - $u) * $color_01_b + $u * $color_11_b)); + $color = ($r << 16) + ($g << 8) + $b; + } + + imagesetpixel($distorted_image, $x, $y, $color); + } + } + } + else { + // Distortion with nearest neighbor interpolation. + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + // Get distorted sample point in source image. + $r = $distortion_amplitude * sin($x * $freq_xr + $y * $freq_yr); + $theta = $x * $freq_xt + $y * $freq_yt; + $sx = $x + $r * cos($theta); + $sy = $y + $r * sin($theta); + $sxf = (int) floor($sx); + $syf = (int) floor($sy); + if ($sxf < 0 || $syf < 0 || $sxf >= $width - 1 || $syf >= $height - 1) { + $color = $background_color; + } + else { + $color = imagecolorat($image, $sxf, $syf); + } + imagesetpixel($distorted_image, $x, $y, $color); + } + } + } + imagedestroy($image); + return $distorted_image; + } + else { + return $image; + } + } + + /** + * Add random noise lines to image with given color. + * + * @param resource $image + * Link to image stream resource. + * @param int $width + * Suggested output image width. + * @param int $height + * Suggested input image width. + * @param array $colors + * Font color. + */ + protected function addLines(&$image, $width, $height, array $colors) { + $line_quantity = $width * $height / 200.0 * ((int) $this->config->get('image_captcha_noise_level')) / 10.0; + + for ($i = 0; $i < $line_quantity; $i++) { + imageline($image, mt_rand(0, $width), mt_rand(0, $height), mt_rand(0, $width), mt_rand(0, $height), $colors[array_rand($colors)]); + } + } + + /** + * Add random noise dots to image with given color. + * + * @param resource $image + * Link to image stream resource. + * @param int $width + * Suggested output image width. + * @param int $height + * Suggested input image width. + * @param array $colors + * Font color. + */ + protected function addDots(&$image, $width, $height, array $colors) { + $noise_quantity = $width * $height * ((int) $this->config->get('image_captcha_noise_level')) / 10.0; + + for ($i = 0; $i < $noise_quantity; $i++) { + imagesetpixel($image, mt_rand(0, $width), mt_rand(0, $height), $colors[array_rand($colors)]); + } + } + + /** + * Helper function for drawing text on the image. + * + * @param resource $image + * Link to image stream resource. + * @param int $width + * Suggested output image width. + * @param int $height + * Suggested input image width. + * @param array $fonts + * Array of fonts names and paths. + * @param int $font_size + * Suggested font size. + * @param string $text + * Text to be written on the image. + * @param bool $rtl + * RTL. + * + * @return bool + * TRUE if image generation was successful, FALSE otherwise. + */ + protected function printString(&$image, $width, $height, array $fonts, $font_size, $text, $rtl = FALSE) { + $characters = _image_captcha_utf8_split($text); + $character_quantity = count($characters); + + $foreground_rgb = $this->hexToRgb($this->config->get('image_captcha_foreground_color')); + $background_rgb = $this->hexToRgb($this->config->get('image_captcha_background_color')); + $foreground_color = imagecolorallocate($image, $foreground_rgb[0], $foreground_rgb[1], $foreground_rgb[2]); + // Precalculate the value ranges for color randomness. + $foreground_randomness = $this->config->get('image_captcha_foreground_color_randomness'); + $foreground_color_range = []; + + if ($foreground_randomness) { + for ($i = 0; $i < 3; $i++) { + $foreground_color_range[$i] = [ + max(0, $foreground_rgb[$i] - $foreground_randomness), + min(255, $foreground_rgb[$i] + $foreground_randomness), + ]; + } + } + + // Set default text color. + $color = $foreground_color; + + // The image is separated in different character cages, one for + // each character that will be somewhere inside that cage. + $ccage_width = $width / $character_quantity; + $ccage_height = $height; + + foreach ($characters as $c => $character) { + // Initial position of character: in the center of its cage. + $center_x = ($c + 0.5) * $ccage_width; + if ($rtl) { + $center_x = $width - $center_x; + } + $center_y = 0.5 * $height; + + // Pick a random font from the list. + $font = $fonts[array_rand($fonts)]; + + // Get character dimensions for TrueType fonts. + if ($font != 'BUILTIN') { + putenv('GDFONTPATH=' . realpath('.')); + $bbox = imagettfbbox($font_size, 0, drupal_realpath($font), $character); + // In very rare cases with some versions of the GD library, the x-value + // of the left side of the bounding box as returned by the first call of + // imagettfbbox is corrupt (value -2147483648 = 0x80000000). + // The weird thing is that calling the function a second time + // can be used as workaround. + // This issue is discussed at http://drupal.org/node/349218. + if ($bbox[2] < 0) { + $bbox = imagettfbbox($font_size, 0, drupal_realpath($font), $character); + } + } + else { + $character_width = imagefontwidth(5); + $character_height = imagefontheight(5); + $bbox = [ + 0, + $character_height, + $character_width, + $character_height, + $character_width, + 0, + 0, + 0, + ]; + } + + // Random (but small) rotation of the character. + // TODO: add a setting for this? + $angle = mt_rand(-10, 10); + + // Determine print position: at what coordinate should the character be + // printed so that the bounding box would be nicely centered in the cage? + $bb_center_x = .5 * ($bbox[0] + $bbox[2]); + $bb_center_y = .5 * ($bbox[1] + $bbox[7]); + $angle_cos = cos($angle * 3.1415 / 180); + $angle_sin = sin($angle * 3.1415 / 180); + $pos_x = $center_x - ($angle_cos * $bb_center_x + $angle_sin * $bb_center_y); + $pos_y = $center_y - (-$angle_sin * $bb_center_x + $angle_cos * $bb_center_y); + + // Calculate available room to jitter: how much + // can the character be moved. So that it stays inside its cage? + $bb_width = $bbox[2] - $bbox[0]; + $bb_height = $bbox[1] - $bbox[7]; + $dev_x = .5 * max(0, $ccage_width - abs($angle_cos) * $bb_width - abs($angle_sin) * $bb_height); + $dev_y = .5 * max(0, $ccage_height - abs($angle_cos) * $bb_height - abs($angle_sin) * $bb_width); + + // Add jitter to position. + $pos_x = $pos_x + mt_rand(-$dev_x, $dev_x); + $pos_y = $pos_y + mt_rand(-$dev_y, $dev_y); + + // Calculate text color in case of randomness. + if ($foreground_randomness) { + $color = imagecolorallocate($image, + mt_rand($foreground_color_range[0][0], $foreground_color_range[0][1]), + mt_rand($foreground_color_range[1][0], $foreground_color_range[1][1]), + mt_rand($foreground_color_range[2][0], $foreground_color_range[2][1]) + ); + } + + // Draw character. + if ($font == 'BUILTIN') { + imagestring($image, 5, $pos_x, $pos_y, $character, $color); + } + else { + imagettftext($image, $font_size, $angle, $pos_x, $pos_y, $color, drupal_realpath($font), $character); + } + } + + return TRUE; + } + +} diff --git a/web/modules/captcha/src/CaptchaPointInterface.php b/web/modules/captcha/src/CaptchaPointInterface.php new file mode 100755 index 0000000000000000000000000000000000000000..58456166f1df220e3e4568a1865f2cafabe57b54 --- /dev/null +++ b/web/modules/captcha/src/CaptchaPointInterface.php @@ -0,0 +1,58 @@ +<?php + +namespace Drupal\captcha; + +use Drupal\Core\Config\Entity\ConfigEntityInterface; + +/** + * Interface CaptchaPointInterface. + * + * @package Drupal\captcha + * + * Provides an interface defining a CaptchaPoint entity. + */ +interface CaptchaPointInterface extends ConfigEntityInterface { + + /** + * Getter for form machine ID property. + */ + public function getFormId(); + + /** + * Setter for label property. + * + * @param string $form_id + * Form machine ID string. + */ + public function setFormId($form_id); + + /** + * Getter for label property. + * + * @return string + * Label string. + */ + public function getLabel(); + + /** + * Setter for label property. + * + * @param string $label + * Label string. + */ + public function setLabel($label); + + /** + * Getter for captcha type property. + */ + public function getCaptchaType(); + + /** + * Setter for captcha type property. + * + * @param string|null $captcha_type + * Captcha type. + */ + public function setCaptchaType($captcha_type); + +} diff --git a/web/modules/captcha/src/Controller/CaptchaPointListBuilder.php b/web/modules/captcha/src/Controller/CaptchaPointListBuilder.php new file mode 100755 index 0000000000000000000000000000000000000000..1ee13ee0ca921d1213e2e5ddb42bd1f2ec1c175e --- /dev/null +++ b/web/modules/captcha/src/Controller/CaptchaPointListBuilder.php @@ -0,0 +1,34 @@ +<?php + +namespace Drupal\captcha\Controller; + +use Drupal\Core\Config\Entity\ConfigEntityListBuilder; +use Drupal\Core\Entity\EntityInterface; + +/** + * Builds the list of capture points for the captcha point form. + */ +class CaptchaPointListBuilder extends ConfigEntityListBuilder { + + /** + * {@inheritdoc} + */ + public function buildHeader() { + module_load_include('inc', 'captcha'); + $header['form_id'] = $this->t('Captcha Point form ID'); + $header['captcha_type'] = $this->t('Captcha Point challenge type'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['form_id'] = $entity->id(); + $row['captcha_type'] = $entity->getCaptchaType(); + + return $row + parent::buildRow($entity); + } + +} diff --git a/web/modules/captcha/src/Element/Captcha.php b/web/modules/captcha/src/Element/Captcha.php new file mode 100644 index 0000000000000000000000000000000000000000..2c76d8f200b68e8c456989db711d8f535b38b794 --- /dev/null +++ b/web/modules/captcha/src/Element/Captcha.php @@ -0,0 +1,191 @@ +<?php + +namespace Drupal\captcha\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\FormElement; + +/** + * Defines the CAPTCHA form element with default properties. + * + * @FormElement("captcha") + */ +class Captcha extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $captcha_element = [ + '#input' => TRUE, + '#process' => [[static::class, 'processCaptchaElement']], + // The type of challenge: e.g. 'default', 'captcha/Math', etc. + '#captcha_type' => 'default', + '#default_value' => '', + // CAPTCHA in admin mode: presolve the CAPTCHA and always show + // it (despite previous successful responses). + '#captcha_admin_mode' => FALSE, + // The default CAPTCHA validation function. + // TODO: should this be a single string or an array of strings? + '#captcha_validate' => 'captcha_validate_strict_equality', + ]; + // Override the default CAPTCHA validation function for case + // insensitive validation. + // TODO: shouldn't this be done somewhere else, e.g. in form_alter? + if (CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE == \Drupal::config('captcha.settings') + ->get('default_validation') + ) { + $captcha_element['#captcha_validate'] = 'captcha_validate_case_insensitive_equality'; + } + return $captcha_element; + } + + /** + * Process callback for CAPTCHA form element. + */ + public static function processCaptchaElement(&$element, FormStateInterface $form_state, &$complete_form) { + // Add captcha.inc file. + module_load_include('inc', 'captcha'); + + // Add JavaScript for general CAPTCHA functionality. + $element['#attached']['library'][] = 'captcha/base'; + + if ($form_state->getTriggeringElement() && is_array($form_state->getTriggeringElement()['#limit_validation_errors'])) { + // This is a partial (ajax) submission with limited validation. Do not + // change anything about the captcha element, assume that it will not + // update the captcha element, do not generate anything, which keeps the + // current token intact for the real submission. + return $element; + } + + // Get the form ID of the form we are currently processing (which is not + // necessary the same form that is submitted (if any). + $this_form_id = $complete_form['form_id']['#value']; + + // Get the CAPTCHA session ID. + // If there is a submitted form: try to retrieve and reuse the + // CAPTCHA session ID from the posted data. + list($posted_form_id, $posted_captcha_sid) = _captcha_get_posted_captcha_info($element, $form_state, $this_form_id); + if ($this_form_id == $posted_form_id && isset($posted_captcha_sid)) { + $captcha_sid = $posted_captcha_sid; + } + else { + // Generate a new CAPTCHA session if we could + // not reuse one from a posted form. + $captcha_sid = _captcha_generate_captcha_session($this_form_id, CAPTCHA_STATUS_UNSOLVED); + $captcha_token = md5(mt_rand()); + db_update('captcha_sessions') + ->fields(['token' => $captcha_token]) + ->condition('csid', $captcha_sid) + ->execute(); + } + + // Store CAPTCHA session ID as hidden field. + // Strictly speaking, it is not necessary to send the CAPTCHA session id + // with the form, as the one time CAPTCHA token (see lower) is enough. + // However, we still send it along, because it can help debugging + // problems on live sites with only access to the markup. + $element['captcha_sid'] = [ + '#type' => 'hidden', + '#value' => $captcha_sid, + ]; + + // Additional one time CAPTCHA token: store in database and send with form. + // $captcha_token = hash('sha256', mt_rand()); + // db_update('captcha_sessions') + // ->fields(['token' => $captcha_token]) + // ->condition('csid', $captcha_sid) + // ->execute(); + $captcha_token = db_query("SELECT token FROM {captcha_sessions} WHERE csid = :csid", [':csid' => $captcha_sid])->fetchField(); + $element['captcha_token'] = [ + '#type' => 'hidden', + '#value' => $captcha_token, + ]; + + // Get implementing module and challenge for CAPTCHA. + list($captcha_type_module, $captcha_type_challenge) = _captcha_parse_captcha_type($element['#captcha_type']); + + // Store CAPTCHA information for further processing in + // - $form_state->get('captcha_info'), which survives + // a form rebuild (e.g. node preview), + // useful in _captcha_get_posted_captcha_info(). + // - $element['#captcha_info'], for post processing functions that do not + // receive a $form_state argument (e.g. the pre_render callback). + $form_state->set('captcha_info', [ + 'this_form_id' => $this_form_id, + 'posted_form_id' => $posted_form_id, + 'captcha_sid' => $captcha_sid, + 'module' => $captcha_type_module, + 'captcha_type' => $captcha_type_challenge, + ]); + $element['#captcha_info'] = [ + 'form_id' => $this_form_id, + 'captcha_sid' => $captcha_sid, + ]; + + if (_captcha_required_for_user($captcha_sid, $this_form_id) || $element['#captcha_admin_mode']) { + // Generate a CAPTCHA and its solution + // (note that the CAPTCHA session ID is given as third argument). + $captcha = \Drupal::moduleHandler() + ->invoke($captcha_type_module, 'captcha', [ + 'generate', + $captcha_type_challenge, + $captcha_sid, + ]); + + // @todo Isn't this moment a bit late to figure out + // that we don't need CAPTCHA? + if (!isset($captcha)) { + return $element; + } + + if (!isset($captcha['form']) || !isset($captcha['solution'])) { + // The selected module did not return what we expected: + // log about it and quit. + \Drupal::logger('CAPTCHA')->error( + 'CAPTCHA problem: unexpected result from hook_captcha() of module %module when trying to retrieve challenge type %type for form %form_id.', + [ + '%type' => $captcha_type_challenge, + '%module' => $captcha_type_module, + '%form_id' => $this_form_id, + ] + ); + + return $element; + } + // Add form elements from challenge as children to CAPTCHA form element. + $element['captcha_widgets'] = $captcha['form']; + + // Add a validation callback for the CAPTCHA form element + // (when not in admin mode). + if (!$element['#captcha_admin_mode']) { + $element['#element_validate'] = ['captcha_validate']; + } + + // Set a custom CAPTCHA validate function if requested. + if (isset($captcha['captcha_validate'])) { + $element['#captcha_validate'] = $captcha['captcha_validate']; + } + + // Set the theme function. + $element['#theme'] = 'captcha'; + + // Add pre_render callback for additional CAPTCHA processing. + if (!isset($element['#pre_render'])) { + $element['#pre_render'] = []; + } + $element['#pre_render'][] = 'captcha_pre_render_process'; + + // Store the solution in the #captcha_info array. + $element['#captcha_info']['solution'] = $captcha['solution']; + + // Make sure we can use a top level form value + // $form_state->getValue('captcha_response'), + // even if the form has #tree=true. + $element['#tree'] = FALSE; + } + + return $element; + } + +} diff --git a/web/modules/captcha/src/Entity/CaptchaPoint.php b/web/modules/captcha/src/Entity/CaptchaPoint.php new file mode 100755 index 0000000000000000000000000000000000000000..4b6c458ee7aaeb3bea69f4ed4e69a91e8ec51b20 --- /dev/null +++ b/web/modules/captcha/src/Entity/CaptchaPoint.php @@ -0,0 +1,114 @@ +<?php + +namespace Drupal\captcha\Entity; + +use Drupal\captcha\CaptchaPointInterface; +use Drupal\Core\Config\Entity\ConfigEntityBase; + +/** + * Defines the CaptchaPoint entity. + * + * The 'rendered' tag for the List cache is necessary since captchas have to be + * rerendered once they are modified. Invalidating the render cache ensures + * we always display the correct captcha for every form. + * + * @ConfigEntityType( + * id = "captcha_point", + * label = @Translation("Captcha Point"), + * handlers = { + * "list_builder" = "Drupal\captcha\Controller\CaptchaPointListBuilder", + * "form" = { + * "add" = "Drupal\captcha\Form\CaptchaPointForm", + * "edit" = "Drupal\captcha\Form\CaptchaPointForm", + * "disable" = "Drupal\captcha\Form\CaptchaPointDisableForm", + * "enable" = "Drupal\captcha\Form\CaptchaPointEnableForm", + * "delete" = "Drupal\captcha\Form\CaptchaPointDeleteForm" + * } + * }, + * config_prefix = "captcha_point", + * admin_permission = "administer CAPTCHA settings", + * list_cache_tags = { + * "rendered" + * }, + * entity_keys = { + * "id" = "formId", + * "label" = "label", + * "status" = "status", + * }, + * config_export = { + * "formId", + * "captchaType", + * "label", + * "uuid", + * }, + * links = { + * "edit-form" = "/admin/config/people/captcha/captcha-points/{captcha_point}", + * "disable" = "/admin/config/people/captcha/captcha-points/{captcha_point}/disable", + * "enable" = "/admin/config/people/captcha/captcha-points/{captcha_point}/enable", + * "delete-form" = "/admin/config/people/captcha/captcha-points/{captcha_point}/delete", + * } + * ) + */ +class CaptchaPoint extends ConfigEntityBase implements CaptchaPointInterface { + public $captchaType; + + protected $label; + + public $formId; + + /** + * {@inheritdoc} + */ + public function id() { + return $this->formId; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return $this->formId; + } + + /** + * {@inheritdoc} + */ + public function setFormId($form_id) { + $this->formId = $form_id; + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + return $this->label; + } + + /** + * {@inheritdoc} + */ + public function setLabel($label) { + $this->label = $label; + } + + /** + * {@inheritdoc} + */ + public function getCaptchaType() { + if (isset($this->captchaType)) { + return $this->captchaType; + } + else { + // @Todo inject config via DI. + return \Drupal::config('captcha.settings')->get('default_challenge'); + } + } + + /** + * {@inheritdoc} + */ + public function setCaptchaType($captcha_type) { + $this->captchaType = $captcha_type != 'default' ? $captcha_type : NULL; + } + +} diff --git a/web/modules/captcha/src/EventSubscriber/CaptchaCachedSettingsSubscriber.php b/web/modules/captcha/src/EventSubscriber/CaptchaCachedSettingsSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..c24c8d3fc2f9bb5e2b42c27b4922180dace8072c --- /dev/null +++ b/web/modules/captcha/src/EventSubscriber/CaptchaCachedSettingsSubscriber.php @@ -0,0 +1,36 @@ +<?php + +namespace Drupal\captcha\EventSubscriber; + +use Drupal\Core\Config\ConfigCrudEvent; +use Drupal\Core\Config\ConfigEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * A subscriber clearing the cached definitions when saving captcha settings. + */ +class CaptchaCachedSettingsSubscriber implements EventSubscriberInterface { + + /** + * Clearing the cached definitions whenever the settings are modified. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + * The Event to process. + */ + public function onSave(ConfigCrudEvent $event) { + // Changing the Captcha settings means that any page might result in other + // settings for captcha so the cached definitions need to be cleared. + if ($event->getConfig()->getName() === 'captcha.settings') { + \Drupal::service('element_info')->clearCachedDefinitions(); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[ConfigEvents::SAVE][] = ['onSave']; + return $events; + } + +} diff --git a/web/modules/captcha/src/Form/CaptchaExamplesForm.php b/web/modules/captcha/src/Form/CaptchaExamplesForm.php new file mode 100755 index 0000000000000000000000000000000000000000..37dd94fe9fd713765b2c73d4a4f5be7b748e20f7 --- /dev/null +++ b/web/modules/captcha/src/Form/CaptchaExamplesForm.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\captcha\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Link; +use Drupal\Core\Url; + +/** + * Displays the captcha settings form. + */ +class CaptchaExamplesForm extends FormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'captcha_examples'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $module = NULL, $challenge = NULL) { + module_load_include('inc', 'captcha', 'captcha.admin'); + + $form = []; + if ($module && $challenge) { + // Generate 10 example challenges. + for ($i = 0; $i < 10; $i++) { + $form["challenge_{$i}"] = _captcha_generate_example_challenge($module, $challenge); + } + } + else { + // Generate a list with examples of the available CAPTCHA types. + $form['info'] = [ + '#markup' => $this->t('This page gives an overview of all available challenge types, generated with their current settings.'), + ]; + + $modules_list = \Drupal::moduleHandler()->getImplementations('captcha'); + foreach ($modules_list as $mkey => $module) { + $challenges = call_user_func_array($module . '_captcha', ['list']); + + if ($challenges) { + foreach ($challenges as $ckey => $challenge) { + $form["captcha_{$mkey}_{$ckey}"] = [ + '#type' => 'details', + '#title' => $this->t('Challenge %challenge by module %module', [ + '%challenge' => $challenge, + '%module' => $module, + ]), + 'challenge' => _captcha_generate_example_challenge($module, $challenge), + 'more_examples' => [ + '#markup' => Link::fromTextAndUrl($this->t('10 more examples of this challenge.'), Url::fromRoute('captcha_examples', [ + 'module' => $module, + 'challenge' => $challenge, + ]))->toString(), + ], + ]; + } + } + } + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + } + +} diff --git a/web/modules/captcha/src/Form/CaptchaPointDeleteForm.php b/web/modules/captcha/src/Form/CaptchaPointDeleteForm.php new file mode 100755 index 0000000000000000000000000000000000000000..fc43bbef4836e1618ebef099cafd03e07debecaf --- /dev/null +++ b/web/modules/captcha/src/Form/CaptchaPointDeleteForm.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\captcha\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Builds the form to delete a Captcha Point. + */ +class CaptchaPointDeleteForm extends EntityConfirmFormBase { + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('captcha_point.list'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + drupal_set_message($this->t('Captcha point %label has been deleted.', ['%label' => $this->entity->label()])); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/captcha/src/Form/CaptchaPointDisableForm.php b/web/modules/captcha/src/Form/CaptchaPointDisableForm.php new file mode 100644 index 0000000000000000000000000000000000000000..89b70618b10ee24570c488401777b28a1e94c7af --- /dev/null +++ b/web/modules/captcha/src/Form/CaptchaPointDisableForm.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\captcha\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Builds the form to delete a Captcha Point. + */ +class CaptchaPointDisableForm extends EntityConfirmFormBase { + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to disable the Captcha?'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This will disable the captcha.'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('captcha_point.list'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Disable'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->disable(); + $this->entity->save(); + drupal_set_message($this->t('Captcha point %label has been disabled.', ['%label' => $this->entity->label()])); + $form_state->setRedirect('captcha_point.list'); + } + +} diff --git a/web/modules/captcha/src/Form/CaptchaPointEnableForm.php b/web/modules/captcha/src/Form/CaptchaPointEnableForm.php new file mode 100644 index 0000000000000000000000000000000000000000..e7805b6c60a610eec0732ec198fcf71e2f3558da --- /dev/null +++ b/web/modules/captcha/src/Form/CaptchaPointEnableForm.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\captcha\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Builds the form to delete a Captcha Point. + */ +class CaptchaPointEnableForm extends EntityConfirmFormBase { + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to enable the Captcha?'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This will enable the captcha.'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('captcha_point.list'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Enable'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->enable(); + $this->entity->save(); + drupal_set_message($this->t('Captcha point %label has been enabled.', ['%label' => $this->entity->label()])); + $form_state->setRedirect('captcha_point.list'); + } + +} diff --git a/web/modules/captcha/src/Form/CaptchaPointForm.php b/web/modules/captcha/src/Form/CaptchaPointForm.php new file mode 100755 index 0000000000000000000000000000000000000000..f5d2a7a654d9cb826ceef50ee08e6f8360a0c9a6 --- /dev/null +++ b/web/modules/captcha/src/Form/CaptchaPointForm.php @@ -0,0 +1,82 @@ +<?php + +namespace Drupal\captcha\Form; + +use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Form\FormStateInterface; + +/** + * Entity Form to edit CAPTCHA points. + */ +class CaptchaPointForm extends EntityForm { + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + module_load_include('inc', 'captcha', 'captcha.admin'); + + /* @var CaptchaPointInterface $captchaPoint */ + $captcha_point = $this->entity; + + // Support to set a default form_id through a query argument. + $request = \Drupal::request(); + if ($captcha_point->isNew() && !$captcha_point->id() && $request->query->has('form_id')) { + $captcha_point->set('formId', $request->query->get('form_id')); + $captcha_point->set('label', $request->query->get('form_id')); + } + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Form ID'), + '#default_value' => $captcha_point->label(), + '#required' => TRUE, + ]; + + $form['formId'] = [ + '#type' => 'machine_name', + '#default_value' => $captcha_point->id(), + '#machine_name' => [ + 'exists' => 'captcha_point_load', + ], + '#disable' => !$captcha_point->isNew(), + '#required' => TRUE, + ]; + + // Select widget for CAPTCHA type. + $form['captchaType'] = [ + '#type' => 'select', + '#title' => $this->t('Challenge type'), + '#description' => $this->t('The CAPTCHA type to use for this form.'), + '#default_value' => ($captcha_point->getCaptchaType() ?: $this->config('captcha.settings') + ->get('default_challenge')), + '#options' => _captcha_available_challenge_types(), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /* @var CaptchaPoint $captcha_point */ + $captcha_point = $this->entity; + $status = $captcha_point->save(); + + if ($status == SAVED_NEW) { + drupal_set_message($this->t('Captcha Point for %form_id form was created.', [ + '%form_id' => $captcha_point->getFormId(), + ])); + } + else { + drupal_set_message($this->t('Captcha Point for %form_id form was updated.', [ + '%form_id' => $captcha_point->getFormId(), + ])); + } + $form_state->setRedirect('captcha_point.list'); + } + +} diff --git a/web/modules/captcha/src/Form/CaptchaSettingsForm.php b/web/modules/captcha/src/Form/CaptchaSettingsForm.php new file mode 100755 index 0000000000000000000000000000000000000000..a9ecbd46eeabb5211f6323b6404920c58cd8ba4b --- /dev/null +++ b/web/modules/captcha/src/Form/CaptchaSettingsForm.php @@ -0,0 +1,235 @@ +<?php + +namespace Drupal\captcha\Form; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Displays the captcha settings form. + */ +class CaptchaSettingsForm extends ConfigFormBase { + + /** + * The cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** + * Constructs a \Drupal\captcha\Form\CaptchaSettingsForm object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + */ + public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $cache_backend) { + parent::__construct($config_factory); + $this->cacheBackend = $cache_backend; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('cache.default') + ); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['captcha.settings']; + } + + /** + * Implements \Drupal\Core\Form\FormInterface::getFormID(). + */ + public function getFormId() { + return 'captcha_settings'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('captcha.settings'); + module_load_include('inc', 'captcha'); + module_load_include('inc', 'captcha', 'captcha.admin'); + + // Configuration of which forms to protect, with what challenge. + $form['form_protection'] = [ + '#type' => 'details', + '#title' => $this->t('Form protection'), + '#description' => $this->t("Select the challenge type you want for each of the listed forms (identified by their so called <em>form_id</em>'s). You can easily add arbitrary forms with the textfield at the bottom of the table or with the help of the option <em>Add CAPTCHA administration links to forms</em> below."), + '#open' => TRUE, + ]; + + $form['form_protection']['default_challenge'] = [ + '#type' => 'select', + '#title' => $this->t('Default challenge type'), + '#description' => $this->t('Select the default challenge type for CAPTCHAs. This can be overridden for each form if desired.'), + '#options' => _captcha_available_challenge_types(FALSE), + '#default_value' => $config->get('default_challenge'), + ]; + + // Field for the CAPTCHA administration mode. + $form['form_protection']['administration_mode'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Add CAPTCHA administration links to forms'), + '#default_value' => $config->get('administration_mode'), + '#description' => $this->t('This option makes it easy to manage CAPTCHA settings on forms. When enabled, users with the <em>administer CAPTCHA settings</em> permission will see a fieldset with CAPTCHA administration links on all forms, except on administrative pages.'), + ]; + // Field for the CAPTCHAs on admin pages. + $form['form_protection']['allow_on_admin_pages'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow CAPTCHAs and CAPTCHA administration links on administrative pages'), + '#default_value' => $config->get('allow_on_admin_pages'), + '#description' => $this->t("This option makes it possible to add CAPTCHAs to forms on administrative pages. CAPTCHAs are disabled by default on administrative pages (which shouldn't be accessible to untrusted users normally) to avoid the related overhead. In some situations, e.g. in the case of demo sites, it can be useful to allow CAPTCHAs on administrative pages."), + ]; + + // Button for clearing the CAPTCHA placement cache. + // Based on Drupal core's "Clear all caches" (performance settings page). + $form['form_protection']['placement_caching'] = [ + '#type' => 'item', + '#title' => $this->t('CAPTCHA placement caching'), + '#description' => $this->t('For efficiency, the positions of the CAPTCHA elements in each of the configured forms are cached. Most of the time, the structure of a form does not change and it would be a waste to recalculate the positions every time. Occasionally however, the form structure can change (e.g. during site building) and clearing the CAPTCHA placement cache can be required to fix the CAPTCHA placement.'), + ]; + $form['form_protection']['placement_caching']['placement_cache_clear'] = [ + '#type' => 'submit', + '#value' => $this->t('Clear the CAPTCHA placement cache'), + '#submit' => ['::clearCaptchaPlacementCacheSubmit'], + ]; + + // Configuration option for adding a CAPTCHA description. + $form['add_captcha_description'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Add a description to the CAPTCHA'), + '#description' => $this->t('Add a configurable description to explain the purpose of the CAPTCHA to the visitor.'), + '#default_value' => $config->get('add_captcha_description'), + ]; + $form['description'] = [ + '#type' => 'textfield', + '#title' => $this->t('Challenge description'), + '#description' => $this->t('Configurable description of the CAPTCHA. An empty entry will reset the description to default.'), + '#default_value' => _captcha_get_description(), + '#maxlength' => 256, + '#attributes' => ['id' => 'edit-captcha-description-wrapper'], + '#states' => [ + 'visible' => [ + ':input[name="add_captcha_description"]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + // Option for case sensitive/insensitive validation of the responses. + $form['default_validation'] = [ + '#type' => 'radios', + '#title' => $this->t('Default CAPTCHA validation'), + '#description' => $this->t('Define how the response should be processed by default. Note that the modules that provide the actual challenges can override or ignore this.'), + '#options' => [ + CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE => $this->t('Case sensitive validation: the response has to exactly match the solution.'), + CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE => $this->t('Case insensitive validation: lowercase/uppercase errors are ignored.'), + ], + '#default_value' => $config->get('default_validation'), + ]; + + // Field for CAPTCHA persistence. + // TODO for D7: Rethink/simplify the explanation and UI strings. + $form['persistence'] = [ + '#type' => 'radios', + '#title' => $this->t('Persistence'), + '#default_value' => $config->get('persistence'), + '#options' => [ + CAPTCHA_PERSISTENCE_SHOW_ALWAYS => $this->t('Always add a challenge.'), + CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE => $this->t('Omit challenges in a multi-step/preview workflow once the user successfully responds to a challenge.'), + CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE => $this->t('Omit challenges on a form type once the user successfully responds to a challenge on a form of that type.'), + CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL => $this->t('Omit challenges on all forms once the user successfully responds to any challenge on the site.'), + ], + '#description' => $this->t('Define if challenges should be omitted during the rest of a session once the user successfully responds to a challenge.'), + ]; + + // Enable wrong response counter. + $form['enable_stats'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable statistics'), + '#description' => $this->t('Keep CAPTCHA related counters in the <a href=":statusreport">status report</a>. Note that this comes with a performance penalty as updating the counters results in clearing the variable cache.', [ + ':statusreport' => Url::fromRoute('system.status')->toString(), + ]), + '#default_value' => $config->get('enable_stats'), + ]; + + // Option for logging wrong responses. + $form['log_wrong_responses'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Log wrong responses'), + '#description' => $this->t('Report information about wrong responses to the log.'), + '#default_value' => $config->get('log_wrong_responses'), + ]; + + // Replace the description with a link if dblog.module is enabled. + if (\Drupal::moduleHandler()->moduleExists('dblog')) { + $form['log_wrong_responses']['#description'] = $this->t('Report information about wrong responses to the <a href=":dblog">log</a>.', [ + ':dblog' => Url::fromRoute('dblog.overview')->toString(), + ]); + } + + // Submit button. + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save configuration'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('captcha.settings'); + $config->set('administration_mode', $form_state->getValue('administration_mode')); + $config->set('allow_on_admin_pages', $form_state->getValue('allow_on_admin_pages')); + $config->set('default_challenge', $form_state->getValue('default_challenge')); + + // CAPTCHA description stuff. + $config->set('add_captcha_description', $form_state->getValue('add_captcha_description')); + // Save (or reset) the CAPTCHA descriptions. + $config->set('description', $form_state->getValue('description')); + + $config->set('default_validation', $form_state->getValue('default_validation')); + $config->set('persistence', $form_state->getValue('persistence')); + $config->set('enable_stats', $form_state->getValue('enable_stats')); + $config->set('log_wrong_responses', $form_state->getValue('log_wrong_responses')); + $config->save(); + drupal_set_message($this->t('The CAPTCHA settings have been saved.'), 'status'); + + parent::submitForm($form, $form_state); + } + + /** + * Submit callback; clear CAPTCHA placement cache. + * + * @param array $form + * Form structured array. + * @param FormStateInterface $form_state + * Form state structured array. + */ + public function clearCaptchaPlacementCacheSubmit(array $form, FormStateInterface $form_state) { + $this->cacheBackend->delete('captcha_placement_map_cache'); + drupal_set_message($this->t('Cleared the CAPTCHA placement cache.')); + } + +} diff --git a/web/modules/captcha/src/Tests/CaptchaAdminTestCase.php b/web/modules/captcha/src/Tests/CaptchaAdminTestCase.php new file mode 100755 index 0000000000000000000000000000000000000000..3e870634dbc97c0b04fb5c8d0f9911e498b71abe --- /dev/null +++ b/web/modules/captcha/src/Tests/CaptchaAdminTestCase.php @@ -0,0 +1,384 @@ +<?php + +namespace Drupal\captcha\Tests; + +use Drupal\captcha\Entity\CaptchaPoint; +use Drupal\Core\Url; + +/** + * Tests CAPTCHA admin settings. + * + * @group captcha + */ +class CaptchaAdminTestCase extends CaptchaBaseWebTestCase { + + /** + * Test access to the admin pages. + */ + public function testAdminAccess() { + $this->drupalLogin($this->normalUser); + $this->drupalGet(self::CAPTCHA_ADMIN_PATH); + // @TODO do we need this ? + // file_put_contents('tmp.simpletest.html', $this->drupalGetContent()); + $this->assertText(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'); + } + + /** + * Test the CAPTCHA point setting getter/setter. + */ + public function testCaptchaPointSettingGetterAndSetter() { + $comment_form_id = self::COMMENT_FORM_ID; + captcha_set_form_id_setting($comment_form_id, 'test'); + /* @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'); + $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'); + + // Set to 'default'. + captcha_set_form_id_setting($comment_form_id, 'default'); + $this->config('captcha.settings') + ->set('default_challenge', 'foo/bar') + ->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'); + $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'); + + // 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'); + $result = captcha_get_form_id_setting($comment_form_id, TRUE); + $this->assertEqual($result, 'baz/boo', 'Setting and symbolic getting CAPTCHA point: "baz/boo"', 'CAPTCHA'); + + // 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'); + $result = captcha_get_form_id_setting($comment_form_id, TRUE); + $this->assertNotNull($result, 'CAPTCHA exists', 'CAPTCHA'); + + // Set with object. + $captcha_type = 'baba/fofo'; + captcha_set_form_id_setting($comment_form_id, $captcha_type); + + $result = captcha_get_form_id_setting($comment_form_id); + $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'); + $result = captcha_get_form_id_setting($comment_form_id, TRUE); + $this->assertEqual($result, 'baba/fofo', 'Setting and symbolic getting CAPTCHA point: "baba/fofo"', 'CAPTCHA'); + } + + /** + * Helper function for checking CAPTCHA setting of a form. + * + * @param string $form_id + * The form_id of the form to investigate. + * @param string $challenge_type + * What the challenge type should be: + * NULL, 'default' or something like 'captcha/Math'. + */ + 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.', + [ + '@expected' => var_export($challenge_type, TRUE), + '@received' => var_export($result, TRUE), + ]), + 'CAPTCHA'); + } + + /** + * Testing of the CAPTCHA administration links. + */ + public function testCaptchaAdminLinks() { + $this->drupalLogin($this->adminUser); + + // Enable CAPTCHA administration links. + $edit = [ + 'administration_mode' => TRUE, + ]; + + $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH, $edit, t('Save configuration')); + + // Create a node with comments enabled. + $node = $this->drupalCreateNode(); + + // Go to node page. + $this->drupalGet('node/' . $node->id()); + + // Click the add new comment link. + $this->clickLink(t('Add new comment')); + $add_comment_url = $this->getUrl(); + + // Remove fragment part from comment URL to avoid + // problems with later asserts. + $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.')); + + // Enable Math CAPTCHA. + $edit = ['captchaType' => 'captcha/Math']; + $this->drupalPostForm($this->getUrl(), $edit, t('Save')); + // Check if returned to original comment form. + $this->assertUrl($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']]), + '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')); + + // Enable Math CAPTCHA. + $edit = ['captchaType' => 'default']; + $this->drupalPostForm($this->getUrl(), $edit, t('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'); + + // 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']]), + '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')); + + // 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'); + + // 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]), + 'Disable challenge through the CAPTCHA admin links', 'CAPTCHA'); + } + + /** + * Test untrusted user posting. + */ + public function testUntrustedUserPosting() { + // Set CAPTCHA on comment form. + captcha_set_form_id_setting(self::COMMENT_FORM_ID, 'captcha/Math'); + + // Create a node with comments enabled. + $node = $this->drupalCreateNode(); + + // Log in as normal (untrusted) user. + $this->drupalLogin($this->normalUser); + + // Go to node page and click the "add comment" link. + $this->drupalGet('node/' . $node->id()); + $this->clickLink(t('Add new comment')); + $add_comment_url = $this->getUrl(); + + // Check if CAPTCHA is visible on form. + $this->assertCaptchaPresence(TRUE); + // 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, + 'wrong CAPTCHA should block form submission.', 'CAPTCHA'); + } + + /** + * Test XSS vulnerability on CAPTCHA description. + */ + public function testXssOnCaptchaDescription() { + // Set CAPTCHA on user register form. + captcha_set_form_id_setting('user_register', 'captcha/Math'); + + // Put JavaScript snippet in CAPTCHA description. + $this->drupalLogin($this->adminUser); + $xss = '<script type="text/javascript">alert("xss")</script>'; + $edit = ['description' => $xss]; + $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH, $edit, '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'); + } + + /** + * Test the CAPTCHA placement clearing. + */ + public function testCaptchaPlacementCacheClearing() { + // Set CAPTCHA on user register form. + captcha_set_form_id_setting('user_register_form', 'captcha/Math'); + // Visit user register form to fill the CAPTCHA placement cache. + $this->drupalGet('user/register'); + // Check if there is CAPTCHA placement cache. + $placement_map = $this->container->get('cache.default') + ->get('captcha_placement_map_cache'); + $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')); + // Check that the placement cache is unset. + $placement_map = $this->container->get('cache.default') + ->get('captcha_placement_map_cache'); + $this->assertFalse($placement_map, 'CAPTCHA placement cache should be unset after cache clear.'); + } + + /** + * Helper function to get CAPTCHA point setting straight from the database. + * + * @param string $form_id + * Form machine ID. + * + * @return \Drupal\captcha\Entity\CaptchaPoint + * CaptchaPoint with mysql query result. + */ + protected function getCaptchaPointSettingFromDatabase($form_id) { + $ids = \Drupal::entityQuery('captcha_point') + ->condition('formId', $form_id) + ->execute(); + return $ids ? CaptchaPoint::load(reset($ids)) : NULL; + } + + /** + * Method for testing the CAPTCHA point administration. + */ + public function testCaptchaPointAdministration() { + // Generate CAPTCHA point data: + // Drupal form ID should consist of lowercase alphanumerics and underscore). + $captcha_point_form_id = 'form_' . strtolower($this->randomMachineName(32)); + // The Math CAPTCHA by the CAPTCHA module is always available, + // so let's use it. + $captcha_point_module = 'captcha'; + $captcha_point_type = 'Math'; + + // Log in as admin. + $this->drupalLogin($this->adminUser); + $label = 'TEST'; + + // Try and set CAPTCHA point without the #required label. Should fail. + $form_values = [ + '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.')); + + // 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])); + + // Check in database. + /* @var CaptchaPoint result */ + $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'); + + // 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'); + + // Check in database. + $result = $this->getCaptchaPointSettingFromDatabase($captcha_point_form_id); + + // Set CAPTCHA point via admin/user/captcha/captcha/captcha_point/$form_id. + $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'); + + // Check 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'); + + // 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]), + 'Deleting of CAPTCHA point'); + + $result = $this->getCaptchaPointSettingFromDatabase($captcha_point_form_id); + $this->assertFalse($result, 'Deleted CAPTCHA point should be in database'); + } + + /** + * Method for testing the CAPTCHA point administration. + */ + public function testCaptchaPointAdministrationByNonAdmin() { + // First add a CAPTCHA point (as admin). + $captcha_point_form_id = 'form_' . strtolower($this->randomMachineName(32)); + $captcha_point_module = 'captcha'; + $captcha_point_type = 'Math'; + $label = 'TEST_2'; + + $this->drupalLogin($this->adminUser); + + $form_values = [ + 'label' => $label, + '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])); + + // Switch from admin to non-admin. + $this->drupalLogin($this->normalUser); + + // 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.'), + '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.'), + '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.'), + 'Non admin should not be able to delete a CAPTCHA point'); + + // Switch from nonadmin to admin again. + $this->drupalLogin($this->adminUser); + + // 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'); + + // 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'); + } + +} diff --git a/web/modules/captcha/src/Tests/CaptchaBaseWebTestCase.php b/web/modules/captcha/src/Tests/CaptchaBaseWebTestCase.php new file mode 100755 index 0000000000000000000000000000000000000000..267938e4fb566491fa6cb746eb11f12537217936 --- /dev/null +++ b/web/modules/captcha/src/Tests/CaptchaBaseWebTestCase.php @@ -0,0 +1,280 @@ +<?php + +namespace Drupal\captcha\Tests; + +use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface; +use Drupal\comment\Tests\CommentTestTrait; +use Drupal\field\Entity\FieldConfig; +use Drupal\simpletest\WebTestBase; + +/** + * The TODO list. + * + * @todo write test for CAPTCHAs on admin pages. + * @todo test for default challenge type. + * @todo test about placement (comment form, node forms, log in form, etc). + * @todo test if captcha_cron does it work right. + * @todo test custom CAPTCHA validation stuff. + * @todo test if entry on status report (Already X blocked form submissions). + * @todo test space ignoring validation of image CAPTCHA. + * @todo refactor the 'comment_body[0][value]' stuff. + */ + +/** + * Base class for CAPTCHA tests. + * + * Provides common setup stuff and various helper functions. + */ +abstract class CaptchaBaseWebTestCase extends WebTestBase { + + use CommentTestTrait; + + /** + * Wrong response error message. + */ + const CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE = 'The answer you entered for the CAPTCHA was not correct.'; + + /** + * Session reuse attack error message. + */ + const CAPTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE = 'CAPTCHA session reuse attack detected.'; + + /** + * Unknown CSID error message. + */ + const CAPTCHA_UNKNOWN_CSID_ERROR_MESSAGE = 'CAPTCHA validation error: unknown CAPTCHA session ID. Contact the site administrator if this problem persists.'; + + /** + * Modules to install for this Test class. + * + * @var array + */ + public static $modules = ['captcha', 'comment']; + + + /** + * User with various administrative permissions. + * + * @var \Drupal\user\Entity\User + */ + protected $adminUser; + + /** + * Normal visitor with limited permissions. + * + * @var \Drupal\user\Entity\User + */ + protected $normalUser; + + /** + * Form ID of comment form on standard (page) node. + */ + const COMMENT_FORM_ID = 'comment_comment_form'; + + const LOGIN_HTML_FORM_ID = 'user-login-form'; + + /** + * Drupal path of the (general) CAPTCHA admin page. + */ + const CAPTCHA_ADMIN_PATH = 'admin/config/people/captcha'; + + /** + * {@inheritdoc} + */ + public function setUp() { + // Load two modules: the captcha module itself and the comment + // module for testing anonymous comments. + parent::setUp(); + module_load_include('inc', 'captcha'); + + $this->drupalCreateContentType(['type' => 'page']); + + // Create a normal user. + $permissions = [ + 'access comments', + 'post comments', + 'skip comment approval', + 'access content', + 'create page content', + 'edit own page content', + ]; + $this->normalUser = $this->drupalCreateUser($permissions); + + // Create an admin user. + $permissions[] = 'administer CAPTCHA settings'; + $permissions[] = 'skip CAPTCHA'; + $permissions[] = 'administer permissions'; + $permissions[] = 'administer content types'; + $this->adminUser = $this->drupalCreateUser($permissions); + + // Open comment for page content type. + $this->addDefaultCommentField('node', 'page'); + + // Put comments on page nodes on a separate page. + $comment_field = FieldConfig::loadByName('node', 'page', 'comment'); + $comment_field->setSetting('form_location', CommentItemInterface::FORM_SEPARATE_PAGE); + $comment_field->save(); + + /* @var \Drupal\captcha\Entity\CaptchaPoint $captcha_point */ + $captcha_point = \Drupal::entityTypeManager() + ->getStorage('captcha_point') + ->load('user_login_form'); + $captcha_point->enable()->save(); + $this->config('captcha.settings') + ->set('default_challenge', 'captcha/test') + ->save(); + } + + /** + * Assert that the response is accepted. + * + * No "unknown CSID" message, no "CSID reuse attack detection" message, + * No "wrong answer" message. + */ + protected function assertCaptchaResponseAccepted() { + // There should be no error message about unknown CAPTCHA session ID. + $this->assertNoText(self::CAPTCHA_UNKNOWN_CSID_ERROR_MESSAGE, + 'CAPTCHA response should be accepted (known CSID).', + 'CAPTCHA' + ); + // There should be no error message about CSID reuse attack. + $this->assertNoText(self::CAPTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE, + 'CAPTCHA response should be accepted (no CAPTCHA session reuse attack detection).', + 'CAPTCHA' + ); + // There should be no error message about wrong response. + $this->assertNoText(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, + 'CAPTCHA response should be accepted (correct response).', + 'CAPTCHA' + ); + } + + /** + * Assert that there is a CAPTCHA on the form or not. + * + * @param bool $presence + * Whether there should be a CAPTCHA or not. + */ + protected function assertCaptchaPresence($presence) { + if ($presence) { + $this->assertText(_captcha_get_description(), + 'There should be a CAPTCHA on the form.', 'CAPTCHA' + ); + } + else { + $this->assertNoText(_captcha_get_description(), + 'There should be no CAPTCHA on the form.', 'CAPTCHA' + ); + } + } + + /** + * Helper function to generate a form values array for comment forms. + */ + protected function getCommentFormValues() { + $edit = [ + 'subject[0][value]' => 'comment_subject ' . $this->randomMachineName(32), + 'comment_body[0][value]' => 'comment_body ' . $this->randomMachineName(256), + ]; + + return $edit; + } + + /** + * Helper function to generate a form values array for node forms. + */ + protected function getNodeFormValues() { + $edit = [ + 'title[0][value]' => 'node_title ' . $this->randomMachineName(32), + 'body[0][value]' => 'node_body ' . $this->randomMachineName(256), + ]; + + return $edit; + } + + /** + * Get the CAPTCHA session id from the current form in the browser. + * + * @param null|string $form_html_id + * HTML form id attribute. + * + * @return int + * Captcha SID integer. + */ + protected function getCaptchaSidFromForm($form_html_id = NULL) { + if (!$form_html_id) { + $elements = $this->xpath('//input[@name="captcha_sid"]'); + } + else { + $elements = $this->xpath('//form[@id="' . $form_html_id . '"]//input[@name="captcha_sid"]'); + } + $captcha_sid = (int) $elements[0]['value']; + + return $captcha_sid; + } + + /** + * Get the CAPTCHA token from the current form in the browser. + * + * @param null|string $form_html_id + * HTML form id attribute. + * + * @return int + * Captcha token integer. + */ + protected function getCaptchaTokenFromForm($form_html_id = NULL) { + if (!$form_html_id) { + $elements = $this->xpath('//input[@name="captcha_token"]'); + } + else { + $elements = $this->xpath('//form[@id="' . $form_html_id . '"]//input[@name="captcha_token"]'); + } + $captcha_token = (int) $elements[0]['value']; + + return $captcha_token; + } + + /** + * Get the solution of the math CAPTCHA from the current form in the browser. + * + * @param null|string $form_html_id + * HTML form id attribute. + * + * @return int + * Calculated Math solution. + */ + protected function getMathCaptchaSolutionFromForm($form_html_id = NULL) { + // Get the math challenge. + if (!$form_html_id) { + $elements = $this->xpath('//div[contains(@class, "form-item-captcha-response")]/span[@class="field-prefix"]'); + } + 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)); + $challenge = (string) $elements[0]; + $this->assert('pass', $challenge); + // Extract terms and operator from challenge. + $matches = []; + preg_match('/\\s*(\\d+)\\s*(-|\\+)\\s*(\\d+)\\s*=\\s*/', $challenge, $matches); + // Solve the challenge. + $a = (int) $matches[1]; + $b = (int) $matches[3]; + $solution = $matches[2] == '-' ? $a - $b : $a + $b; + + return $solution; + } + + /** + * Helper function to allow comment posting for anonymous users. + */ + protected function allowCommentPostingForAnonymousVisitors() { + // Enable anonymous comments. + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, [ + 'access comments', + 'post comments', + 'skip comment approval', + ]); + } + +} diff --git a/web/modules/captcha/src/Tests/CaptchaCacheTestCase.php b/web/modules/captcha/src/Tests/CaptchaCacheTestCase.php new file mode 100644 index 0000000000000000000000000000000000000000..97bd0b017e74e3990b3bc2a20b689830a5433f0f --- /dev/null +++ b/web/modules/captcha/src/Tests/CaptchaCacheTestCase.php @@ -0,0 +1,74 @@ +<?php + +namespace Drupal\captcha\Tests; + +/** + * Tests CAPTCHA caching on various pages. + * + * @group captcha + */ +class CaptchaCacheTestCase extends CaptchaBaseWebTestCase { + + /** + * Modules to install for this Test class. + * + * @var array + */ + public static $modules = ['block', 'image_captcha']; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->drupalPlaceBlock('user_login_block', ['id' => 'login']); + } + + /** + * Test the cache tags. + */ + public function testCacheTags() { + global $base_path; + // Check caching without captcha as anonymous user. + $this->drupalGet(''); + $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS'); + $this->drupalGet(''); + $this->assertEqual($this->drupalGetHeader('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(); + $math_challenge = (string) $this->xpath('//span[@class="field-prefix"]')[0]; + $this->assertFalse($this->drupalGetHeader('x-drupal-cache'), 'Cache is disabled'); + $this->drupalGet(''); + $this->assertNotEqual($sid, $this->getCaptchaSidFromForm()); + $this->assertNotEqual($math_challenge, (string) $this->xpath('//span[@class="field-prefix"]')[0]); + + // 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->assertFalse($this->drupalGetHeader('x-drupal-cache'), 'Cache is disabled'); + $this->drupalGet(''); + $this->assertNotEqual($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 = (string) $this->xpath('//div[@class="details-wrapper"]/img/@src')[0]; + $this->assertFalse($this->drupalGetHeader('x-drupal-cache'), 'Cache disabled'); + // Check that we get a new image when vising the page again. + $this->drupalGet(''); + $this->assertNotEqual($image_path, (string) $this->xpath('//div[@class="details-wrapper"]/img/@src')[0]); + // Check image caching, remove the base path since drupalGet() expects the + // internal path. + $this->drupalGet(substr($image_path, strlen($base_path))); + $this->assertResponse(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); + } + +} diff --git a/web/modules/captcha/src/Tests/CaptchaCronTestCase.php b/web/modules/captcha/src/Tests/CaptchaCronTestCase.php new file mode 100644 index 0000000000000000000000000000000000000000..e0b24a93a844687e98b84ed6de0d96a73719a079 --- /dev/null +++ b/web/modules/captcha/src/Tests/CaptchaCronTestCase.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\captcha\Tests; + +use Drupal\Core\Database\Database; +use Drupal\simpletest\WebTestBase; + +/** + * Tests CAPTCHA cron. + * + * @group captcha + */ +class CaptchaCronTestCase extends WebTestBase { + + /** + * Modules to install for this Test class. + * + * @var array + */ + public static $modules = ['captcha']; + + /** + * Temporary captcha sessions storage. + * + * @var [int] + */ + public $captchaSessions; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + // Add removed session. + $time = REQUEST_TIME - 1 - 60 * 60 * 24; + $this->captchaSessions['remove_sid'] = $this->addCaptchaSession('captcha_cron_test_remove', $time); + // Add remain session. + $this->captchaSessions['remain_sid'] = $this->addCaptchaSession('captcha_cron_test_remain', REQUEST_TIME); + } + + /** + * Add test CAPTCHA session data. + * + * @param string $form_id + * Form id. + * @param int $request_time + * Timestamp. + * + * @return int + * CAPTCHA session id. + */ + public function addCaptchaSession($form_id, $request_time) { + // Initialize solution with random data. + $solution = hash('sha256', mt_rand()); + + // Insert an entry and thankfully receive the value + // of the autoincrement field 'csid'. + $connection = Database::getConnection(); + $captcha_sid = $connection->insert('captcha_sessions')->fields([ + 'uid' => 0, + 'sid' => session_id(), + 'ip_address' => \Drupal::request()->getClientIp(), + 'timestamp' => $request_time, + 'form_id' => $form_id, + 'solution' => $solution, + 'status' => 1, + 'attempts' => 0, + ])->execute(); + + return $captcha_sid; + } + + /** + * Test CAPTCHA cron. + */ + public function testCron() { + $this->cronRun(); + + $connection = Database::getConnection(); + $sids = $connection->select('captcha_sessions') + ->fields('captcha_sessions', ['csid']) + ->condition('csid', array_values($this->captchaSessions), 'IN') + ->execute() + ->fetchCol('csid'); + + // Test if CAPTCHA cron appropriately removes sessions older than a day. + $this->assertTrue(!in_array($this->captchaSessions['remove_sid'], $sids), 'CAPTCHA cron removes captcha session data older than 1 day.'); + + // Test if CAPTCHA cron appropriately keeps sessions younger than a day. + $this->assertTrue(in_array($this->captchaSessions['remain_sid'], $sids), 'CAPTCHA cron does not remove captcha session data younger than 1 day.'); + } + +} diff --git a/web/modules/captcha/src/Tests/CaptchaPersistenceTestCase.php b/web/modules/captcha/src/Tests/CaptchaPersistenceTestCase.php new file mode 100755 index 0000000000000000000000000000000000000000..8d14c3ebe6c206be863af6a1166d42972b69bb13 --- /dev/null +++ b/web/modules/captcha/src/Tests/CaptchaPersistenceTestCase.php @@ -0,0 +1,213 @@ +<?php + +namespace Drupal\captcha\Tests; + +/** + * Tests CAPTCHA Persistence. + * + * @group captcha + */ +class CaptchaPersistenceTestCase extends CaptchaBaseWebTestCase { + + /** + * Set up the persistence and CAPTCHA settings. + * + * @param int $persistence + * The persistence value. + */ + private function setUpPersistence($persistence) { + $this->drupalLogin($this->adminUser); + // Set persistence. + $edit = ['persistence' => $persistence]; + $this->drupalPostForm(self::CAPTCHA_ADMIN_PATH, $edit, 'Save configuration'); + // Log admin out. + $this->drupalLogout(); + + // Set the Test123 CAPTCHA on user register and comment form. + // We have to do this with the function captcha_set_form_id_setting() + // (because the CATCHA admin form does not show the Test123 option). + // We also have to do this after all usage of the CAPTCHA admin form + // (because posting the CAPTCHA admin form would set the CAPTCHA to 'none'). + captcha_set_form_id_setting('user_login_form', 'captcha/Test'); + $this->drupalGet('user'); + $this->assertCaptchaPresence(TRUE); + captcha_set_form_id_setting('user_register_form', 'captcha/Test'); + $this->drupalGet('user/register'); + $this->assertCaptchaPresence(TRUE); + } + + /** + * Check if Captcha sid present in form. + * + * @param string $captcha_sid_initial + * Captcha SID token. + */ + protected function assertPreservedCsid($captcha_sid_initial) { + $captcha_sid = $this->getCaptchaSidFromForm(); + $this->assertEqual($captcha_sid_initial, $captcha_sid, + "CAPTCHA session ID should be preserved (expected: $captcha_sid_initial, found: $captcha_sid)."); + } + + /** + * Check if message about SID present. + * + * @param string $captcha_sid_initial + * Captcha SID token. + */ + protected function assertDifferentCsid($captcha_sid_initial) { + $captcha_sid = $this->getCaptchaSidFromForm(); + $this->assertNotEqual($captcha_sid_initial, $captcha_sid, "CAPTCHA session ID should be different."); + } + + /** + * Test persistence always. + */ + public function testPersistenceAlways() { + // Set up of persistence and CAPTCHAs. + $this->setUpPersistence(CAPTCHA_PERSISTENCE_SHOW_ALWAYS); + + // Go to login form and check if there is a CAPTCHA + // on the login form (look for the title). + $this->drupalGet('<front>'); + $this->assertCaptchaPresence(TRUE); + $captcha_sid_initial = $this->getCaptchaSidFromForm(); + + // Try to with wrong user name and password, but correct CAPTCHA. + $edit = [ + 'name' => 'foobar', + 'pass' => 'bazlaz', + 'captcha_response' => 'Test 123', + ]; + $this->drupalPostForm(NULL, $edit, t('Log in'), [], [], self::LOGIN_HTML_FORM_ID); + // Check that there was no error message for the CAPTCHA. + $this->assertCaptchaResponseAccepted(); + + // Name and password were wrong, we should get an updated + // form with a fresh CAPTCHA. + $this->assertCaptchaPresence(TRUE); + $this->assertPreservedCsid($captcha_sid_initial); + + // Post from again. + $this->drupalPostForm(NULL, $edit, 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); + } + + /** + * Test persistence per form instance. + */ + public function testPersistencePerFormInstance() { + // Set up of persistence and CAPTCHAs. + $this->setUpPersistence(CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE); + + // Go to login form and check if there is a CAPTCHA on the login form. + $this->drupalGet('<front>'); + $this->assertCaptchaPresence(TRUE); + $captcha_sid_initial = $this->getCaptchaSidFromForm(); + + // Try to with wrong user name and password, but correct CAPTCHA. + $edit = [ + 'name' => 'foobar', + 'pass' => 'bazlaz', + 'captcha_response' => 'Test 123', + ]; + $this->drupalPostForm(NULL, $edit, 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. + $this->assertCaptchaPresence(FALSE); + $this->assertPreservedCsid($captcha_sid_initial); + + // Start a new form instance/session. + $this->drupalGet('node'); + $this->drupalGet('user'); + $this->assertCaptchaPresence(TRUE); + $this->assertDifferentCsid($captcha_sid_initial); + + // Check another form. + $this->drupalGet('user/register'); + $this->assertCaptchaPresence(TRUE); + $this->assertDifferentCsid($captcha_sid_initial); + } + + /** + * Test Persistence per form type. + */ + public function testPersistencePerFormType() { + // Set up of persistence and CAPTCHAs. + $this->setUpPersistence(CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE); + + // Go to login form and check if there is a CAPTCHA on the login form. + $this->drupalGet('<front>'); + $this->assertCaptchaPresence(TRUE); + $captcha_sid_initial = $this->getCaptchaSidFromForm(); + + // Try to with wrong user name and password, but correct CAPTCHA. + $edit = [ + 'name' => 'foobar', + 'pass' => 'bazlaz', + 'captcha_response' => 'Test 123', + ]; + $this->drupalPostForm(NULL, $edit, 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. + $this->assertCaptchaPresence(FALSE); + $this->assertPreservedCsid($captcha_sid_initial); + + // Start a new form instance/session. + $this->drupalGet('node'); + $this->drupalGet('user'); + $this->assertCaptchaPresence(FALSE); + $this->assertDifferentCsid($captcha_sid_initial); + + // Check another form. + /* @var \Drupal\captcha\Entity\CaptchaPoint $captcha_point */ + $captcha_point = \Drupal::entityTypeManager() + ->getStorage('captcha_point') + ->load('user_register_form'); + $captcha_point->enable()->save(); + $this->drupalGet('user/register'); + $this->assertCaptchaPresence(TRUE); + $this->assertDifferentCsid($captcha_sid_initial); + } + + /** + * Test Persistence "Only once". + */ + public function testPersistenceOnlyOnce() { + // Set up of persistence and CAPTCHAs. + $this->setUpPersistence(CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL); + + // Go to login form and check if there is a CAPTCHA on the login form. + $this->drupalGet('<front>'); + $this->assertCaptchaPresence(TRUE); + $captcha_sid_initial = $this->getCaptchaSidFromForm(); + + // Try to with wrong user name and password, but correct CAPTCHA. + $edit = [ + 'name' => 'foobar', + 'pass' => 'bazlaz', + 'captcha_response' => 'Test 123', + ]; + $this->drupalPostForm(NULL, $edit, 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. + $this->assertCaptchaPresence(FALSE); + $this->assertPreservedCsid($captcha_sid_initial); + + // Start a new form instance/session. + $this->drupalGet('node'); + $this->drupalGet('user'); + $this->assertCaptchaPresence(FALSE); + $this->assertDifferentCsid($captcha_sid_initial); + + // Check another form. + $this->drupalGet('user/register'); + $this->assertCaptchaPresence(FALSE); + $this->assertDifferentCsid($captcha_sid_initial); + } + +} diff --git a/web/modules/captcha/src/Tests/CaptchaSessionReuseAttackTestCase.php b/web/modules/captcha/src/Tests/CaptchaSessionReuseAttackTestCase.php new file mode 100755 index 0000000000000000000000000000000000000000..040bba9c32134a10c45ca2bd5e7ef43c0e052dd6 --- /dev/null +++ b/web/modules/captcha/src/Tests/CaptchaSessionReuseAttackTestCase.php @@ -0,0 +1,190 @@ +<?php + +namespace Drupal\captcha\Tests; + +/** + * Tests CAPTCHA session reusing. + * + * @group captcha + */ +class CaptchaSessionReuseAttackTestCase extends CaptchaBaseWebTestCase { + + /** + * Assert that the CAPTCHA session ID reuse attack was detected. + */ + protected function assertCaptchaSessionIdReuseAttackDetection() { + $this->assertText(self::CAPTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE, + 'CAPTCHA session ID reuse attack should be detected.', + 'CAPTCHA' + ); + // There should be an error message about wrong response. + $this->assertText(self::CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE, + 'CAPTCHA response should flagged as wrong.', + 'CAPTCHA' + ); + } + + /** + * Test captcha attack detection on comment form. + */ + public function testCaptchaSessionReuseAttackDetectionOnCommentPreview() { + // Create commentable node. + $node = $this->drupalCreateNode(); + // Set Test CAPTCHA on comment form. + captcha_set_form_id_setting(self::COMMENT_FORM_ID, 'captcha/Test'); + $this->config('captcha.settings') + ->set('persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE) + ->save(); + + // Log in as normal user. + $this->drupalLogin($this->normalUser); + + // Go to comment form of commentable node. + $this->drupalGet('comment/reply/node/' . $node->id() . '/comment'); + $this->assertCaptchaPresence(TRUE); + + // Get CAPTCHA session ID and solution of the challenge. + $captcha_sid = $this->getCaptchaSidFromForm(); + $captcha_token = $this->getCaptchaTokenFromForm(); + $solution = "Test 123"; + + // Post the form with the solution. + $edit = $this->getCommentFormValues(); + $edit['captcha_response'] = $solution; + $this->drupalPostForm(NULL, $edit, t('Preview')); + // Answer should be accepted and further CAPTCHA omitted. + $this->assertCaptchaResponseAccepted(); + $this->assertCaptchaPresence(FALSE); + + // Post a new comment, reusing the previous CAPTCHA session. + $edit = $this->getCommentFormValues(); + $edit['captcha_sid'] = $captcha_sid; + $edit['captcha_token'] = $captcha_token; + $edit['captcha_response'] = $solution; + $this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $edit, t('Preview')); + // CAPTCHA session reuse attack should be detected. + $this->assertCaptchaSessionIdReuseAttackDetection(); + // There should be a CAPTCHA. + $this->assertCaptchaPresence(TRUE); + } + + /** + * Test captcha attach detection on node form. + */ + public function testCaptchaSessionReuseAttackDetectionOnNodeForm() { + // Set CAPTCHA on page form. + captcha_set_form_id_setting('node_page_form', 'captcha/Test'); + $this->config('captcha.settings') + ->set('persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE) + ->save(); + + // Log in as normal user. + $this->drupalLogin($this->normalUser); + + // Go to node add form. + $this->drupalGet('node/add/page'); + $this->assertCaptchaPresence(TRUE); + + // Get CAPTCHA session ID and solution of the challenge. + $captcha_sid = $this->getCaptchaSidFromForm(); + $captcha_token = $this->getCaptchaTokenFromForm(); + $solution = "Test 123"; + + // Page settings to post, with correct CAPTCHA answer. + $edit = $this->getNodeFormValues(); + $edit['captcha_response'] = $solution; + // Preview the node. + $this->drupalPostForm(NULL, $edit, t('Preview')); + // Answer should be accepted. + $this->assertCaptchaResponseAccepted(); + // Check that there is no CAPTCHA after preview. + $this->assertCaptchaPresence(FALSE); + + // Post a new comment, reusing the previous CAPTCHA session. + $edit = $this->getNodeFormValues(); + $edit['captcha_sid'] = $captcha_sid; + $edit['captcha_token'] = $captcha_token; + $edit['captcha_response'] = $solution; + $this->drupalPostForm('node/add/page', $edit, t('Preview')); + // CAPTCHA session reuse attack should be detected. + $this->assertCaptchaSessionIdReuseAttackDetection(); + // There should be a CAPTCHA. + $this->assertCaptchaPresence(TRUE); + } + + /** + * Test Captcha attack detection on login form. + */ + public function testCaptchaSessionReuseAttackDetectionOnLoginForm() { + // Set CAPTCHA on login form. + captcha_set_form_id_setting('user_login_form', 'captcha/Test'); + $this->config('captcha.settings') + ->set('persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE) + ->save(); + + // Go to log in form. + // @TODO Bartik has two login forms because of sidebar's one on + // user page that's why we have a bug. + $this->drupalGet('<front>'); + $this->assertCaptchaPresence(TRUE); + + // Get CAPTCHA session ID and solution of the challenge. + $captcha_sid = $this->getCaptchaSidFromForm(); + $captcha_token = $this->getCaptchaTokenFromForm(); + $solution = "Test 123"; + + // Log in through form. + $edit = [ + 'name' => $this->normalUser->getUsername(), + 'pass' => $this->normalUser->pass_raw, + 'captcha_response' => $solution, + ]; + $this->drupalPostForm(NULL, $edit, t('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->getUsername()); + + // Log out again. + $this->drupalLogout(); + + // Try to log in again, reusing the previous CAPTCHA session. + $edit += [ + 'captcha_sid' => $captcha_sid, + 'captcha_token' => $captcha_token, + ]; + $this->assert('pass', json_encode($edit)); + $this->drupalPostForm('<front>', $edit, t('Log in')); + // CAPTCHA session reuse attack should be detected. + $this->assertCaptchaSessionIdReuseAttackDetection(); + // There should be a CAPTCHA. + $this->assertCaptchaPresence(TRUE); + } + + /** + * Test multiple captcha widgets on single page. + */ + public function testMultipleCaptchaProtectedFormsOnOnePage() { + \Drupal::service('module_installer')->install(['block']); + $this->drupalPlaceBlock('user_login_block'); + // Set Test CAPTCHA on comment form and login block. + captcha_set_form_id_setting(self::COMMENT_FORM_ID, 'captcha/Test'); + captcha_set_form_id_setting('user_login_form', 'captcha/Test'); + $this->allowCommentPostingForAnonymousVisitors(); + + // Create a node with comments enabled. + $node = $this->drupalCreateNode(); + + // Preview comment with correct CAPTCHA answer. + $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')); + // 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); + } + +} diff --git a/web/modules/captcha/src/Tests/CaptchaTestCase.php b/web/modules/captcha/src/Tests/CaptchaTestCase.php new file mode 100755 index 0000000000000000000000000000000000000000..924c1f7f3a2ee308b1e6911b9a9cc1bc77aae900 --- /dev/null +++ b/web/modules/captcha/src/Tests/CaptchaTestCase.php @@ -0,0 +1,262 @@ +<?php + +namespace Drupal\captcha\Tests; + +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; + +/** + * Tests CAPTCHA main test case sensitivity. + * + * @group captcha + */ +class CaptchaTestCase extends CaptchaBaseWebTestCase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['block']; + + /** + * Testing the protection of the user log in form. + */ + public function testCaptchaOnLoginForm() { + // Create user and test log in without CAPTCHA. + $user = $this->drupalCreateUser(); + $this->drupalLogin($user); + // Log out again. + $this->drupalLogout(); + + // 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 there is a CAPTCHA on the login form (look for the title). + $this->drupalGet(''); + $this->assertCaptchaPresence(TRUE); + + // Try to log in, which should fail. + $edit = [ + 'name' => $user->getUsername(), + 'pass' => $user->pass_raw, + 'captcha_response' => '?', + ]; + $this->drupalPostForm(NULL, $edit, 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'); + + // 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'); + } + + /** + * Assert function for testing if comment posting works as it should. + * + * Creates node with comment writing enabled, tries to post comment + * with given CAPTCHA response (caller should enable the desired + * challenge on page node comment forms) and checks if + * the result is as expected. + * + * @param string $captcha_response + * The response on the CAPTCHA. + * @param bool $should_pass + * Describing if the posting should pass or should be blocked. + * @param string $message + * To prefix to nested asserts. + */ + protected function assertCommentPosting($captcha_response, $should_pass, $message) { + // Make sure comments on pages can be saved directly without preview. + $this->container->get('state') + ->set('comment_preview_page', DRUPAL_OPTIONAL); + + // Create a node with comments enabled. + $node = $this->drupalCreateNode(); + + // Post comment on node. + $edit = $this->getCommentFormValues(); + $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'); + + 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'); + } + else { + // Check for error message. + $this->assertText(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'); + } + } + + /** + * Testing the case sensitive/insensitive validation. + */ + public function testCaseInsensitiveValidation() { + $config = $this->config('captcha.settings'); + // Set Test CAPTCHA on comment form. + captcha_set_form_id_setting(self::COMMENT_FORM_ID, 'captcha/Test'); + + // Log in as normal user. + $this->drupalLogin($this->normalUser); + + // Test case sensitive posting. + $config->set('default_validation', CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE); + $config->save(); + + $this->assertCommentPosting('Test 123', TRUE, 'Case sensitive validation of right casing.'); + $this->assertCommentPosting('test 123', FALSE, 'Case sensitive validation of wrong casing.'); + $this->assertCommentPosting('TEST 123', FALSE, 'Case sensitive validation of wrong casing.'); + + // Test case insensitive posting (the default). + $config->set('default_validation', CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE); + $config->save(); + + $this->assertCommentPosting('Test 123', TRUE, 'Case insensitive validation of right casing.'); + $this->assertCommentPosting('test 123', TRUE, 'Case insensitive validation of wrong casing.'); + $this->assertCommentPosting('TEST 123', TRUE, 'Case insensitive validation of wrong casing.'); + } + + /** + * Test if the CAPTCHA description is only shown with challenge widgets. + * + * For example, when a comment is previewed with correct CAPTCHA answer, + * a challenge is generated and added to the form but removed in the + * pre_render phase. The CAPTCHA description should not show up either. + * + * @see testCaptchaSessionReuseOnNodeForms() + */ + public function testCaptchaDescriptionAfterCommentPreview() { + // Set Test CAPTCHA on comment form. + captcha_set_form_id_setting(self::COMMENT_FORM_ID, 'captcha/Test'); + + // Log in as normal user. + $this->drupalLogin($this->normalUser); + + // Create a node with comments enabled. + $node = $this->drupalCreateNode(); + + // 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')); + + // Check that there is no CAPTCHA after preview. + $this->assertCaptchaPresence(FALSE); + } + + /** + * Test if the CAPTCHA session ID is reused when previewing nodes. + * + * Node preview after correct response should not show CAPTCHA anymore. + * The preview functionality of comments and nodes works + * slightly different under the hood. + * CAPTCHA module should be able to handle both. + * + * @see testCaptchaDescriptionAfterCommentPreview() + */ + public function testCaptchaSessionReuseOnNodeForms() { + // Set Test CAPTCHA on page form. + captcha_set_form_id_setting('node_page_form', 'captcha/Test'); + + // Log in as normal user. + $this->drupalLogin($this->normalUser); + + // Page settings to post, with correct CAPTCHA answer. + $edit = $this->getNodeFormValues(); + $edit['captcha_response'] = 'Test 123'; + $this->drupalGet('node/add/page'); + $this->drupalPostForm(NULL, $edit, t('Preview')); + + $this->assertCaptchaPresence(FALSE); + } + + /** + * CAPTCHA should be put on admin pages even if visitor has no access. + */ + public function testCaptchaOnLoginBlockOnAdminPagesIssue893810() { + // Set a CAPTCHA on login block 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(); + + // Enable the user login block. + $this->drupalPlaceBlock('user_login_block', ['id' => 'login']); + + // Check if there is a CAPTCHA on home page. + $this->drupalGet(''); + $this->assertCaptchaPresence(TRUE); + + // Check there is a CAPTCHA on "forbidden" admin pages. + $this->drupalGet('admin'); + $this->assertCaptchaPresence(TRUE); + } + + /** + * Tests that the CAPTCHA is not changed on AJAX form rebuilds. + */ + public function testAjaxFormRebuild() { + // Setup captcha point for user edit form. + \Drupal::entityManager()->getStorage('captcha_point')->create([ + 'id' => 'user_form', + 'formId' => 'user_form', + 'status' => TRUE, + 'captchaType' => 'captcha/Math', + ])->save(); + + // Add multiple text field on user edit form. + $field_storage_config = FieldStorageConfig::create([ + 'field_name' => 'field_texts', + 'type' => 'string', + 'entity_type' => 'user', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ]); + $field_storage_config->save(); + FieldConfig::create([ + 'field_storage' => $field_storage_config, + 'bundle' => 'user', + ])->save(); + entity_get_form_display('user', 'user', 'default') + ->setComponent('field_texts', [ + 'type' => 'string_textfield', + 'weight' => 10, + ]) + ->save(); + + // Create and login a user. + $user = $this->drupalCreateUser([]); + $this->drupalLogin($user); + + // On edit form, add another item and save. + $this->drupalGet("user/{$user->id()}/edit"); + $this->drupalPostAjaxForm(NULL, [], 'field_texts_add_more'); + $this->drupalPostForm(NULL, [ + 'captcha_response' => $this->getMathCaptchaSolutionFromForm('user-form'), + ], t('Save')); + + // No error. + $this->assertText(t('The changes have been saved.')); + } + +} diff --git a/web/modules/captcha/templates/captcha.html.twig b/web/modules/captcha/templates/captcha.html.twig new file mode 100755 index 0000000000000000000000000000000000000000..cf3b2cf952b4a7aab53ddb4b168679de87b70357 --- /dev/null +++ b/web/modules/captcha/templates/captcha.html.twig @@ -0,0 +1,5 @@ +{% if details %} + {{ details }} +{% else %} + <div class="captcha">{{ element }}</div> +{% endif %} \ No newline at end of file diff --git a/web/modules/recaptcha/.eslintignore b/web/modules/recaptcha/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..a253fe8158db6f050b8f61f71c150790c3a3a476 --- /dev/null +++ b/web/modules/recaptcha/.eslintignore @@ -0,0 +1 @@ +recaptcha-php/** diff --git a/web/modules/recaptcha/LICENSE.txt b/web/modules/recaptcha/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1 --- /dev/null +++ b/web/modules/recaptcha/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/modules/recaptcha/README.txt b/web/modules/recaptcha/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..6cc216f4a2e2dbcccf7e310d74b1d6511c94b29e --- /dev/null +++ b/web/modules/recaptcha/README.txt @@ -0,0 +1,51 @@ +reCAPTCHA for Drupal +==================== + +The reCAPTCHA module uses the reCAPTCHA web service to +improve the CAPTCHA system and protect email addresses. For +more information on what reCAPTCHA is, please visit: + https://www.google.com/recaptcha + +This version of the module uses the new Google No CAPTCHA reCAPTCHA API. + +DEPENDENCIES +------------ + +* reCAPTCHA module depends on the CAPTCHA module. + https://drupal.org/project/captcha + + +CONFIGURATION +------------- + +1. Enable reCAPTCHA and CAPTCHA modules in: + admin/modules + +2. You'll now find a reCAPTCHA tab in the CAPTCHA + administration page available at: + admin/config/people/captcha/recaptcha + +3. Register your web site at + https://www.google.com/recaptcha/admin/create + +4. Input the site and private keys into the reCAPTCHA settings. + +5. Visit the Captcha administration page and set where you + want the reCAPTCHA form to be presented: + admin/config/people/captcha + +KNOWN ISSUES +------------ + +- cURL requests fail because of outdated root certificate. The reCAPTCHA module + may not able to connect to Google servers and fails to verify the answer. + + See https://www.drupal.org/node/2481341 for more information. + + +THANK YOU +--------- + + * Thank you goes to the reCAPTCHA team for all their + help, support and their amazing Captcha solution + https://www.google.com/recaptcha diff --git a/web/modules/recaptcha/composer.json b/web/modules/recaptcha/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..c4d75d37ee4a18327fc4845ec600702f332d9cc7 --- /dev/null +++ b/web/modules/recaptcha/composer.json @@ -0,0 +1,24 @@ +{ + "name": "drupal/recaptcha", + "description": "Protect your website from spam and abuse while letting real people pass through with ease.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/recaptcha", + "authors": [ + { + "name": "hass", + "homepage": "https://www.drupal.org/u/hass" + }, + { + "name": "See other contributors", + "homepage":"https://www.drupal.org/node/147903/committers" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/recaptcha", + "source": "https://git.drupal.org/project/recaptcha.git" + }, + "license": "GPL-2.0+", + "require": { + "drupal/captcha": "^1.0.0-alpha1" + } +} diff --git a/web/modules/recaptcha/config/install/recaptcha.settings.yml b/web/modules/recaptcha/config/install/recaptcha.settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..4591931228385d24888aca2b0b86fb5a89e08900 --- /dev/null +++ b/web/modules/recaptcha/config/install/recaptcha.settings.yml @@ -0,0 +1,10 @@ +site_key: '' +secret_key: '' +verify_hostname: false +use_globally: false +widget: + theme: 'light' + type: 'image' + size: '' + tabindex: 0 + noscript: false diff --git a/web/modules/recaptcha/config/schema/recaptcha.schema.yml b/web/modules/recaptcha/config/schema/recaptcha.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..dcc3b570d0fed7638abd7fd58cc7d969c9026c4b --- /dev/null +++ b/web/modules/recaptcha/config/schema/recaptcha.schema.yml @@ -0,0 +1,37 @@ +# Schema for the configuration files of the recaptcha module. + +recaptcha.settings: + type: config_object + label: 'reCAPTCHA settings' + mapping: + site_key: + type: string + label: 'Site key' + secret_key: + type: string + label: 'Secret key' + verify_hostname: + type: boolean + label: 'Local domain name validation' + use_globally: + type: boolean + label: 'Use reCAPTCHA globally' + widget: + type: mapping + label: 'Widget settings' + mapping: + theme: + type: string + label: 'Theme' + type: + type: string + label: 'Type' + size: + type: string + label: 'Size' + tabindex: + type: integer + label: 'Tabindex' + noscript: + type: boolean + label: 'Enable fallback for browsers with JavaScript disabled' diff --git a/web/modules/recaptcha/migrations/d6_recaptcha_settings.yml b/web/modules/recaptcha/migrations/d6_recaptcha_settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..67283bca4e5600dfab13aca67468854c2f0fa710 --- /dev/null +++ b/web/modules/recaptcha/migrations/d6_recaptcha_settings.yml @@ -0,0 +1,27 @@ +id: d6_recaptcha_settings +label: reCAPTCHA 6 configuration +migration_groups: + - Drupal 6 + - Configuration +source: + plugin: variable + variables: + - recaptcha_noscript + - recaptcha_site_key + - recaptcha_size + - recaptcha_secret_key + - recaptcha_tabindex + - recaptcha_theme + - recaptcha_type + source_module: recaptcha +process: + site_key: recaptcha_site_key + secret_key: recaptcha_secret_key + 'widget/theme': recaptcha_theme + 'widget/type': recaptcha_type + 'widget/size': recaptcha_size + 'widget/tabindex': recaptcha_tabindex + 'widget/noscript': recaptcha_noscript +destination: + plugin: config + config_name: recaptcha.settings diff --git a/web/modules/recaptcha/migrations/d7_recaptcha_settings.yml b/web/modules/recaptcha/migrations/d7_recaptcha_settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..3b58b51c2f8ebcdf00161ae337ebc78bfea52419 --- /dev/null +++ b/web/modules/recaptcha/migrations/d7_recaptcha_settings.yml @@ -0,0 +1,31 @@ +id: d7_recaptcha_settings +label: reCAPTCHA 7 configuration +migration_groups: + - Drupal 7 + - Configuration +source: + plugin: variable + variables: + - recaptcha_noscript + - recaptcha_site_key + - recaptcha_size + - recaptcha_secret_key + - recaptcha_tabindex + - recaptcha_theme + - recaptcha_type + - recaptcha_use_globally + - recaptcha_verify_hostname + source_module: recaptcha +process: + site_key: recaptcha_site_key + secret_key: recaptcha_secret_key + verify_hostname: recaptcha_verify_hostname + use_globally: recaptcha_use_globally + 'widget/theme': recaptcha_theme + 'widget/type': recaptcha_type + 'widget/size': recaptcha_size + 'widget/tabindex': recaptcha_tabindex + 'widget/noscript': recaptcha_noscript +destination: + plugin: config + config_name: recaptcha.settings diff --git a/web/modules/recaptcha/phpcs.xml.dist b/web/modules/recaptcha/phpcs.xml.dist new file mode 100644 index 0000000000000000000000000000000000000000..73ef67a4384522be490a023216495f7f8c4b3f7c --- /dev/null +++ b/web/modules/recaptcha/phpcs.xml.dist @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<ruleset name="reCAPTCHA for Drupal"> + <description>reCAPTCHA for Drupal PHP_CodeSniffer standards overrides.</description> + <file>.</file> + <arg name="extensions" value="inc,install,module,php,profile,test,theme,yml"/> + + <!-- Include existing standards. --> + <rule ref="Drupal"/> + <rule ref="DrupalPractice"/> + + <exclude-pattern>recaptcha-php/*</exclude-pattern> +</ruleset> \ No newline at end of file diff --git a/web/modules/recaptcha/recaptcha-php/.github/ISSUE_TEMPLATE/bug_report.md b/web/modules/recaptcha/recaptcha-php/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000000000000000000000000000000..a14dcfeebb8ceaa0c9d7a382a351644ea87a9140 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: PHP client issue +about: Report an issue with the PHP client library + +--- + +**Issue description** +<!-- One or two sentences describing the problem --> + +**Environment** +<!-- The server or development environment where you're seeing the problem --> + + * OS name and version: + * PHP version: + * Web server name and version: + * `google/recaptcha` version: + * Browser name and version: + +**Reproducing the issue** +<!-- Where possible link to a URL where the problem can be seen or show code that causes it --> + + * URL (optional): <!-- if your integration is already deployed and the issue is visible --> + * Code (optional): <!-- share a link to the code you're using or an example in a Gist --> + + ***User steps*** + <!-- Detail the necessary steps to reproduce the issue. Include the output of any error messages. --> + + 1. Visit page... diff --git a/web/modules/recaptcha/recaptcha-php/.gitignore b/web/modules/recaptcha/recaptcha-php/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1c7edf1a022b704fff1ea97e602bcf60f69c85b3 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/.gitignore @@ -0,0 +1,6 @@ +/.php_cs.cache +/build +/composer.lock +/examples/config.php +/nbproject/private/ +/vendor/ diff --git a/web/modules/recaptcha/recaptcha-php/.travis.yml b/web/modules/recaptcha/recaptcha-php/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..13f9d7ea8b040a8c3ffcaa105f61e9c8dd4a805c --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/.travis.yml @@ -0,0 +1,31 @@ +dist: trusty + +language: php + +sudo: false + +php: + - '5.5' + - '5.6' + - '7.0' + - '7.1' + +before_script: + - composer install + - phpenv version-name | grep ^5.[34] && echo "extension=apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true + - phpenv version-name | grep ^5.[34] && echo "apc.enable_cli=1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true + +script: + - mkdir -p build/logs + - composer run-script lint + - composer run-script test + +after_success: + - travis_retry php vendor/bin/php-coveralls + +cache: + directories: + - "$HOME/.composer/cache/files" + +git: + depth: 5 diff --git a/web/modules/recaptcha/recaptcha-php/ARCHITECTURE.md b/web/modules/recaptcha/recaptcha-php/ARCHITECTURE.md new file mode 100644 index 0000000000000000000000000000000000000000..13add26535d710661af4027bf6b4f84cc7ea767a --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/ARCHITECTURE.md @@ -0,0 +1,64 @@ +# Architecture + +The general pattern of usage is to instantiate the `ReCaptcha` class with your +secret key, specify any additional validation rules, and then call `verify()` +with the reCAPTCHA response and user's IP address. For example: + +```php +<?php +$recaptcha = new \ReCaptcha\ReCaptcha($secret); +$resp = $recaptcha->setExpectedHostname('recaptcha-demo.appspot.com') + ->verify($gRecaptchaResponse, $remoteIp); +if ($resp->isSuccess()) { + // Verified! +} else { + $errors = $resp->getErrorCodes(); +} +``` + +By default, this will use the +[`stream_context_create()`](https://secure.php.net/stream_context_create) and +[`file_get_contents()`](https://secure.php.net/file_get_contents) to make a POST +request to the reCAPTCHA service. This is handled by the +[`RequestMethod\Post`](./src/ReCaptcha/RequestMethod/Post.php) class. + +## Alternate request methods + +You may need to use other methods for making requests in your environment. The +[`ReCaptcha`](./src/ReCaptcha/ReCaptcha.php) class allows an optional +[`RequestMethod`](./src/ReCaptcha/RequestMethod.php) instance to configure this. +For example, if you want to use [cURL](https://secure.php.net/curl) instead you +can do this: + +```php +<?php +$recaptcha = new \ReCaptcha\ReCaptcha($secret, new \ReCaptcha\RequestMethod\CurlPost()); +``` + +Alternatively, you can also use a [socket](https://secure.php.net/fsockopen): + +```php +<?php +$recaptcha = new \ReCaptcha\ReCaptcha($secret, new \ReCaptcha\RequestMethod\SocketPost()); +``` + +## Adding new request methods + +Create a class that implements the +[`RequestMethod`](./src/ReCaptcha/RequestMethod.php) interface. The convention +is to name this class `RequestMethod\`_MethodType_`Post` and create a separate +`RequestMethod\`_MethodType_ class that wraps just the calls to the network +calls themselves. This means that the `RequestMethod\`_MethodType_`Post` can be +unit tested by passing in a mock. Take a look at +[`RequestMethod\CurlPost`](./src/ReCaptcha/RequestMethod/CurlPost.php) and +[`RequestMethod\Curl`](./src/ReCaptcha/RequestMethod/Curl.php) with the matching +[`RequestMethod/CurlPostTest`](./tests/ReCaptcha/RequestMethod/CurlPostTest.php) +to see this pattern in action. + +### Error conventions + +The client returns the response as provided by the reCAPTCHA services augmented +with additional error codes based on the client's checks. When adding a new +[`RequestMethod`](./src/ReCaptcha/RequestMethod.php) ensure that it returns the +`ReCaptcha::E_CONNECTION_FAILED` and `ReCaptcha::E_BAD_RESPONSE` where +appropriate. diff --git a/web/modules/recaptcha/recaptcha-php/CONTRIBUTING.md b/web/modules/recaptcha/recaptcha-php/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..a23686249715a134cb4779f64463e7448d803b1a --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing + +Want to contribute? Great! First, read this page (including the small print at +the end). + +## Contributor License Agreement + +Before we can use your code, you must sign the [Google Individual Contributor +License +Agreement](https://developers.google.com/open-source/cla/individual?csw=1) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review (a link will be +automatically added to your Pull Request) and a member has approved it, but you +must do it before we can put your code into our codebase. Before you start +working on a larger contribution, you should get in touch with us first through +the issue tracker with your idea so that we can help out and possibly guide you. +Coordinating up front makes it much easier to avoid frustration later on. + +## Linting and testing + +We use PHP Coding Standards Fixer to maintain coding standards and PHPUnit to +run our tests. For convenience, there are Composer scripts to run each of these: + +```sh +composer run-script lint +composer run-script test +``` + +These are run automatically by [Travis +CI](https://travis-ci.org/google/recaptcha) against your Pull Request, but it's +a good idea to run them locally before submission to avoid getting things +bounced back. That said, tests can be a little daunting so feel free to submit +your PR and ask for help. + +## Code reviews + +All submissions, including submissions by project members, require review. +Reviews are conducted on the Pull Requests. The reviews are there to ensure and +improve code quality, so treat them like a discussion and opportunity to learn. +Don't get disheartened if your Pull Request isn't just automatically approved. + +### The small print + +Contributions made by corporations are covered by a different agreement than the +one above, the Software Grant and Corporate Contributor License Agreement. diff --git a/web/modules/recaptcha/recaptcha-php/LICENSE b/web/modules/recaptcha/recaptcha-php/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f6412328f4c81c7e63b390ce88a4081e34b2b7f7 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/LICENSE @@ -0,0 +1,29 @@ +Copyright 2014, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/web/modules/recaptcha/recaptcha-php/README.md b/web/modules/recaptcha/recaptcha-php/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c007553413bc81c467e962a6dce7c4f6190b691d --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/README.md @@ -0,0 +1,139 @@ +# reCAPTCHA PHP client library + +[](https://travis-ci.org/google/recaptcha) +[](https://coveralls.io/github/google/recaptcha) +[](https://packagist.org/packages/google/recaptcha) +[](https://packagist.org/packages/google/recaptcha) + +reCAPTCHA is a free CAPTCHA service that protect websites from spam and abuse. +This is a PHP library that wraps up the server-side verification step required +to process responses from the reCAPTCHA service. This client supports both v2 +and v3. + +- reCAPTCHA: https://www.google.com/recaptcha +- This repo: https://github.com/google/recaptcha +- Version: 1.2.1 +- License: BSD, see [LICENSE](LICENSE) + +## Installation + +### Composer (recommended) + +Use [Composer](https://getcomposer.org) to install this library from Packagist: +[`google/recaptcha`](https://packagist.org/packages/google/recaptcha) + +Run the following command from your project directory to add the dependency: + +```sh +composer require google/recaptcha "^1.2" +``` + +Alternatively, add the dependency directly to your `composer.json` file: + +```json +"require": { + "google/recaptcha": "^1.2" +} +``` + +### Direct download + +Download the [ZIP file](https://github.com/google/recaptcha/archive/master.zip) +and extract into your project. An autoloader script is provided in +`src/autoload.php` which you can require into your script. For example: + +```php +require_once '/path/to/recaptcha/src/autoload.php'; +$recaptcha = new \ReCaptcha\ReCaptcha($secret); +``` + +The classes in the project are structured according to the +[PSR-4](http://www.php-fig.org/psr/psr-4/) standard, so you can also use your +own autoloader or require the needed files directly in your code. + +## Usage + +First obtain the appropriate keys for the type of reCAPTCHA you wish to +integrate for v2 at https://www.google.com/recaptcha/admin or v3 at +https://g.co/recaptcha/v3. + +Then follow the [integration guide on the developer +site](https://developers.google.com/recaptcha/intro) to add the reCAPTCHA +functionality into your frontend. + +This library comes in when you need to verify the user's response. On the PHP +side you need the response from the reCAPTCHA service and secret key from your +credentials. Instantiate the `ReCaptcha` class with your secret key, specify any +additional validation rules, and then call `verify()` with the reCAPTCHA +response and user's IP address. For example: + +```php +<?php +$recaptcha = new \ReCaptcha\ReCaptcha($secret); +$resp = $recaptcha->setExpectedHostname('recaptcha-demo.appspot.com') + ->verify($gRecaptchaResponse, $remoteIp); +if ($resp->isSuccess()) { + // Verified! +} else { + $errors = $resp->getErrorCodes(); +} +``` + +The following methods are available: + +- `setExpectedHostname($hostname)`: ensures the hostname matches. You must do + this if you have disabled "Domain/Package Name Validation" for your + credentials. +- `setExpectedApkPackageName($apkPackageName)`: if you're verifying a response + from an Android app. Again, you must do this if you have disabled + "Domain/Package Name Validation" for your credentials. +- `setExpectedAction($action)`: ensures the action matches for the v3 API. +- `setScoreThreshold($threshold)`: set a score theshold for responses from the + v3 API +- `setChallengeTimeout($timeoutSeconds)`: set a timeout between the user passing + the reCAPTCHA and your server processing it. + +Each of the `set`\*`()` methods return the `ReCaptcha` instance so you can chain +them together. For example: + +```php +<?php +$recaptcha = new \ReCaptcha\ReCaptcha($secret); +$resp = $recaptcha->setExpectedHostname('recaptcha-demo.appspot.com') + ->setExpectedAction('homepage') + ->setScoreThreshold(0.5) + ->verify($gRecaptchaResponse, $remoteIp); + +if ($resp->isSuccess()) { + // Verified! +} else { + $errors = $resp->getErrorCodes(); +} +``` + +You can find the constants for the libraries error codes in the `ReCaptcha` +class constants, e.g. `ReCaptcha::E_HOSTNAME_MISMATCH` + +For more details on usage and structure, see [ARCHITECTURE](ARCHITECTURE.md). + +### Examples + +You can see examples of each reCAPTCHA type in [examples/](examples/). You can +run the examples locally by using the Composer script: + +```sh +composer run-script serve-examples +``` + +This makes use of the in-built PHP dev server to host the examples at +http://localhost:8080/ + +These are also hosted on Google AppEngine Flexible environment at +https://recaptcha-demo.appspot.com/. This is configured by +[`app.yaml`](./app.yaml) which you can also use to [deploy to your own AppEngine +project](https://cloud.google.com/appengine/docs/flexible/php/download). + +## Contributing + +No one ever has enough engineers, so we're very happy to accept contributions +via Pull Requests. For details, see [CONTRIBUTING](CONTRIBUTING.md) diff --git a/web/modules/recaptcha/recaptcha-php/app.yaml b/web/modules/recaptcha/recaptcha-php/app.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b6ccaf18b74931295059544e9b1ca1ef0ae13c45 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/app.yaml @@ -0,0 +1,8 @@ +runtime: php +env: flex + +skip_files: +- tests + +runtime_config: + document_root: examples diff --git a/web/modules/recaptcha/recaptcha-php/composer.json b/web/modules/recaptcha/recaptcha-php/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..d4695b7ae29cc03fa16dff31ac71bf61d649e9ad --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/composer.json @@ -0,0 +1,39 @@ +{ + "name": "google/recaptcha", + "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.", + "type": "library", + "keywords": ["recaptcha", "captcha", "spam", "abuse"], + "homepage": "https://www.google.com/recaptcha/", + "license": "BSD-3-Clause", + "support": { + "forum": "https://groups.google.com/forum/#!forum/recaptcha", + "source": "https://github.com/google/recaptcha" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7", + "friendsofphp/php-cs-fixer": "^2.2.20|^2.12", + "php-coveralls/php-coveralls": "^2.1" + }, + "autoload": { + "psr-4": { + "ReCaptcha\\": "src/ReCaptcha" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "scripts": { + "lint": "vendor/bin/php-cs-fixer -vvv fix --using-cache=no --dry-run .", + "lint-fix": "vendor/bin/php-cs-fixer -vvv fix --using-cache=no .", + "test": "vendor/bin/phpunit --colors=always", + "serve-examples": "@php -S localhost:8080 -t examples" + }, + "config": { + "process-timeout": 0 + } +} diff --git a/web/modules/recaptcha/recaptcha-php/examples/appengine-https.php b/web/modules/recaptcha/recaptcha-php/examples/appengine-https.php new file mode 100644 index 0000000000000000000000000000000000000000..fb6feca0af7bb2646075dcd8352b3205919bdc97 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/appengine-https.php @@ -0,0 +1,33 @@ +<?php +/** + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +// Redirect to HTTPS by default (for AppEngine) +if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'http') { + header('HTTP/1.1 301 Moved Permanently'); + header('Location: https://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']); + exit(0); + } else { + header('Strict-Transport-Security: max-age=63072000; includeSubDomains; preload'); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/examples/config.php.dist b/web/modules/recaptcha/recaptcha-php/examples/config.php.dist new file mode 100644 index 0000000000000000000000000000000000000000..faea194b106c5e087430f4869f5e6c81ac76168b --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/config.php.dist @@ -0,0 +1,15 @@ +<?php +return [ + 'v2-standard' => [ + 'site' => '', + 'secret' => '', + ], + 'v2-invisible' => [ + 'site' => '', + 'secret' => '', + ], + 'v3' => [ + 'site' => '', + 'secret' => '', + ], +]; diff --git a/web/modules/recaptcha/recaptcha-php/examples/examples.css b/web/modules/recaptcha/recaptcha-php/examples/examples.css new file mode 100644 index 0000000000000000000000000000000000000000..3d10aa769706c286add9350215b314abbdfdb3ee --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/examples.css @@ -0,0 +1,37 @@ +body { + font-family: sans-serif; + margin: 0; + padding: 0; +} + +h1, +h2, +p { + margin: 0; + padding: 0.5rem 0 0 0; + font-weight: normal; +} + +h1, +h2 { + color: #222244; +} + +h2 { + font-style: italic; +} + +header { + padding: 0.5rem 2rem 0.5rem 2rem; + background: #f0f0f4; + border-bottom: 1px solid #aaaabb; +} + +main { + padding: 0.5rem 2rem 0.5rem 2rem; +} + +.form-field { + display: block; + margin: 1rem; +} diff --git a/web/modules/recaptcha/recaptcha-php/examples/google0afd8760fd68f119.html b/web/modules/recaptcha/recaptcha-php/examples/google0afd8760fd68f119.html new file mode 100644 index 0000000000000000000000000000000000000000..457c47179deefe1408beb7343f233ded996d1b3e --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/google0afd8760fd68f119.html @@ -0,0 +1 @@ +google-site-verification: google0afd8760fd68f119.html \ No newline at end of file diff --git a/web/modules/recaptcha/recaptcha-php/examples/index.php b/web/modules/recaptcha/recaptcha-php/examples/index.php new file mode 100644 index 0000000000000000000000000000000000000000..fec315848bdc141016fa2630ddf02a27e4ad11c6 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/index.php @@ -0,0 +1,65 @@ +<?php +/** + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +require __DIR__ . '/appengine-https.php'; +?> +<!DOCTYPE html> +<html lang="en"> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width,height=device-height,minimum-scale=1"> +<link rel="shortcut icon" href="https://www.gstatic.com/recaptcha/admin/favicon.ico" type="image/x-icon"/> +<link rel="canonical" href="https://recaptcha-demo.appspot.com/"> +<script type="application/ld+json">{ "@context": "http://schema.org", "@type": "WebSite", "name": "reCAPTCHA demo", "url": "http://recaptcha-demo.appspot.com/" }</script> +<meta name="description" content="reCAPTCHA demo" /> +<meta property="og:url" content="https://recaptcha-demo.appspot.com/" /> +<meta property="og:type" content="website" /> +<meta property="og:title" content="reCAPTCHA demo" /> +<meta property="og:description" content="Examples of the reCAPTCHA client." /> +<link rel="stylesheet" type="text/css" href="/examples.css"> +<title>reCAPTCHA demo</title> + +<header> + <h1>reCAPTCHA demo</h1> +</header> +<main> + <p>Try out the various forms of <a href="https://www.google.com/recaptcha/">reCAPTCHA</a>.</p> + <p>You can find the source code for these examples on GitHub in <kbd><a href="https://github.com/google/recaptcha">google/recaptcha</a></kbd>.</p> + <ul> + <li><h2>reCAPTCHA v2</h2> + <ul> + <li><a href="/recaptcha-v2-checkbox.php">"I'm not a robot" checkbox</a></li> + <li><a href="/recaptcha-v2-checkbox-explicit.php">"I'm not a robot" checkbox - Explicit render</a></li> + <li><a href="/recaptcha-v2-invisible.php">Invisible</a></li> + </ul> + </li> + <li><h2>reCAPTCHA v3</h2> + <ul> + <li><a href="/recaptcha-v3-request-scores.php">Request scores</a></li> + </ul> + </li> + </ul> +</main> + +<!-- Google Analytics - just ignore this --> +<script async src="https://www.googletagmanager.com/gtag/js?id=UA-123057962-1"></script> +<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-123057962-1');</script> diff --git a/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-checkbox-explicit.php b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-checkbox-explicit.php new file mode 100644 index 0000000000000000000000000000000000000000..5a53f199347959258b05dcf013292eab940548e8 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-checkbox-explicit.php @@ -0,0 +1,139 @@ +<?php +/** + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +require __DIR__ . '/appengine-https.php'; + +// Initiate the autoloader. The file should be generated by Composer. +// You will provide your own autoloader or require the files directly if you did +// not install via Composer. +require_once __DIR__ . '/../vendor/autoload.php'; + +// Register API keys at https://www.google.com/recaptcha/admin +$siteKey = ''; +$secret = ''; + +// Copy the config.php.dist file to config.php and update it with your keys to run the examples +if ($siteKey == '' && is_readable(__DIR__ . '/config.php')) { + $config = include __DIR__ . '/config.php'; + $siteKey = $config['v2-standard']['site']; + $secret = $config['v2-standard']['secret']; +} + +// reCAPTCHA supports 40+ languages listed here: https://developers.google.com/recaptcha/docs/language +$lang = 'en'; +?> +<!DOCTYPE html> +<html lang="en"> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width,height=device-height,minimum-scale=1"> +<link rel="shortcut icon" href="https://www.gstatic.com/recaptcha/admin/favicon.ico" type="image/x-icon"/> +<link rel="canonical" href="https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php"> +<script type="application/ld+json">{ "@context": "http://schema.org", "@type": "WebSite", "name": "reCAPTCHA demo - \"I'm not a robot\" checkbox - Explicit render", "url": "https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php" }</script> +<meta name="description" content="reCAPTCHA demo - "I'm not a robot" checkbox - Explicit render" /> +<meta property="og:url" content="https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox-explicit.php" /> +<meta property="og:type" content="website" /> +<meta property="og:title" content="reCAPTCHA demo - "I'm not a robot" checkbox - Explicit render" /> +<meta property="og:description" content="reCAPTCHA demo - "I'm not a robot" checkbox - Explicit render" /> +<link rel="stylesheet" type="text/css" href="/examples.css"> +<title>reCAPTCHA demo - "I'm not a robot" checkbox - Explicit render</title> + +<header> + <h1>reCAPTCHA demo</h1><h2>"I'm not a robot" checkbox - Explicit render</h2> + <p><a href="/">↤ Home</a></p> +</header> +<main> +<?php +if ($siteKey === '' || $secret === ''): +?> + <h2>Add your keys</h2> + <p>If you do not have keys already then visit <kbd> <a href = "https://www.google.com/recaptcha/admin">https://www.google.com/recaptcha/admin</a></kbd> to generate them. Edit this file and set the respective keys in the <kbd>config.php</kbd> file or directly to <kbd>$siteKey</kbd> and <kbd>$secret</kbd>. Reload the page after this.</p> + <?php +elseif (isset($_POST['g-recaptcha-response'])): + // The POST data here is unfiltered because this is an example. + // In production, *always* sanitise and validate your input' + ?> + <h2><kbd>POST</kbd> data</h2> + <kbd><pre><?php var_export($_POST);?></pre></kbd> + <?php + // If the form submission includes the "g-captcha-response" field + // Create an instance of the service using your secret + $recaptcha = new \ReCaptcha\ReCaptcha($secret); + + // If file_get_contents() is locked down on your PHP installation to disallow + // its use with URLs, then you can use the alternative request method instead. + // This makes use of fsockopen() instead. + // $recaptcha = new \ReCaptcha\ReCaptcha($secret, new \ReCaptcha\RequestMethod\SocketPost()); + // Make the call to verify the response and also pass the user's IP address + $resp = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME']) + ->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']); + + if ($resp->isSuccess()): + // If the response is a success, that's it! + ?> + <h2>Success!</h2> + <kbd><pre><?php var_export($resp);?></pre></kbd> + <p>That's it. Everything is working. Go integrate this into your real project.</p> + <p><a href="/recaptcha-v2-checkbox-explicit.php">⟳ Try again</a></p> + <?php + else: + // If it's not successful, then one or more error codes will be returned. + ?> + <h2>Something went wrong</h2> + <kbd><pre><?php var_export($resp);?></pre></kbd> + <p>Check the error code reference at <kbd><a href="https://developers.google.com/recaptcha/docs/verify#error-code-reference">https://developers.google.com/recaptcha/docs/verify#error-code-reference</a></kbd>. + <p><strong>Note:</strong> Error code <kbd>missing-input-response</kbd> may mean the user just didn't complete the reCAPTCHA.</p> + <p><a href="/recaptcha-v2-checkbox-explicit.php">⟳ Try again</a></p> + <?php + endif; +else: + // Add the g-recaptcha tag to the form you want to include the reCAPTCHA element + ?> + <p>Complete the reCAPTCHA then submit the form.</p> + <form action="/recaptcha-v2-checkbox-explicit.php" method="post"> + <fieldset> + <legend>An example form</legend> + <label class="form-field">Example input A: <input type="text" name="ex-a" value="foo"></label> + <label class="form-field">Example input B: <input type="text" name="ex-b" value="bar"></label> + <!-- Set up a container to render the widget --> + <div class="g-recaptcha form-field"></div> + <!-- Disable the button by default, will enable when the widget loads --> + <button class="form-field" type="submit" disabled>Submit ↦</button> + </fieldset> + </form> + <script type="text/javascript"> + var onloadCallback = function() { + var captchaContainer = document.querySelector('.g-recaptcha'); + grecaptcha.render(captchaContainer, { + 'sitekey' : '<?php echo $siteKey; ?>' + }); + document.querySelector('button[type="submit"]').disabled = false; + }; + </script> + <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?hl=<?php echo $lang; ?>&onload=onloadCallback&render=explicit" async defer></script> + <?php +endif;?> +</main> + +<!-- Google Analytics - just ignore this --> +<script async src="https://www.googletagmanager.com/gtag/js?id=UA-123057962-1"></script> +<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-123057962-1');</script> diff --git a/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-checkbox.php b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-checkbox.php new file mode 100644 index 0000000000000000000000000000000000000000..6f66c58febff9488a73c2f785cd7bc6201991d15 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-checkbox.php @@ -0,0 +1,130 @@ +<?php +/** + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +require __DIR__ . '/appengine-https.php'; + + // Initiate the autoloader. The file should be generated by Composer. +// You will provide your own autoloader or require the files directly if you did +// not install via Composer. +require_once __DIR__ . '/../vendor/autoload.php'; + +// Register API keys at https://www.google.com/recaptcha/admin +$siteKey = ''; +$secret = ''; + +// Copy the config.php.dist file to config.php and update it with your keys to run the examples +if ($siteKey == '' && is_readable(__DIR__ . '/config.php')) { + $config = include __DIR__ . '/config.php'; + $siteKey = $config['v2-standard']['site']; + $secret = $config['v2-standard']['secret']; +} + +// reCAPTCHA supports 40+ languages listed here: https://developers.google.com/recaptcha/docs/language +$lang = 'en'; +?> +<!DOCTYPE html> +<html lang="en"> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width,height=device-height,minimum-scale=1"> +<link rel="shortcut icon" href="https://www.gstatic.com/recaptcha/admin/favicon.ico" type="image/x-icon"/> +<link rel="canonical" href="https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php"> +<script type="application/ld+json">{ "@context": "http://schema.org", "@type": "WebSite", "name": "reCAPTCHA demo - \"I'm not a robot\" checkbox", "url": "https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php" }</script> +<meta name="description" content="reCAPTCHA demo - "I'm not a robot" checkbox" /> +<meta property="og:url" content="https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php" /> +<meta property="og:type" content="website" /> +<meta property="og:title" content="reCAPTCHA demo - "I'm not a robot" checkbox" /> +<meta property="og:description" content="reCAPTCHA demo - "I'm not a robot" checkbox" /> +<link rel="stylesheet" type="text/css" href="/examples.css"> +<title>reCAPTCHA demo - "I'm not a robot" checkbox</title> + +<header> + <h1>reCAPTCHA demo</h1><h2>"I'm not a robot" checkbox</h2> + <p><a href="/">↤ Home</a></p> +</header> +<main> +<?php +if ($siteKey === '' || $secret === ''): +?> + <h2>Add your keys</h2> + <p>If you do not have keys already then visit <kbd> <a href = "https://www.google.com/recaptcha/admin">https://www.google.com/recaptcha/admin</a></kbd> to generate them. Edit this file and set the respective keys in the <kbd>config.php</kbd> file or directly to <kbd>$siteKey</kbd> and <kbd>$secret</kbd>. Reload the page after this.</p> + <?php +elseif (isset($_POST['g-recaptcha-response'])): + // The POST data here is unfiltered because this is an example. + // In production, *always* sanitise and validate your input' + ?> + <h2><kbd>POST</kbd> data</h2> + <kbd><pre><?php var_export($_POST);?></pre></kbd> + <?php + // If the form submission includes the "g-captcha-response" field + // Create an instance of the service using your secret + $recaptcha = new \ReCaptcha\ReCaptcha($secret); + + // If file_get_contents() is locked down on your PHP installation to disallow + // its use with URLs, then you can use the alternative request method instead. + // This makes use of fsockopen() instead. + // $recaptcha = new \ReCaptcha\ReCaptcha($secret, new \ReCaptcha\RequestMethod\SocketPost()); + + // Make the call to verify the response and also pass the user's IP address + $resp = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME']) + ->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']); + if ($resp->isSuccess()): + // If the response is a success, that's it! + ?> + <h2>Success!</h2> + <kbd><pre><?php var_export($resp);?></pre></kbd> + <p>That's it. Everything is working. Go integrate this into your real project.</p> + <p><a href="/recaptcha-v2-checkbox.php">⟳ Try again</a></p> + <?php + else: + // If it's not successful, then one or more error codes will be returned. + ?> + <h2>Something went wrong</h2> + <kbd><pre><?php var_export($resp);?></pre></kbd> + <p>Check the error code reference at <kbd><a href="https://developers.google.com/recaptcha/docs/verify#error-code-reference">https://developers.google.com/recaptcha/docs/verify#error-code-reference</a></kbd>. + <p><strong>Note:</strong> Error code <kbd>missing-input-response</kbd> may mean the user just didn't complete the reCAPTCHA.</p> + <p><a href="/recaptcha-v2-checkbox.php">⟳ Try again</a></p> + <?php + endif; +else: + // Add the g-recaptcha tag to the form you want to include the reCAPTCHA element + ?> + <p>Complete the reCAPTCHA then submit the form.</p> + <form action="/recaptcha-v2-checkbox.php" method="post"> + <fieldset> + <legend>An example form</legend> + <label class="form-field">Example input A: <input type="text" name="ex-a" value="foo"></label> + <label class="form-field">Example input B: <input type="text" name="ex-b" value="bar"></label> + <!-- Default behaviour looks for the g-recaptcha class with a data-sitekey attribute --> + <div class="g-recaptcha form-field" data-sitekey="<?php echo $siteKey; ?>"></div> + <!-- Submitting before the widget loads will result in a missing-input-response error so you need to verify server side --> + <button class="form-field" type="submit">Submit ↦</button> + </fieldset> + </form> + <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?hl=<?php echo $lang; ?>"></script> + <?php +endif;?> +</main> + +<!-- Google Analytics - just ignore this --> +<script async src="https://www.googletagmanager.com/gtag/js?id=UA-123057962-1"></script> +<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-123057962-1');</script> diff --git a/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-invisible.php b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-invisible.php new file mode 100644 index 0000000000000000000000000000000000000000..e1aba6445433617b955d70c2da350127b389d270 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v2-invisible.php @@ -0,0 +1,132 @@ +<?php +/** + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +require __DIR__ . '/appengine-https.php'; + + // Initiate the autoloader. The file should be generated by Composer. +// You will provide your own autoloader or require the files directly if you did +// not install via Composer. +require_once __DIR__ . '/../vendor/autoload.php'; + +// Register API keys at https://www.google.com/recaptcha/admin +$siteKey = ''; +$secret = ''; + +// Copy the config.php.dist file to config.php and update it with your keys to run the examples +if ($siteKey == '' && is_readable(__DIR__ . '/config.php')) { + $config = include __DIR__ . '/config.php'; + $siteKey = $config['v2-invisible']['site']; + $secret = $config['v2-invisible']['secret']; +} + +// reCAPTCHA supports 40+ languages listed here: https://developers.google.com/recaptcha/docs/language +$lang = 'en'; +?> +<!DOCTYPE html> +<html lang="en"> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width,height=device-height,minimum-scale=1"> +<link rel="shortcut icon" href="https://www.gstatic.com/recaptcha/admin/favicon.ico" type="image/x-icon"/> +<link rel="canonical" href="https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php"> +<script type="application/ld+json">{ "@context": "http://schema.org", "@type": "WebSite", "name": "reCAPTCHA demo - Invisible", "url": "https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php" }</script> +<meta name="description" content="reCAPTCHA demo - Invisible" /> +<meta property="og:url" content="https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php" /> +<meta property="og:type" content="website" /> +<meta property="og:title" content="reCAPTCHA demo - Invisible" /> +<meta property="og:description" content="reCAPTCHA demo - Invisible" /> +<link rel="stylesheet" type="text/css" href="/examples.css"> +<title>reCAPTCHA demo - Invisible</title> + +<header> + <h1>reCAPTCHA demo</h1><h2>Invisible</h2> + <p><a href="/">↤ Home</a></p> +</header> +<main> +<?php +if ($siteKey === '' || $secret === ''): +?> + <h2>Add your keys</h2> + <p>If you do not have keys already then visit <kbd> <a href = "https://www.google.com/recaptcha/admin">https://www.google.com/recaptcha/admin</a></kbd> to generate them. Edit this file and set the respective keys in <kbd>$siteKey</kbd> and <kbd>$secret</kbd>. Reload the page after this.</p> + <?php +elseif (isset($_POST['g-recaptcha-response'])): + // The POST data here is unfiltered because this is an example. + // In production, *always* sanitise and validate your input' + ?> + <h2><kbd>POST</kbd> data</h2> + <kbd><pre><?php var_export($_POST);?></pre></kbd> + <?php + // If the form submission includes the "g-captcha-response" field + // Create an instance of the service using your secret + $recaptcha = new \ReCaptcha\ReCaptcha($secret); + + // If file_get_contents() is locked down on your PHP installation to disallow + // its use with URLs, then you can use the alternative request method instead. + // This makes use of fsockopen() instead. + // $recaptcha = new \ReCaptcha\ReCaptcha($secret, new \ReCaptcha\RequestMethod\SocketPost()); + + // Make the call to verify the response and also pass the user's IP address + $resp = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME']) + ->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']); + if ($resp->isSuccess()): + // If the response is a success, that's it! + ?> + <h2>Success!</h2> + <kbd><pre><?php var_export($resp);?></pre></kbd> + <p>That's it. Everything is working. Go integrate this into your real project.</p> + <p><a href="/recaptcha-v2-invisible.php">⟳ Try again</a></p> + <?php + else: + // If it's not successful, then one or more error codes will be returned. + ?> + <h2>Something went wrong</h2> + <kbd><pre><?php var_export($resp);?></pre></kbd> + <p>Check the error code reference at <kbd><a href="https://developers.google.com/recaptcha/docs/verify#error-code-reference">https://developers.google.com/recaptcha/docs/verify#error-code-reference</a></kbd>. + <p><strong>Note:</strong> Error code <kbd>missing-input-response</kbd> may mean the user just didn't complete the reCAPTCHA.</p> + <p><a href="/recaptcha-v2-invisible.php">⟳ Try again</a></p> + <?php + endif; +else: + // Add the g-recaptcha tag to the form you want to include the reCAPTCHA element + ?> + <p>Submit the form and reCAPTCHA will run automatically.</p> + <form action="/recaptcha-v2-invisible.php" method="post" id="demo-form"> + <fieldset> + <legend>An example form</legend> + <label class="form-field">Example input A: <input type="text" name="ex-a" value="foo"></label> + <label class="form-field">Example input B: <input type="text" name="ex-b" value="bar"></label> + <button class="g-recaptcha form-field" data-sitekey="<?php echo $siteKey; ?>" data-callback='onSubmit'>Submit ↦</button> + </fieldset> + </form> + <script type="text/javascript" src="https://www.google.com/recaptcha/api.js?hl=<?php echo $lang; ?>" async defer></script> + <script type="text/javascript"> + function onSubmit(token) { + document.getElementById("demo-form").submit(); + } + </script> + <?php +endif;?> +</main> + +<!-- Google Analytics - just ignore this --> +<script async src="https://www.googletagmanager.com/gtag/js?id=UA-123057962-1"></script> +<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-123057962-1');</script> diff --git a/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v3-request-scores.php b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v3-request-scores.php new file mode 100644 index 0000000000000000000000000000000000000000..6d5efeed8e013cb952e167f438a1153d4ff29523 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v3-request-scores.php @@ -0,0 +1,109 @@ +<?php +/** + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +require __DIR__ . '/appengine-https.php'; + +// Initiate the autoloader. The file should be generated by Composer. +// You will provide your own autoloader or require the files directly if you did +// not install via Composer. +require_once __DIR__ . '/../vendor/autoload.php'; + +// Register API keys at https://www.google.com/recaptcha/admin +$siteKey = ''; +$secret = ''; + +// Copy the config.php.dist file to config.php and update it with your keys to run the examples +if ($siteKey == '' && is_readable(__DIR__ . '/config.php')) { + $config = include __DIR__ . '/config.php'; + $siteKey = $config['v3']['site']; + $secret = $config['v3']['secret']; +} + +// reCAPTCHA supports 40+ languages listed here: https://developers.google.com/recaptcha/docs/language +$lang = 'en'; + + +?> +<!DOCTYPE html> +<html lang="en"> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width,height=device-height,minimum-scale=1"> +<link rel="shortcut icon" href="https://www.gstatic.com/recaptcha/admin/favicon.ico" type="image/x-icon"/> +<link rel="canonical" href="https://recaptcha-demo.appspot.com/recaptcha-v2-request-scores.php"> +<script type="application/ld+json">{ "@context": "http://schema.org", "@type": "WebSite", "name": "reCAPTCHA demo - Request scores", "url": "https://recaptcha-demo.appspot.com/recaptcha-v2-request-scores.php" }</script> +<meta name="description" content="reCAPTCHA demo - Request scores" /> +<meta property="og:url" content="https://recaptcha-demo.appspot.com/recaptcha-v2-request-scores.php" /> +<meta property="og:type" content="website" /> +<meta property="og:title" content="reCAPTCHA demo - Request scores" /> +<meta property="og:description" content="reCAPTCHA demo - Request scores" /> +<link rel="stylesheet" type="text/css" href="/examples.css"> +<title>reCAPTCHA demo - Request scores</title> + +<header> + <h1>reCAPTCHA demo</h1><h2>Request scores</h2> + <p><a href="/">↤ Home</a></p> +</header> +<main> +<?php +if ($siteKey === '' || $secret === ''): +?> + <h2>Add your keys</h2> + <p>If you do not have keys already then visit <kbd> <a href = "https://www.google.com/recaptcha/admin">https://www.google.com/recaptcha/admin</a></kbd> to generate them. Edit this file and set the respective keys in <kbd>$siteKey</kbd> and <kbd>$secret</kbd>. Reload the page after this.</p> + <?php +else: + // Add the g-recaptcha tag to the form you want to include the reCAPTCHA element + ?> + <p>reCAPTCHA will provide a score for this request.</p> + <ol id="recaptcha-steps"> + <li class="step0">reCAPTCHA script loading</li> + <li style="display:none" class="step1"><kbd>grecaptcha.ready()</kbd> fired, calling <pre>grecaptcha.execute('<?php echo $siteKey; ?>', {action: 'homepage'})'</pre></li> + <li style="display:none" class="step2">Received token from reCAPTCHA service, sending to our backend with <kbd>fetch('/recaptcha-v3-verify.php?token='+<span class="token">123</span>)</kbd></li> + <li style="display:none" class="step3">Received response from our backend: <pre class="response">response</pre></li> + </ol> + <p><a href="/recaptcha-v3-request-scores.php">⟳ Try again</a></p> + + <script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script> + <script> + const steps = document.getElementById('recaptcha-steps'); + grecaptcha.ready(function() { + document.querySelector('.step1').style.display = 'list-item'; + grecaptcha.execute('<?php echo $siteKey; ?>', {action: 'homepage'}).then(function(token) { + document.querySelector('.token').innerHTML = token; + document.querySelector('.step2').style.display = 'list-item'; + + fetch('/recaptcha-v3-verify.php?token='+token).then(function(response) { + response.json().then(function(data) { + document.querySelector('.response').innerHTML = JSON.stringify(data, null, 2); + document.querySelector('.step3').style.display = 'list-item'; + }); + }); + }); + }); +</script> + <?php +endif;?> +</main> + +<!-- Google Analytics - just ignore this --> +<script async src="https://www.googletagmanager.com/gtag/js?id=UA-123057962-1"></script> +<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-123057962-1');</script> diff --git a/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v3-verify.php b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v3-verify.php new file mode 100644 index 0000000000000000000000000000000000000000..31c574c86f4cc43e164313fd444413bdfbc4f017 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/recaptcha-v3-verify.php @@ -0,0 +1,49 @@ +<?php +/** + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +require __DIR__ . '/appengine-https.php'; + +// Initiate the autoloader. The file should be generated by Composer. +// You will provide your own autoloader or require the files directly if you did +// not install via Composer. +require_once __DIR__ . '/../vendor/autoload.php'; + +// Register API keys at https://www.google.com/recaptcha/admin +$siteKey = ''; +$secret = ''; + +// Copy the config.php.dist file to config.php and update it with your keys to run the examples +if ($siteKey == '' && is_readable(__DIR__ . '/config.php')) { + $config = include __DIR__ . '/config.php'; + $siteKey = $config['v3']['site']; + $secret = $config['v3']['secret']; +} + +// Effectively we're providing an API endpoint here that will accept the token, verify it, and return the action / score to the page +$recaptcha = new \ReCaptcha\ReCaptcha($secret); +$resp = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME']) + ->setExpectedAction('homepage') + ->setScoreThreshold(0.5) + ->verify($_GET['token'], $_SERVER['REMOTE_ADDR']); +header('Content-type:application/json'); +echo json_encode($resp->toArray()); diff --git a/web/modules/recaptcha/recaptcha-php/examples/robots.txt b/web/modules/recaptcha/recaptcha-php/examples/robots.txt new file mode 100644 index 0000000000000000000000000000000000000000..eb0536286f3081c6c0646817037faf5446e3547d --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/examples/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/web/modules/recaptcha/recaptcha-php/phpunit.xml.dist b/web/modules/recaptcha/recaptcha-php/phpunit.xml.dist new file mode 100644 index 0000000000000000000000000000000000000000..ae8661044bc57b6ecc77d3226531bb59fcd34463 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/phpunit.xml.dist @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.4/phpunit.xsd" + colors="true" + verbose="true" + bootstrap="src/autoload.php"> + <testsuites> + <testsuite name="reCAPTCHA Test Suite"> + <directory>tests/ReCaptcha/</directory> + </testsuite> + </testsuites> + <filter> + <whitelist> + <directory suffix=".php">src/ReCaptcha/</directory> + </whitelist> + </filter> + <logging> + <log type="coverage-clover" target="build/logs/clover.xml"/> + </logging> +</phpunit> diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/ReCaptcha.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/ReCaptcha.php new file mode 100644 index 0000000000000000000000000000000000000000..8939e84b0303a62ed402a1ec39eda1b6a3a63a49 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/ReCaptcha.php @@ -0,0 +1,261 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha; + +/** + * reCAPTCHA client. + */ +class ReCaptcha +{ + /** + * Version of this client library. + * @const string + */ + const VERSION = 'php_1.2.1'; + + /** + * URL for reCAPTCHA sitevrerify API + * @const string + */ + const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'; + + /** + * Invalid JSON received + * @const string + */ + const E_INVALID_JSON = 'invalid-json'; + + /** + * Could not connect to service + * @const string + */ + const E_CONNECTION_FAILED = 'connection-failed'; + + /** + * Did not receive a 200 from the service + * @const string + */ + const E_BAD_RESPONSE = 'bad-response'; + + /** + * Not a success, but no error codes received! + * @const string + */ + const E_UNKNOWN_ERROR = 'unknown-error'; + + /** + * ReCAPTCHA response not provided + * @const string + */ + const E_MISSING_INPUT_RESPONSE = 'missing-input-response'; + + /** + * Expected hostname did not match + * @const string + */ + const E_HOSTNAME_MISMATCH = 'hostname-mismatch'; + + /** + * Expected APK package name did not match + * @const string + */ + const E_APK_PACKAGE_NAME_MISMATCH = 'apk_package_name-mismatch'; + + /** + * Expected action did not match + * @const string + */ + const E_ACTION_MISMATCH = 'action-mismatch'; + + /** + * Score threshold not met + * @const string + */ + const E_SCORE_THRESHOLD_NOT_MET = 'score-threshold-not-met'; + + /** + * Challenge timeout + * @const string + */ + const E_CHALLENGE_TIMEOUT = 'challenge-timeout'; + + /** + * Shared secret for the site. + * @var string + */ + private $secret; + + /** + * Method used to communicate with service. Defaults to POST request. + * @var RequestMethod + */ + private $requestMethod; + + /** + * Create a configured instance to use the reCAPTCHA service. + * + * @param string $secret The shared key between your site and reCAPTCHA. + * @param RequestMethod $requestMethod method used to send the request. Defaults to POST. + * @throws \RuntimeException if $secret is invalid + */ + public function __construct($secret, RequestMethod $requestMethod = null) + { + if (empty($secret)) { + throw new \RuntimeException('No secret provided'); + } + + if (!is_string($secret)) { + throw new \RuntimeException('The provided secret must be a string'); + } + + $this->secret = $secret; + $this->requestMethod = (is_null($requestMethod)) ? new RequestMethod\Post() : $requestMethod; + } + + /** + * Calls the reCAPTCHA siteverify API to verify whether the user passes + * CAPTCHA test and additionally runs any specified additional checks + * + * @param string $response The user response token provided by reCAPTCHA, verifying the user on your site. + * @param string $remoteIp The end user's IP address. + * @return Response Response from the service. + */ + public function verify($response, $remoteIp = null) + { + // Discard empty solution submissions + if (empty($response)) { + $recaptchaResponse = new Response(false, array(self::E_MISSING_INPUT_RESPONSE)); + return $recaptchaResponse; + } + + $params = new RequestParameters($this->secret, $response, $remoteIp, self::VERSION); + $rawResponse = $this->requestMethod->submit($params); + $initialResponse = Response::fromJson($rawResponse); + $validationErrors = array(); + + if (isset($this->hostname) && strcasecmp($this->hostname, $initialResponse->getHostname()) !== 0) { + $validationErrors[] = self::E_HOSTNAME_MISMATCH; + } + + if (isset($this->apkPackageName) && strcasecmp($this->apkPackageName, $initialResponse->getApkPackageName()) !== 0) { + $validationErrors[] = self::E_APK_PACKAGE_NAME_MISMATCH; + } + + if (isset($this->action) && strcasecmp($this->action, $initialResponse->getAction()) !== 0) { + $validationErrors[] = self::E_ACTION_MISMATCH; + } + + if (isset($this->threshold) && $this->threshold > $initialResponse->getScore()) { + $validationErrors[] = self::E_SCORE_THRESHOLD_NOT_MET; + } + + if (isset($this->timeoutSeconds)) { + $challengeTs = strtotime($initialResponse->getChallengeTs()); + + if ($challengeTs > 0 && time() - $challengeTs > $this->timeoutSeconds) { + $validationErrors[] = self::E_CHALLENGE_TIMEOUT; + } + } + + if (empty($validationErrors)) { + return $initialResponse; + } + + return new Response( + false, + array_merge($initialResponse->getErrorCodes(), $validationErrors), + $initialResponse->getHostname(), + $initialResponse->getChallengeTs(), + $initialResponse->getApkPackageName(), + $initialResponse->getScore(), + $initialResponse->getAction() + ); + } + + /** + * Provide a hostname to match against in verify() + * This should be without a protocol or trailing slash, e.g. www.google.com + * + * @param string $hostname Expected hostname + * @return ReCaptcha Current instance for fluent interface + */ + public function setExpectedHostname($hostname) + { + $this->hostname = $hostname; + return $this; + } + + /** + * Provide an APK package name to match against in verify() + * + * @param string $apkPackageName Expected APK package name + * @return ReCaptcha Current instance for fluent interface + */ + public function setExpectedApkPackageName($apkPackageName) + { + $this->apkPackageName = $apkPackageName; + return $this; + } + + /** + * Provide an action to match against in verify() + * This should be set per page. + * + * @param string $action Expected action + * @return ReCaptcha Current instance for fluent interface + */ + public function setExpectedAction($action) + { + $this->action = $action; + return $this; + } + + /** + * Provide a threshold to meet or exceed in verify() + * Threshold should be a float between 0 and 1 which will be tested as response >= threshold. + * + * @param float $threshold Expected threshold + * @return ReCaptcha Current instance for fluent interface + */ + public function setScoreThreshold($threshold) + { + $this->threshold = floatval($threshold); + return $this; + } + + /** + * Provide a timeout in seconds to test against the challenge timestamp in verify() + * + * @param int $timeoutSeconds Expected hostname + * @return ReCaptcha Current instance for fluent interface + */ + public function setChallengeTimeout($timeoutSeconds) + { + $this->timeoutSeconds = $timeoutSeconds; + return $this; + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..2fd94b3b142f9bf6f9f3752ee195d8a4badce5f3 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod.php @@ -0,0 +1,42 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha; + +/** + * Method used to send the request to the service. + */ +interface RequestMethod +{ + + /** + * Submit the request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * @return string Body of the reCAPTCHA response + */ + public function submit(RequestParameters $params); +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Curl.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Curl.php new file mode 100644 index 0000000000000000000000000000000000000000..3d8dddd1608a47997f28a4de37d2124fd8055a15 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Curl.php @@ -0,0 +1,74 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +/** + * Convenience wrapper around the cURL functions to allow mocking. + */ +class Curl +{ + + /** + * @see http://php.net/curl_init + * @param string $url + * @return resource cURL handle + */ + public function init($url = null) + { + return curl_init($url); + } + + /** + * @see http://php.net/curl_setopt_array + * @param resource $ch + * @param array $options + * @return bool + */ + public function setoptArray($ch, array $options) + { + return curl_setopt_array($ch, $options); + } + + /** + * @see http://php.net/curl_exec + * @param resource $ch + * @return mixed + */ + public function exec($ch) + { + return curl_exec($ch); + } + + /** + * @see http://php.net/curl_close + * @param resource $ch + */ + public function close($ch) + { + curl_close($ch); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/CurlPost.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/CurlPost.php new file mode 100644 index 0000000000000000000000000000000000000000..59886f8ab419c79fd4795208ec551f425b34afe5 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/CurlPost.php @@ -0,0 +1,96 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +use ReCaptcha\ReCaptcha; +use ReCaptcha\RequestMethod; +use ReCaptcha\RequestParameters; + +/** + * Sends cURL request to the reCAPTCHA service. + * Note: this requires the cURL extension to be enabled in PHP + * @see http://php.net/manual/en/book.curl.php + */ +class CurlPost implements RequestMethod +{ + /** + * Curl connection to the reCAPTCHA service + * @var Curl + */ + private $curl; + + /** + * URL for reCAPTCHA sitevrerify API + * @var string + */ + private $siteVerifyUrl; + + /** + * Only needed if you want to override the defaults + * + * @param Curl $curl Curl resource + * @param string $siteVerifyUrl URL for reCAPTCHA sitevrerify API + */ + public function __construct(Curl $curl = null, $siteVerifyUrl = null) + { + $this->curl = (is_null($curl)) ? new Curl() : $curl; + $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl; + } + + /** + * Submit the cURL request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * @return string Body of the reCAPTCHA response + */ + public function submit(RequestParameters $params) + { + $handle = $this->curl->init($this->siteVerifyUrl); + + $options = array( + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $params->toQueryString(), + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/x-www-form-urlencoded' + ), + CURLINFO_HEADER_OUT => false, + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true + ); + $this->curl->setoptArray($handle, $options); + + $response = $this->curl->exec($handle); + $this->curl->close($handle); + + if ($response !== false) { + return $response; + } + + return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}'; + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Post.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Post.php new file mode 100644 index 0000000000000000000000000000000000000000..9e2658217726d7a35bfd2d14813f6dd8e1e41a44 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Post.php @@ -0,0 +1,80 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +use ReCaptcha\ReCaptcha; +use ReCaptcha\RequestMethod; +use ReCaptcha\RequestParameters; + +/** + * Sends POST requests to the reCAPTCHA service. + */ +class Post implements RequestMethod +{ + /** + * URL for reCAPTCHA sitevrerify API + * @var string + */ + private $siteVerifyUrl; + + /** + * Only needed if you want to override the defaults + * + * @param string $siteVerifyUrl URL for reCAPTCHA sitevrerify API + */ + public function __construct($siteVerifyUrl = null) + { + $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl; + } + + /** + * Submit the POST request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * @return string Body of the reCAPTCHA response + */ + public function submit(RequestParameters $params) + { + $options = array( + 'http' => array( + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => $params->toQueryString(), + // Force the peer to validate (not needed in 5.6.0+, but still works) + 'verify_peer' => true, + ), + ); + $context = stream_context_create($options); + $response = file_get_contents($this->siteVerifyUrl, false, $context); + + if ($response !== false) { + return $response; + } + + return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}'; + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Socket.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Socket.php new file mode 100644 index 0000000000000000000000000000000000000000..12322e8c0ad081b662dbcb66fd87c6303daeefa7 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/Socket.php @@ -0,0 +1,104 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +/** + * Convenience wrapper around native socket and file functions to allow for + * mocking. + */ +class Socket +{ + private $handle = null; + + /** + * fsockopen + * + * @see http://php.net/fsockopen + * @param string $hostname + * @param int $port + * @param int $errno + * @param string $errstr + * @param float $timeout + * @return resource + */ + public function fsockopen($hostname, $port = -1, &$errno = 0, &$errstr = '', $timeout = null) + { + $this->handle = fsockopen($hostname, $port, $errno, $errstr, (is_null($timeout) ? ini_get("default_socket_timeout") : $timeout)); + + if ($this->handle != false && $errno === 0 && $errstr === '') { + return $this->handle; + } + return false; + } + + /** + * fwrite + * + * @see http://php.net/fwrite + * @param string $string + * @param int $length + * @return int | bool + */ + public function fwrite($string, $length = null) + { + return fwrite($this->handle, $string, (is_null($length) ? strlen($string) : $length)); + } + + /** + * fgets + * + * @see http://php.net/fgets + * @param int $length + * @return string + */ + public function fgets($length = null) + { + return fgets($this->handle, $length); + } + + /** + * feof + * + * @see http://php.net/feof + * @return bool + */ + public function feof() + { + return feof($this->handle); + } + + /** + * fclose + * + * @see http://php.net/fclose + * @return bool + */ + public function fclose() + { + return fclose($this->handle); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/SocketPost.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/SocketPost.php new file mode 100644 index 0000000000000000000000000000000000000000..ca1ca907b295fd2e9b9f657d3027a9f793b1522f --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestMethod/SocketPost.php @@ -0,0 +1,100 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +use ReCaptcha\ReCaptcha; +use ReCaptcha\RequestMethod; +use ReCaptcha\RequestParameters; + +/** + * Sends a POST request to the reCAPTCHA service, but makes use of fsockopen() + * instead of get_file_contents(). This is to account for people who may be on + * servers where allow_url_open is disabled. + */ +class SocketPost implements RequestMethod +{ + /** + * Socket to the reCAPTCHA service + * @var Socket + */ + private $socket; + + /** + * Only needed if you want to override the defaults + * + * @param \ReCaptcha\RequestMethod\Socket $socket optional socket, injectable for testing + * @param string $siteVerifyUrl URL for reCAPTCHA sitevrerify API + */ + public function __construct(Socket $socket = null, $siteVerifyUrl = null) + { + $this->socket = (is_null($socket)) ? new Socket() : $socket; + $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl; + } + + /** + * Submit the POST request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * @return string Body of the reCAPTCHA response + */ + public function submit(RequestParameters $params) + { + $errno = 0; + $errstr = ''; + $urlParsed = parse_url($this->siteVerifyUrl); + + if (false === $this->socket->fsockopen('ssl://' . $urlParsed['host'], 443, $errno, $errstr, 30)) { + return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}'; + } + + $content = $params->toQueryString(); + + $request = "POST " . $urlParsed['path'] . " HTTP/1.1\r\n"; + $request .= "Host: " . $urlParsed['host'] . "\r\n"; + $request .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $request .= "Content-length: " . strlen($content) . "\r\n"; + $request .= "Connection: close\r\n\r\n"; + $request .= $content . "\r\n\r\n"; + + $this->socket->fwrite($request); + $response = ''; + + while (!$this->socket->feof()) { + $response .= $this->socket->fgets(4096); + } + + $this->socket->fclose(); + + if (0 !== strpos($response, 'HTTP/1.1 200 OK')) { + return '{"success": false, "error-codes": ["'.ReCaptcha::E_BAD_RESPONSE.'"]}'; + } + + $parts = preg_split("#\n\s*\n#Uis", $response); + + return $parts[1]; + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestParameters.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestParameters.php new file mode 100644 index 0000000000000000000000000000000000000000..b6dd998d8eb5cd3b4275dbc71f3817f0de17dec8 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/RequestParameters.php @@ -0,0 +1,103 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha; + +/** + * Stores and formats the parameters for the request to the reCAPTCHA service. + */ +class RequestParameters +{ + /** + * The shared key between your site and reCAPTCHA. + * @var string + */ + private $secret; + + /** + * The user response token provided by reCAPTCHA, verifying the user on your site. + * @var string + */ + private $response; + + /** + * Remote user's IP address. + * @var string + */ + private $remoteIp; + + /** + * Client version. + * @var string + */ + private $version; + + /** + * Initialise parameters. + * + * @param string $secret Site secret. + * @param string $response Value from g-captcha-response form field. + * @param string $remoteIp User's IP address. + * @param string $version Version of this client library. + */ + public function __construct($secret, $response, $remoteIp = null, $version = null) + { + $this->secret = $secret; + $this->response = $response; + $this->remoteIp = $remoteIp; + $this->version = $version; + } + + /** + * Array representation. + * + * @return array Array formatted parameters. + */ + public function toArray() + { + $params = array('secret' => $this->secret, 'response' => $this->response); + + if (!is_null($this->remoteIp)) { + $params['remoteip'] = $this->remoteIp; + } + + if (!is_null($this->version)) { + $params['version'] = $this->version; + } + + return $params; + } + + /** + * Query string representation for HTTP request. + * + * @return string Query string formatted parameters. + */ + public function toQueryString() + { + return http_build_query($this->toArray(), '', '&'); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/Response.php b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/Response.php new file mode 100644 index 0000000000000000000000000000000000000000..5c15c372cbf20f7c894afa33f2e5789e55a9343f --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/ReCaptcha/Response.php @@ -0,0 +1,210 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha; + +/** + * The response returned from the service. + */ +class Response +{ + /** + * Success or failure. + * @var boolean + */ + private $success = false; + + /** + * Error code strings. + * @var array + */ + private $errorCodes = array(); + + /** + * The hostname of the site where the reCAPTCHA was solved. + * @var string + */ + private $hostname; + + /** + * Timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) + * @var string + */ + private $challengeTs; + + /** + * APK package name + * @var string + */ + private $apkPackageName; + + /** + * Score assigned to the request + * @var float + */ + private $score; + + /** + * Action as specified by the page + * @var string + */ + private $action; + + /** + * Build the response from the expected JSON returned by the service. + * + * @param string $json + * @return \ReCaptcha\Response + */ + public static function fromJson($json) + { + $responseData = json_decode($json, true); + + if (!$responseData) { + return new Response(false, array(ReCaptcha::E_INVALID_JSON)); + } + + $hostname = isset($responseData['hostname']) ? $responseData['hostname'] : null; + $challengeTs = isset($responseData['challenge_ts']) ? $responseData['challenge_ts'] : null; + $apkPackageName = isset($responseData['apk_package_name']) ? $responseData['apk_package_name'] : null; + $score = isset($responseData['score']) ? floatval($responseData['score']) : null; + $action = isset($responseData['action']) ? $responseData['action'] : null; + + if (isset($responseData['success']) && $responseData['success'] == true) { + return new Response(true, array(), $hostname, $challengeTs, $apkPackageName, $score, $action); + } + + if (isset($responseData['error-codes']) && is_array($responseData['error-codes'])) { + return new Response(false, $responseData['error-codes'], $hostname, $challengeTs, $apkPackageName, $score, $action); + } + + return new Response(false, array(ReCaptcha::E_UNKNOWN_ERROR), $hostname, $challengeTs, $apkPackageName, $score, $action); + } + + /** + * Constructor. + * + * @param boolean $success + * @param string $hostname + * @param string $challengeTs + * @param string $apkPackageName + * @param float $score + * @param strong $action + * @param array $errorCodes + */ + public function __construct($success, array $errorCodes = array(), $hostname = null, $challengeTs = null, $apkPackageName = null, $score = null, $action = null) + { + $this->success = $success; + $this->hostname = $hostname; + $this->challengeTs = $challengeTs; + $this->apkPackageName = $apkPackageName; + $this->score = $score; + $this->action = $action; + $this->errorCodes = $errorCodes; + } + + /** + * Is success? + * + * @return boolean + */ + public function isSuccess() + { + return $this->success; + } + + /** + * Get error codes. + * + * @return array + */ + public function getErrorCodes() + { + return $this->errorCodes; + } + + /** + * Get hostname. + * + * @return string + */ + public function getHostname() + { + return $this->hostname; + } + + /** + * Get challenge timestamp + * + * @return string + */ + public function getChallengeTs() + { + return $this->challengeTs; + } + + /** + * Get APK package name + * + * @return string + */ + public function getApkPackageName() + { + return $this->apkPackageName; + } + /** + * Get score + * + * @return float + */ + public function getScore() + { + return $this->score; + } + + /** + * Get action + * + * @return string + */ + public function getAction() + { + return $this->action; + } + + public function toArray() + { + return array( + 'success' => $this->isSuccess(), + 'hostname' => $this->getHostname(), + 'challenge_ts' => $this->getChallengeTs(), + 'apk_package_name' => $this->getApkPackageName(), + 'score' => $this->getScore(), + 'action' => $this->getAction(), + 'error-codes' => $this->getErrorCodes(), + ); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/src/autoload.php b/web/modules/recaptcha/recaptcha-php/src/autoload.php new file mode 100644 index 0000000000000000000000000000000000000000..95e249e95991852d105651a5280bcf402d17850a --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/src/autoload.php @@ -0,0 +1,40 @@ +<?php + +/* An autoloader for ReCaptcha\Foo classes. This should be required() + * by the user before attempting to instantiate any of the ReCaptcha + * classes. + */ + +spl_autoload_register(function ($class) { + if (substr($class, 0, 10) !== 'ReCaptcha\\') { + /* If the class does not lie under the "ReCaptcha" namespace, + * then we can exit immediately. + */ + return; + } + + /* All of the classes have names like "ReCaptcha\Foo", so we need + * to replace the backslashes with frontslashes if we want the + * name to map directly to a location in the filesystem. + */ + $class = str_replace('\\', '/', $class); + + /* First, check under the current directory. It is important that + * we look here first, so that we don't waste time searching for + * test classes in the common case. + */ + $path = dirname(__FILE__).'/'.$class.'.php'; + if (is_readable($path)) { + require_once $path; + + return; + } + + /* If we didn't find what we're looking for already, maybe it's + * a test class? + */ + $path = dirname(__FILE__).'/../tests/'.$class.'.php'; + if (is_readable($path)) { + require_once $path; + } +}); diff --git a/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/ReCaptchaTest.php b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/ReCaptchaTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d3adb542cf3ccd2315046d297e6750e0a72a743d --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/ReCaptchaTest.php @@ -0,0 +1,190 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha; + +use PHPUnit\Framework\TestCase; + +class ReCaptchaTest extends TestCase +{ + + /** + * @expectedException \RuntimeException + * @dataProvider invalidSecretProvider + */ + public function testExceptionThrownOnInvalidSecret($invalid) + { + $rc = new ReCaptcha($invalid); + } + + public function invalidSecretProvider() + { + return array( + array(''), + array(null), + array(0), + array(new \stdClass()), + array(array()), + ); + } + + public function testVerifyReturnsErrorOnMissingResponse() + { + $rc = new ReCaptcha('secret'); + $response = $rc->verify(''); + $this->assertFalse($response->isSuccess()); + $this->assertEquals(array(Recaptcha::E_MISSING_INPUT_RESPONSE), $response->getErrorCodes()); + } + + private function getMockRequestMethod($responseJson) + { + $method = $this->getMockBuilder(\ReCaptcha\RequestMethod::class) + ->disableOriginalConstructor() + ->setMethods(array('submit')) + ->getMock(); + $method->expects($this->any()) + ->method('submit') + ->with($this->callback(function ($params) { + return true; + })) + ->will($this->returnValue($responseJson)); + return $method; + } + + public function testVerifyReturnsResponse() + { + $method = $this->getMockRequestMethod('{"success": true}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->verify('response'); + $this->assertTrue($response->isSuccess()); + } + + public function testVerifyReturnsInitialResponseWithoutAdditionalChecks() + { + $method = $this->getMockRequestMethod('{"success": true}'); + $rc = new ReCaptcha('secret', $method); + $initialResponse = $rc->verify('response'); + $this->assertEquals($initialResponse, $rc->verify('response')); + } + + public function testVerifyHostnameMatch() + { + $method = $this->getMockRequestMethod('{"success": true, "hostname": "host.name"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setExpectedHostname('host.name')->verify('response'); + $this->assertTrue($response->isSuccess()); + } + + public function testVerifyHostnameMisMatch() + { + $method = $this->getMockRequestMethod('{"success": true, "hostname": "host.NOTname"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setExpectedHostname('host.name')->verify('response'); + $this->assertFalse($response->isSuccess()); + $this->assertEquals(array(ReCaptcha::E_HOSTNAME_MISMATCH), $response->getErrorCodes()); + } + + public function testVerifyApkPackageNameMatch() + { + $method = $this->getMockRequestMethod('{"success": true, "apk_package_name": "apk.name"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setExpectedApkPackageName('apk.name')->verify('response'); + $this->assertTrue($response->isSuccess()); + } + + public function testVerifyApkPackageNameMisMatch() + { + $method = $this->getMockRequestMethod('{"success": true, "apk_package_name": "apk.NOTname"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setExpectedApkPackageName('apk.name')->verify('response'); + $this->assertFalse($response->isSuccess()); + $this->assertEquals(array(ReCaptcha::E_APK_PACKAGE_NAME_MISMATCH), $response->getErrorCodes()); + } + + public function testVerifyActionMatch() + { + $method = $this->getMockRequestMethod('{"success": true, "action": "action/name"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setExpectedAction('action/name')->verify('response'); + $this->assertTrue($response->isSuccess()); + } + + public function testVerifyActionMisMatch() + { + $method = $this->getMockRequestMethod('{"success": true, "action": "action/NOTname"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setExpectedAction('action/name')->verify('response'); + $this->assertFalse($response->isSuccess()); + $this->assertEquals(array(ReCaptcha::E_ACTION_MISMATCH), $response->getErrorCodes()); + } + + public function testVerifyAboveThreshold() + { + $method = $this->getMockRequestMethod('{"success": true, "score": "0.9"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setScoreThreshold('0.5')->verify('response'); + $this->assertTrue($response->isSuccess()); + } + + public function testVerifyBelowThreshold() + { + $method = $this->getMockRequestMethod('{"success": true, "score": "0.1"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setScoreThreshold('0.5')->verify('response'); + $this->assertFalse($response->isSuccess()); + $this->assertEquals(array(ReCaptcha::E_SCORE_THRESHOLD_NOT_MET), $response->getErrorCodes()); + } + + public function testVerifyWithinTimeout() + { + // Responses come back like 2018-07-31T13:48:41Z + $challengeTs = date('Y-M-d\TH:i:s\Z', time()); + $method = $this->getMockRequestMethod('{"success": true, "challenge_ts": "'.$challengeTs.'"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setChallengeTimeout('1000')->verify('response'); + $this->assertTrue($response->isSuccess()); + } + + public function testVerifyOverTimeout() + { + // Responses come back like 2018-07-31T13:48:41Z + $challengeTs = date('Y-M-d\TH:i:s\Z', time() - 600); + $method = $this->getMockRequestMethod('{"success": true, "challenge_ts": "'.$challengeTs.'"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setChallengeTimeout('60')->verify('response'); + $this->assertFalse($response->isSuccess()); + $this->assertEquals(array(ReCaptcha::E_CHALLENGE_TIMEOUT), $response->getErrorCodes()); + } + + public function testVerifyMergesErrors() + { + $method = $this->getMockRequestMethod('{"success": false, "error-codes": ["initial-error"], "score": "0.1"}'); + $rc = new ReCaptcha('secret', $method); + $response = $rc->setScoreThreshold('0.5')->verify('response'); + $this->assertFalse($response->isSuccess()); + $this->assertEquals(array('initial-error', ReCaptcha::E_SCORE_THRESHOLD_NOT_MET), $response->getErrorCodes()); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/CurlPostTest.php b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/CurlPostTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a0f4e971f8f50042a10dfe41d8e3211bae73c5c1 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/CurlPostTest.php @@ -0,0 +1,115 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +use \ReCaptcha\ReCaptcha; +use \ReCaptcha\RequestParameters; +use PHPUnit\Framework\TestCase; + +class CurlPostTest extends TestCase +{ + protected function setUp() + { + if (!extension_loaded('curl')) { + $this->markTestSkipped( + 'The cURL extension is not available.' + ); + } + } + + public function testSubmit() + { + $curl = $this->getMockBuilder(\ReCaptcha\RequestMethod\Curl::class) + ->disableOriginalConstructor() + ->setMethods(array('init', 'setoptArray', 'exec', 'close')) + ->getMock(); + $curl->expects($this->once()) + ->method('init') + ->willReturn(new \stdClass); + $curl->expects($this->once()) + ->method('setoptArray') + ->willReturn(true); + $curl->expects($this->once()) + ->method('exec') + ->willReturn('RESPONSEBODY'); + $curl->expects($this->once()) + ->method('close'); + + $pc = new CurlPost($curl); + $response = $pc->submit(new RequestParameters("secret", "response")); + $this->assertEquals('RESPONSEBODY', $response); + } + + public function testOverrideSiteVerifyUrl() + { + $url = 'OVERRIDE'; + + $curl = $this->getMockBuilder(\ReCaptcha\RequestMethod\Curl::class) + ->disableOriginalConstructor() + ->setMethods(array('init', 'setoptArray', 'exec', 'close')) + ->getMock(); + $curl->expects($this->once()) + ->method('init') + ->with($url) + ->willReturn(new \stdClass); + $curl->expects($this->once()) + ->method('setoptArray') + ->willReturn(true); + $curl->expects($this->once()) + ->method('exec') + ->willReturn('RESPONSEBODY'); + $curl->expects($this->once()) + ->method('close'); + + $pc = new CurlPost($curl, $url); + $response = $pc->submit(new RequestParameters("secret", "response")); + $this->assertEquals('RESPONSEBODY', $response); + } + + public function testConnectionFailureReturnsError() + { + $curl = $this->getMockBuilder(\ReCaptcha\RequestMethod\Curl::class) + ->disableOriginalConstructor() + ->setMethods(array('init', 'setoptArray', 'exec', 'close')) + ->getMock(); + $curl->expects($this->once()) + ->method('init') + ->willReturn(new \stdClass); + $curl->expects($this->once()) + ->method('setoptArray') + ->willReturn(true); + $curl->expects($this->once()) + ->method('exec') + ->willReturn(false); + $curl->expects($this->once()) + ->method('close'); + + $pc = new CurlPost($curl); + $response = $pc->submit(new RequestParameters("secret", "response")); + $this->assertEquals('{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}', $response); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/PostTest.php b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/PostTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1bf7437fb3c47bb77445e70434e88d36c21e6289 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/PostTest.php @@ -0,0 +1,141 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +use \ReCaptcha\ReCaptcha; +use ReCaptcha\RequestParameters; +use PHPUnit\Framework\TestCase; + +class PostTest extends TestCase +{ + public static $assert = null; + protected $parameters = null; + protected $runcount = 0; + + public function setUp() + { + $this->parameters = new RequestParameters('secret', 'response', 'remoteip', 'version'); + } + + public function tearDown() + { + self::$assert = null; + } + + public function testHTTPContextOptions() + { + $req = new Post(); + self::$assert = array($this, 'httpContextOptionsCallback'); + $req->submit($this->parameters); + $this->assertEquals(1, $this->runcount, 'The assertion was ran'); + } + + public function testSSLContextOptions() + { + $req = new Post(); + self::$assert = array($this, 'sslContextOptionsCallback'); + $req->submit($this->parameters); + $this->assertEquals(1, $this->runcount, 'The assertion was ran'); + } + + public function testOverrideVerifyUrl() + { + $req = new Post('https://over.ride/some/path'); + self::$assert = array($this, 'overrideUrlOptions'); + $req->submit($this->parameters); + $this->assertEquals(1, $this->runcount, 'The assertion was ran'); + } + + public function testConnectionFailureReturnsError() + { + $req = new Post('https://bad.connection/'); + self::$assert = array($this, 'connectionFailureResponse'); + $response = $req->submit($this->parameters); + $this->assertEquals('{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}', $response); + } + + public function connectionFailureResponse() + { + return false; + } + public function overrideUrlOptions(array $args) + { + $this->runcount++; + $this->assertEquals('https://over.ride/some/path', $args[0]); + } + + public function httpContextOptionsCallback(array $args) + { + $this->runcount++; + $this->assertCommonOptions($args); + + $options = stream_context_get_options($args[2]); + $this->assertArrayHasKey('http', $options); + + $this->assertArrayHasKey('method', $options['http']); + $this->assertEquals('POST', $options['http']['method']); + + $this->assertArrayHasKey('content', $options['http']); + $this->assertEquals($this->parameters->toQueryString(), $options['http']['content']); + + $this->assertArrayHasKey('header', $options['http']); + $headers = array( + 'Content-type: application/x-www-form-urlencoded', + ); + foreach ($headers as $header) { + $this->assertContains($header, $options['http']['header']); + } + } + + public function sslContextOptionsCallback(array $args) + { + $this->runcount++; + $this->assertCommonOptions($args); + + $options = stream_context_get_options($args[2]); + $this->assertArrayHasKey('http', $options); + $this->assertArrayHasKey('verify_peer', $options['http']); + $this->assertTrue($options['http']['verify_peer']); + } + + protected function assertCommonOptions(array $args) + { + $this->assertCount(3, $args); + $this->assertStringStartsWith('https://www.google.com/', $args[0]); + $this->assertFalse($args[1]); + $this->assertTrue(is_resource($args[2]), 'The context options should be a resource'); + } +} + +function file_get_contents() +{ + if (PostTest::$assert) { + return call_user_func(PostTest::$assert, func_get_args()); + } + // Since we can't represent maxlen in userland... + return call_user_func_array('file_get_contents', func_get_args()); +} diff --git a/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/SocketPostTest.php b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/SocketPostTest.php new file mode 100644 index 0000000000000000000000000000000000000000..59970e709bf96381812c2c694f9d5ffc5c6a1e88 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestMethod/SocketPostTest.php @@ -0,0 +1,128 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha\RequestMethod; + +use ReCaptcha\ReCaptcha; +use ReCaptcha\RequestParameters; +use PHPUnit\Framework\TestCase; + +class SocketPostTest extends TestCase +{ + public function testSubmitSuccess() + { + $socket = $this->getMockBuilder(\ReCaptcha\RequestMethod\Socket::class) + ->disableOriginalConstructor() + ->setMethods(array('fsockopen', 'fwrite', 'fgets', 'feof', 'fclose')) + ->getMock(); + $socket->expects($this->once()) + ->method('fsockopen') + ->willReturn(true); + $socket->expects($this->once()) + ->method('fwrite'); + $socket->expects($this->once()) + ->method('fgets') + ->willReturn("HTTP/1.1 200 OK\n\nRESPONSEBODY"); + $socket->expects($this->exactly(2)) + ->method('feof') + ->will($this->onConsecutiveCalls(false, true)); + $socket->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $ps = new SocketPost($socket); + $response = $ps->submit(new RequestParameters("secret", "response", "remoteip", "version")); + $this->assertEquals('RESPONSEBODY', $response); + } + + public function testOverrideSiteVerifyUrl() + { + $socket = $this->getMockBuilder(\ReCaptcha\RequestMethod\Socket::class) + ->disableOriginalConstructor() + ->setMethods(array('fsockopen', 'fwrite', 'fgets', 'feof', 'fclose')) + ->getMock(); + $socket->expects($this->once()) + ->method('fsockopen') + ->with('ssl://over.ride', 443, 0, '', 30) + ->willReturn(true); + $socket->expects($this->once()) + ->method('fwrite') + ->with($this->matchesRegularExpression('/^POST \/some\/path.*Host: over\.ride/s')); + $socket->expects($this->once()) + ->method('fgets') + ->willReturn("HTTP/1.1 200 OK\n\nRESPONSEBODY"); + $socket->expects($this->exactly(2)) + ->method('feof') + ->will($this->onConsecutiveCalls(false, true)); + $socket->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $ps = new SocketPost($socket, 'https://over.ride/some/path'); + $response = $ps->submit(new RequestParameters("secret", "response", "remoteip", "version")); + $this->assertEquals('RESPONSEBODY', $response); + } + + public function testSubmitBadResponse() + { + $socket = $this->getMockBuilder(\ReCaptcha\RequestMethod\Socket::class) + ->disableOriginalConstructor() + ->setMethods(array('fsockopen', 'fwrite', 'fgets', 'feof', 'fclose')) + ->getMock(); + $socket->expects($this->once()) + ->method('fsockopen') + ->willReturn(true); + $socket->expects($this->once()) + ->method('fwrite'); + $socket->expects($this->once()) + ->method('fgets') + ->willReturn("HTTP/1.1 500 NOPEn\\nBOBBINS"); + $socket->expects($this->exactly(2)) + ->method('feof') + ->will($this->onConsecutiveCalls(false, true)); + $socket->expects($this->once()) + ->method('fclose') + ->willReturn(true); + + $ps = new SocketPost($socket); + $response = $ps->submit(new RequestParameters("secret", "response", "remoteip", "version")); + $this->assertEquals('{"success": false, "error-codes": ["'.ReCaptcha::E_BAD_RESPONSE.'"]}', $response); + } + + public function testConnectionFailureReturnsError() + { + $socket = $this->getMockBuilder(\ReCaptcha\RequestMethod\Socket::class) + ->disableOriginalConstructor() + ->setMethods(array('fsockopen')) + ->getMock(); + $socket->expects($this->once()) + ->method('fsockopen') + ->willReturn(false); + $ps = new SocketPost($socket); + $response = $ps->submit(new RequestParameters("secret", "response", "remoteip", "version")); + $this->assertEquals('{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}', $response); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestParametersTest.php b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestParametersTest.php new file mode 100644 index 0000000000000000000000000000000000000000..697d6faca7efe2fbb846f435e7ddec85a95ca558 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/RequestParametersTest.php @@ -0,0 +1,62 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha; + +use PHPUnit\Framework\TestCase; + +class RequestParametersTest extends Testcase +{ + public function provideValidData() + { + return array( + array('SECRET', 'RESPONSE', 'REMOTEIP', 'VERSION', + array('secret' => 'SECRET', 'response' => 'RESPONSE', 'remoteip' => 'REMOTEIP', 'version' => 'VERSION'), + 'secret=SECRET&response=RESPONSE&remoteip=REMOTEIP&version=VERSION'), + array('SECRET', 'RESPONSE', null, null, + array('secret' => 'SECRET', 'response' => 'RESPONSE'), + 'secret=SECRET&response=RESPONSE'), + ); + } + + /** + * @dataProvider provideValidData + */ + public function testToArray($secret, $response, $remoteIp, $version, $expectedArray, $expectedQuery) + { + $params = new RequestParameters($secret, $response, $remoteIp, $version); + $this->assertEquals($params->toArray(), $expectedArray); + } + + /** + * @dataProvider provideValidData + */ + public function testToQueryString($secret, $response, $remoteIp, $version, $expectedArray, $expectedQuery) + { + $params = new RequestParameters($secret, $response, $remoteIp, $version); + $this->assertEquals($params->toQueryString(), $expectedQuery); + } +} diff --git a/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/ResponseTest.php b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/ResponseTest.php new file mode 100644 index 0000000000000000000000000000000000000000..623519e23111ccb6041d7be2c00190774a7231b3 --- /dev/null +++ b/web/modules/recaptcha/recaptcha-php/tests/ReCaptcha/ResponseTest.php @@ -0,0 +1,165 @@ +<?php +/** + * This is a PHP library that handles calling reCAPTCHA. + * + * @copyright Copyright (c) 2015, Google Inc. + * @link https://www.google.com/recaptcha + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace ReCaptcha; + +use PHPUnit\Framework\TestCase; + +class ResponseTest extends TestCase +{ + + /** + * @dataProvider provideJson + */ + public function testFromJson($json, $success, $errorCodes, $hostname, $challengeTs, $apkPackageName, $score, $action) + { + $response = Response::fromJson($json); + $this->assertEquals($success, $response->isSuccess()); + $this->assertEquals($errorCodes, $response->getErrorCodes()); + $this->assertEquals($hostname, $response->getHostname()); + $this->assertEquals($challengeTs, $response->getChallengeTs()); + $this->assertEquals($apkPackageName, $response->getApkPackageName()); + $this->assertEquals($score, $response->getScore()); + $this->assertEquals($action, $response->getAction()); + } + + public function provideJson() + { + return array( + array( + '{"success": true}', + true, array(), null, null, null, null, null, + ), + array( + '{"success": true, "hostname": "google.com"}', + true, array(), 'google.com', null, null, null, null, + ), + array( + '{"success": false, "error-codes": ["test"]}', + false, array('test'), null, null, null, null, null, + ), + array( + '{"success": false, "error-codes": ["test"], "hostname": "google.com"}', + false, array('test'), 'google.com', null, null, null, null, + ), + array( + '{"success": false, "error-codes": ["test"], "hostname": "google.com", "challenge_ts": "timestamp", "apk_package_name": "apk", "score": "0.5", "action": "action"}', + false, array('test'), 'google.com', 'timestamp', 'apk', 0.5, 'action', + ), + array( + '{"success": true, "error-codes": ["test"]}', + true, array(), null, null, null, null, null, + ), + array( + '{"success": true, "error-codes": ["test"], "hostname": "google.com"}', + true, array(), 'google.com', null, null, null, null, + ), + array( + '{"success": false}', + false, array(ReCaptcha::E_UNKNOWN_ERROR), null, null, null, null, null, + ), + array( + '{"success": false, "hostname": "google.com"}', + false, array(ReCaptcha::E_UNKNOWN_ERROR), 'google.com', null, null, null, null, + ), + array( + 'BAD JSON', + false, array(ReCaptcha::E_INVALID_JSON), null, null, null, null, null, + ), + ); + } + + public function testIsSuccess() + { + $response = new Response(true); + $this->assertTrue($response->isSuccess()); + + $response = new Response(false); + $this->assertFalse($response->isSuccess()); + + $response = new Response(true, array(), 'example.com'); + $this->assertEquals('example.com', $response->getHostName()); + } + + public function testGetErrorCodes() + { + $errorCodes = array('test'); + $response = new Response(true, $errorCodes); + $this->assertEquals($errorCodes, $response->getErrorCodes()); + } + + public function testGetHostname() + { + $hostname = 'google.com'; + $errorCodes = array(); + $response = new Response(true, $errorCodes, $hostname); + $this->assertEquals($hostname, $response->getHostname()); + } + + public function testGetChallengeTs() + { + $timestamp = 'timestamp'; + $errorCodes = array(); + $response = new Response(true, array(), 'hostname', $timestamp); + $this->assertEquals($timestamp, $response->getChallengeTs()); + } + + public function TestGetApkPackageName() + { + $apk = 'apk'; + $response = new Response(true, array(), 'hostname', 'timestamp', 'apk'); + $this->assertEquals($apk, $response->getApkPackageName()); + } + + public function testGetScore() + { + $score = 0.5; + $response = new Response(true, array(), 'hostname', 'timestamp', 'apk', $score); + $this->assertEquals($score, $response->getScore()); + } + + public function testGetAction() + { + $action = 'homepage'; + $response = new Response(true, array(), 'hostname', 'timestamp', 'apk', '0.5', 'homepage'); + $this->assertEquals($action, $response->getAction()); + } + + public function testToArray() + { + $response = new Response(true, array(), 'hostname', 'timestamp', 'apk', '0.5', 'homepage'); + $expected = array( + 'success' => true, + 'error-codes' => array(), + 'hostname' => 'hostname', + 'challenge_ts' => 'timestamp', + 'apk_package_name' => 'apk', + 'score' => 0.5, + 'action' => 'homepage', + ); + $this->assertEquals($expected, $response->toArray()); + } +} diff --git a/web/modules/recaptcha/recaptcha.info.yml b/web/modules/recaptcha/recaptcha.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..123f386383fb206889b8bb0d76a9f04625c89031 --- /dev/null +++ b/web/modules/recaptcha/recaptcha.info.yml @@ -0,0 +1,14 @@ +name: 'reCAPTCHA' +type: module +description: 'Protect your website from spam and abuse while letting real people pass through with ease.' +package: Spam control +# core: 8.x +configure: recaptcha.admin_settings_form +dependencies: + - captcha:captcha + +# Information added by Drupal.org packaging script on 2019-01-31 +version: '8.x-2.4' +core: '8.x' +project: 'recaptcha' +datestamp: 1548967984 diff --git a/web/modules/recaptcha/recaptcha.links.menu.yml b/web/modules/recaptcha/recaptcha.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..877bb52ffc3b7d98ca299eee68a7def158113e7c --- /dev/null +++ b/web/modules/recaptcha/recaptcha.links.menu.yml @@ -0,0 +1,6 @@ +recaptcha.admin_settings_form: + title: 'reCAPTCHA' + parent: captcha.settings + description: 'Administer the Google No CAPTCHA reCAPTCHA web service.' + route_name: recaptcha.admin_settings_form + weight: 1 diff --git a/web/modules/recaptcha/recaptcha.links.task.yml b/web/modules/recaptcha/recaptcha.links.task.yml new file mode 100644 index 0000000000000000000000000000000000000000..892fac987e941cb8eed823c4aa4bdd766c778bb4 --- /dev/null +++ b/web/modules/recaptcha/recaptcha.links.task.yml @@ -0,0 +1,4 @@ +recaptcha.admin_settings_form_tab: + route_name: recaptcha.admin_settings_form + title: reCAPTCHA + base_route: captcha_settings diff --git a/web/modules/recaptcha/recaptcha.module b/web/modules/recaptcha/recaptcha.module new file mode 100644 index 0000000000000000000000000000000000000000..cc1953e1ee2c4901c53fd071afee379ae7319b70 --- /dev/null +++ b/web/modules/recaptcha/recaptcha.module @@ -0,0 +1,223 @@ +<?php + +/** + * @file + * Verifies if user is a human without necessity to solve a CAPTCHA. + */ + +use ReCaptcha\RequestMethod\Drupal8Post; +use ReCaptcha\ReCaptcha; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Template\Attribute; +use Drupal\Core\Url; + +require_once dirname(__FILE__) . '/recaptcha-php/src/ReCaptcha/ReCaptcha.php'; +require_once dirname(__FILE__) . '/recaptcha-php/src/ReCaptcha/RequestMethod.php'; +require_once dirname(__FILE__) . '/recaptcha-php/src/ReCaptcha/RequestParameters.php'; +require_once dirname(__FILE__) . '/recaptcha-php/src/ReCaptcha/Response.php'; +require_once dirname(__FILE__) . '/src/ReCaptcha/RequestMethod/Drupal8Post.php'; + +/** + * Implements hook_help(). + */ +function recaptcha_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.recaptcha': + $output = ''; + $output .= '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('Google <a href=":url">reCAPTCHA</a> is a free service to protect your website from spam and abuse. reCAPTCHA uses an advanced risk analysis engine and adaptive CAPTCHAs to keep automated software from engaging in abusive activities on your site. It does this while letting your valid users pass through with ease.', [':url' => 'https://www.google.com/recaptcha']) . '</p>'; + $output .= '<h3>' . t('Uses') . '</h3>'; + $output .= '<dl>'; + $output .= '<dt>' . t('Protects and defends') . '</dt>'; + $output .= '<dd>' . t('reCAPTCHA is built for security. Armed with state of the art technology, it always stays at the forefront of spam and abuse fighting trends. reCAPTCHA is on guard for you, so you can rest easy.') . '</dd>'; + $output .= '</dl>'; + $output .= '<h3>' . t('Configuration') . '</h3>'; + $output .= '<ol>'; + $output .= '<li>' . t('Enable reCAPTCHA and CAPTCHA modules in Adminstration > Extend') . '</li>'; + $output .= '<li>' . t('You will now find a reCAPTCHA tab in the CAPTCHA administration page available at: Administration > Configuration > People > CAPTCHA module settings > reCAPTCHA') . '</li>'; + $output .= '<li>' . t('Register your web site at <a href=":url">https://www.google.com/recaptcha/admin/create</a>', [':url' => 'https://www.google.com/recaptcha/admin/create']) . '</li>'; + $output .= '<li>' . t('Input the site and private keys into the reCAPTCHA settings.') . '</li>'; + $output .= '<li>' . t('Visit the Captcha administration page and set where you want the reCAPTCHA form to be presented: Administration > Configuration > People > CAPTCHA module settings') . '</li>'; + $output .= '</ol>'; + return $output; + } +} + +/** + * Implements hook_theme(). + */ +function recaptcha_theme() { + return [ + 'recaptcha_widget_noscript' => [ + 'variables' => [ + 'widget' => NULL, + ], + 'template' => 'recaptcha-widget-noscript', + ], + ]; +} + +/** + * Implements hook_captcha(). + */ +function recaptcha_captcha($op, $captcha_type = '') { + + switch ($op) { + case 'list': + return ['reCAPTCHA']; + + case 'generate': + $captcha = []; + if ($captcha_type == 'reCAPTCHA') { + $config = \Drupal::config('recaptcha.settings'); + $renderer = \Drupal::service('renderer'); + $recaptcha_site_key = $config->get('site_key'); + $recaptcha_secret_key = $config->get('secret_key'); + $recaptcha_use_globally = $config->get('use_globally'); + + if (!empty($recaptcha_site_key) && !empty($recaptcha_secret_key)) { + // Build the reCAPTCHA captcha form if site_key and secret_key are + // configured. Captcha requires TRUE to be returned in solution. + $captcha['solution'] = TRUE; + $captcha['captcha_validate'] = 'recaptcha_captcha_validation'; + $captcha['form']['captcha_response'] = [ + '#type' => 'hidden', + '#value' => 'Google no captcha', + ]; + + // As the validate callback does not depend on sid or solution, this + // captcha type can be displayed on cached pages. + $captcha['cacheable'] = TRUE; + + // Check if reCAPTCHA use globally is enabled. + $recaptcha_src = 'https://www.google.com/recaptcha/api.js'; + $recaptcha_src_fallback = 'https://www.google.com/recaptcha/api/fallback'; + if ($recaptcha_use_globally) { + $recaptcha_src = 'https://www.recaptcha.net/recaptcha/api.js'; + $recaptcha_src_fallback = 'https://www.recaptcha.net/recaptcha/api/fallback'; + } + + $noscript = ''; + if ($config->get('widget.noscript')) { + $recaptcha_widget_noscript = [ + '#theme' => 'recaptcha_widget_noscript', + '#widget' => [ + 'sitekey' => $recaptcha_site_key, + 'recaptcha_src_fallback' => $recaptcha_src_fallback, + 'language' => \Drupal::service('language_manager')->getCurrentLanguage()->getId(), + ], + ]; + $noscript = $renderer->render($recaptcha_widget_noscript); + } + + $attributes = [ + 'class' => 'g-recaptcha', + 'data-sitekey' => $recaptcha_site_key, + 'data-theme' => $config->get('widget.theme'), + 'data-type' => $config->get('widget.type'), + 'data-size' => $config->get('widget.size'), + 'data-tabindex' => $config->get('widget.tabindex'), + ]; + // Filter out empty tabindex/size. + $attributes = array_filter($attributes); + + $captcha['form']['recaptcha_widget'] = [ + '#markup' => '<div' . new Attribute($attributes) . '></div>', + '#suffix' => $noscript, + '#attached' => [ + 'html_head' => [ + [ + [ + '#tag' => 'script', + '#attributes' => [ + 'src' => Url::fromUri($recaptcha_src, ['query' => ['hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId()], 'absolute' => TRUE])->toString(), + 'async' => TRUE, + 'defer' => TRUE, + ], + ], + 'recaptcha_api', + ], + ], + ], + ]; + } + else { + // Fallback to Math captcha as reCAPTCHA is not configured. + $captcha = captcha_captcha('generate', 'Math'); + } + + // If module configuration changes the form cache need to be refreshed. + $renderer->addCacheableDependency($captcha['form'], $config); + } + return $captcha; + } +} + +/** + * CAPTCHA Callback; Validates the reCAPTCHA code. + */ +function recaptcha_captcha_validation($solution, $response, $element, $form_state) { + $config = \Drupal::config('recaptcha.settings'); + + $recaptcha_secret_key = $config->get('secret_key'); + if (empty($_POST['g-recaptcha-response']) || empty($recaptcha_secret_key)) { + return FALSE; + } + + // Use Drupal::httpClient() to circumvent all issues with the Google library. + $recaptcha = new ReCaptcha($recaptcha_secret_key, new Drupal8Post()); + + // Ensures the hostname matches. Required if "Domain Name Validation" is + // disabled for credentials. + if ($config->get('verify_hostname')) { + $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME']); + } + + $resp = $recaptcha->verify( + $_POST['g-recaptcha-response'], + \Drupal::request()->getClientIp() + ); + + if ($resp->isSuccess()) { + // Verified! + return TRUE; + } + else { + // Error code reference, https://developers.google.com/recaptcha/docs/verify + $error_codes = [ + 'action-mismatch' => t('Expected action did not match.'), + 'apk_package_name-mismatch' => t('Expected APK package name did not match.'), + 'bad-response' => t('Did not receive a 200 from the service.'), + 'bad-request' => t('The request is invalid or malformed.'), + 'challenge-timeout' => t('Challenge timeout.'), + 'connection-failed' => t('Could not connect to service.'), + 'invalid-input-response' => t('The response parameter is invalid or malformed.'), + 'invalid-input-secret' => t('The secret parameter is invalid or malformed.'), + 'invalid-json' => t('The json response is invalid or malformed.'), + 'missing-input-response' => t('The response parameter is missing.'), + 'missing-input-secret' => t('The secret parameter is missing.'), + 'hostname-mismatch' => t('Expected hostname did not match.'), + 'score-threshold-not-met' => t('Score threshold not met.'), + 'timeout-or-duplicate' => t('The challenge response timed out or was already verified.'), + 'unknown-error' => t('Not a success, but no error codes received!'), + ]; + foreach ($resp->getErrorCodes() as $code) { + if (!isset($error_codes[$code])) { + $code = 'unknown-error'; + } + \Drupal::logger('reCAPTCHA web service')->error('@error', ['@error' => $error_codes[$code]]); + } + } + return FALSE; +} + +/** + * Process variables for recaptcha-widget-noscript.tpl.php. + * + * @see recaptcha-widget-noscript.tpl.php + */ +function template_preprocess_recaptcha_widget_noscript(&$variables) { + $variables['sitekey'] = $variables['widget']['sitekey']; + $variables['language'] = $variables['widget']['language']; + $variables['url'] = Url::fromUri($variables['widget']['recaptcha_src_fallback'], ['query' => ['k' => $variables['widget']['sitekey'], 'hl' => $variables['widget']['language']], 'absolute' => TRUE])->toString(); +} diff --git a/web/modules/recaptcha/recaptcha.permissions.yml b/web/modules/recaptcha/recaptcha.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..96b5c4c37eb3a92aed8802e3f079549e4ece81ce --- /dev/null +++ b/web/modules/recaptcha/recaptcha.permissions.yml @@ -0,0 +1,3 @@ +administer recaptcha: + title: 'Administer reCAPTCHA' + description: 'Administer reCAPTCHA settings.' diff --git a/web/modules/recaptcha/recaptcha.routing.yml b/web/modules/recaptcha/recaptcha.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..fe5b60ef6154ed022c5e6580c4008b186f3f4b13 --- /dev/null +++ b/web/modules/recaptcha/recaptcha.routing.yml @@ -0,0 +1,7 @@ +recaptcha.admin_settings_form: + path: '/admin/config/people/captcha/recaptcha' + defaults: + _form: '\Drupal\recaptcha\Form\ReCaptchaAdminSettingsForm' + _title: 'reCAPTCHA' + requirements: + _permission: 'administer recaptcha' diff --git a/web/modules/recaptcha/src/Form/ReCaptchaAdminSettingsForm.php b/web/modules/recaptcha/src/Form/ReCaptchaAdminSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..2029bf0717ef1ab94f6428ad29da6c857759b746 --- /dev/null +++ b/web/modules/recaptcha/src/Form/ReCaptchaAdminSettingsForm.php @@ -0,0 +1,146 @@ +<?php + +namespace Drupal\recaptcha\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Configure reCAPTCHA settings for this site. + */ +class ReCaptchaAdminSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'recaptcha_admin_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['recaptcha.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('recaptcha.settings'); + + $form['general'] = [ + '#type' => 'details', + '#title' => $this->t('General settings'), + '#open' => TRUE, + ]; + + $form['general']['recaptcha_site_key'] = [ + '#default_value' => $config->get('site_key'), + '#description' => $this->t('The site key given to you when you <a href=":url">register for reCAPTCHA</a>.', [':url' => 'https://www.google.com/recaptcha/admin']), + '#maxlength' => 40, + '#required' => TRUE, + '#title' => $this->t('Site key'), + '#type' => 'textfield', + ]; + + $form['general']['recaptcha_secret_key'] = [ + '#default_value' => $config->get('secret_key'), + '#description' => $this->t('The secret key given to you when you <a href=":url">register for reCAPTCHA</a>.', [':url' => 'https://www.google.com/recaptcha/admin']), + '#maxlength' => 40, + '#required' => TRUE, + '#title' => $this->t('Secret key'), + '#type' => 'textfield', + ]; + + $form['general']['recaptcha_verify_hostname'] = [ + '#default_value' => $config->get('verify_hostname'), + '#description' => $this->t('Checks the hostname on your server when verifying a solution. Enable this validation only, if <em>Verify the origin of reCAPTCHA solutions</em> is unchecked for your key pair. Provides crucial security by verifying requests come from one of your listed domains.'), + '#title' => $this->t('Local domain name validation'), + '#type' => 'checkbox', + ]; + + $form['general']['recaptcha_use_globally'] = [ + '#default_value' => $config->get('use_globally'), + '#description' => $this->t('Enable this in circumstances when "www.google.com" is not accessible, e.g. China.'), + '#title' => $this->t('Use reCAPTCHA globally'), + '#type' => 'checkbox', + ]; + + // Widget configurations. + $form['widget'] = [ + '#type' => 'details', + '#title' => $this->t('Widget settings'), + '#open' => TRUE, + ]; + $form['widget']['recaptcha_theme'] = [ + '#default_value' => $config->get('widget.theme'), + '#description' => $this->t('Defines which theme to use for reCAPTCHA.'), + '#options' => [ + 'light' => $this->t('Light (default)'), + 'dark' => $this->t('Dark'), + ], + '#title' => $this->t('Theme'), + '#type' => 'select', + ]; + $form['widget']['recaptcha_type'] = [ + '#default_value' => $config->get('widget.type'), + '#description' => $this->t('The type of CAPTCHA to serve.'), + '#options' => [ + 'image' => $this->t('Image (default)'), + 'audio' => $this->t('Audio'), + ], + '#title' => $this->t('Type'), + '#type' => 'select', + ]; + $form['widget']['recaptcha_size'] = [ + '#default_value' => $config->get('widget.size'), + '#description' => $this->t('The size of CAPTCHA to serve.'), + '#options' => [ + '' => $this->t('Normal (default)'), + 'compact' => $this->t('Compact'), + ], + '#title' => $this->t('Size'), + '#type' => 'select', + ]; + $form['widget']['recaptcha_tabindex'] = [ + '#default_value' => $config->get('widget.tabindex'), + '#description' => $this->t('Set the <a href=":tabindex">tabindex</a> of the widget and challenge (Default = 0). If other elements in your page use tabindex, it should be set to make user navigation easier.', [':tabindex' => Url::fromUri('https://www.w3.org/TR/html4/interact/forms.html', ['fragment' => 'adef-tabindex'])->toString()]), + '#maxlength' => 4, + '#title' => $this->t('Tabindex'), + '#type' => 'number', + '#min' => -1, + ]; + $form['widget']['recaptcha_noscript'] = [ + '#default_value' => $config->get('widget.noscript'), + '#description' => $this->t('If JavaScript is a requirement for your site, you should <strong>not</strong> enable this feature. With this enabled, a compatibility layer will be added to the captcha to support non-js users.'), + '#title' => $this->t('Enable fallback for browsers with JavaScript disabled'), + '#type' => 'checkbox', + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('recaptcha.settings'); + $config + ->set('site_key', $form_state->getValue('recaptcha_site_key')) + ->set('secret_key', $form_state->getValue('recaptcha_secret_key')) + ->set('verify_hostname', $form_state->getValue('recaptcha_verify_hostname')) + ->set('use_globally', $form_state->getValue('recaptcha_use_globally')) + ->set('widget.theme', $form_state->getValue('recaptcha_theme')) + ->set('widget.type', $form_state->getValue('recaptcha_type')) + ->set('widget.size', $form_state->getValue('recaptcha_size')) + ->set('widget.tabindex', $form_state->getValue('recaptcha_tabindex')) + ->set('widget.noscript', $form_state->getValue('recaptcha_noscript')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/recaptcha/src/ReCaptcha/RequestMethod/Drupal8Post.php b/web/modules/recaptcha/src/ReCaptcha/RequestMethod/Drupal8Post.php new file mode 100644 index 0000000000000000000000000000000000000000..5ff29e1a55a45c8b3f0381606f5f23614a2444cb --- /dev/null +++ b/web/modules/recaptcha/src/ReCaptcha/RequestMethod/Drupal8Post.php @@ -0,0 +1,51 @@ +<?php + +namespace ReCaptcha\RequestMethod; + +use ReCaptcha\ReCaptcha; +use ReCaptcha\RequestMethod; +use ReCaptcha\RequestParameters; + +/** + * Sends POST requests to the reCAPTCHA service with Drupal 8 httpClient. + */ +class Drupal8Post implements RequestMethod { + + /** + * Submit the POST request with the specified parameters. + * + * @param \ReCaptcha\ReCaptcha\RequestParameters $params + * Request parameters. + * + * @return string + * Body of the reCAPTCHA response. + */ + public function submit(RequestParameters $params) { + + $options = [ + 'headers' => [ + 'Content-type' => 'application/x-www-form-urlencoded', + ], + 'body' => $params->toQueryString(), + // Stop firing exception on response status code >= 300. + // See http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html + 'http_errors' => FALSE, + ]; + + $response = \Drupal::httpClient()->post(ReCaptcha::SITE_VERIFY_URL, $options); + + if ($response->getStatusCode() == 200) { + // The service request was successful. + return (string) $response->getBody(); + } + elseif ($response->getStatusCode() < 0) { + // Negative status codes typically point to network or socket issues. + return '{"success": false, "error-codes": ["' . ReCaptcha::E_CONNECTION_FAILED . '"]}'; + } + else { + // Positive none 200 status code typically means the request has failed. + return '{"success": false, "error-codes": ["' . ReCaptcha::E_BAD_RESPONSE . '"]}'; + } + } + +} diff --git a/web/modules/recaptcha/templates/recaptcha-widget-noscript.html.twig b/web/modules/recaptcha/templates/recaptcha-widget-noscript.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..b1737fe9f3834c0a9d5f535eaf0f02f75a398842 --- /dev/null +++ b/web/modules/recaptcha/templates/recaptcha-widget-noscript.html.twig @@ -0,0 +1,29 @@ +{# +/** + * @file recaptcha-widget-noscript.tpl.php + * Default theme implementation to present the reCAPTCHA noscript code. + * + * Available variables: + * - sitekey: Google web service site key. + * - language: Current site language code. + * - url: Google web service API url. + * + * @see template_preprocess() + * @see template_preprocess_recaptcha_widget_noscript() + * + * @ingroup themeable + */ +#} + +<noscript> + <div style="width: 302px; height: 352px;"> + <div style="width: 302px; height: 352px; position: relative;"> + <div style="width: 302px; height: 352px; position: absolute;"> + <iframe src="{{ url }}" frameborder="0" scrolling="no" style="width: 302px; height:352px; border-style: none;"></iframe> + </div> + <div style="width: 250px; height: 80px; position: absolute; border-style: none; bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;"> + <textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 80px; border: 1px solid #c1c1c1; margin: 0px; padding: 0px; resize: none;" value=""></textarea> + </div> + </div> + </div> +</noscript> diff --git a/web/modules/recaptcha/tests/src/Functional/ReCaptchaBasicTest.php b/web/modules/recaptcha/tests/src/Functional/ReCaptchaBasicTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1189ac2e430d003ebfd873e53e08a79d19cebcac --- /dev/null +++ b/web/modules/recaptcha/tests/src/Functional/ReCaptchaBasicTest.php @@ -0,0 +1,210 @@ +<?php + +namespace Drupal\Tests\recaptcha\Functional; + +use Drupal\Core\Url; +use Drupal\Component\Utility\Html; +use Drupal\Tests\BrowserTestBase; + +/** + * Test basic functionality of reCAPTCHA module. + * + * @group reCAPTCHA + * + * @dependencies captcha + */ +class ReCaptchaBasicTest extends BrowserTestBase { + + /** + * A normal user. + * + * @var \Drupal\user\UserInterface + */ + protected $normalUser; + + /** + * An admin user. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['recaptcha', 'captcha']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + module_load_include('inc', 'captcha'); + + // Create a normal user. + $permissions = [ + 'access content', + ]; + $this->normalUser = $this->drupalCreateUser($permissions); + + // Create an admin user. + $permissions += [ + 'administer CAPTCHA settings', + 'skip CAPTCHA', + 'administer permissions', + 'administer content types', + 'administer recaptcha', + ]; + $this->adminUser = $this->drupalCreateUser($permissions); + } + + /** + * Test access to the administration page. + */ + public function testReCaptchaAdminAccess() { + $this->drupalLogin($this->adminUser); + $this->drupalGet('admin/config/people/captcha/recaptcha'); + $this->assertSession()->pageTextNotContains(t('Access denied'), 'Admin users should be able to access the reCAPTCHA admin page', 'reCAPTCHA'); + $this->drupalLogout(); + } + + /** + * Test the reCAPTCHA settings form. + */ + public function testReCaptchaAdminSettingsForm() { + $this->drupalLogin($this->adminUser); + + $site_key = $this->randomMachineName(40); + $secret_key = $this->randomMachineName(40); + + // Check form validation. + $edit['recaptcha_site_key'] = ''; + $edit['recaptcha_secret_key'] = ''; + $this->drupalPostForm('admin/config/people/captcha/recaptcha', $edit, t('Save configuration')); + + $this->assertSession()->responseContains(t('Site key field is required.'), '[testReCaptchaConfiguration]: Empty site key detected.'); + $this->assertSession()->responseContains(t('Secret key field is required.'), '[testReCaptchaConfiguration]: Empty secret key detected.'); + + // Save form with valid values. + $edit['recaptcha_site_key'] = $site_key; + $edit['recaptcha_secret_key'] = $secret_key; + $edit['recaptcha_tabindex'] = 0; + $this->drupalPostForm('admin/config/people/captcha/recaptcha', $edit, t('Save configuration')); + $this->assertSession()->responseContains(t('The configuration options have been saved.'), '[testReCaptchaConfiguration]: The configuration options have been saved.'); + + $this->assertSession()->responseNotContains(t('Site key field is required.'), '[testReCaptchaConfiguration]: Site key was not empty.'); + $this->assertSession()->responseNotContains(t('Secret key field is required.'), '[testReCaptchaConfiguration]: Secret key was not empty.'); + $this->assertSession()->responseNotContains(t('The tabindex must be an integer.'), '[testReCaptchaConfiguration]: Tab index had a valid input.'); + + $this->drupalLogout(); + } + + /** + * Testing the protection of the user login form. + */ + public function testReCaptchaOnLoginForm() { + $site_key = $this->randomMachineName(40); + $secret_key = $this->randomMachineName(40); + $grecaptcha = '<div class="g-recaptcha" data-sitekey="' . $site_key . '" data-theme="light" data-type="image"></div>'; + + // Test if login works. + $this->drupalLogin($this->normalUser); + $this->drupalLogout(); + + $this->drupalGet('user/login'); + $this->assertSession()->responseNotContains($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is not shown on form.'); + + // Enable 'captcha/Math' CAPTCHA on login form. + captcha_set_form_id_setting('user_login_form', 'captcha/Math'); + + $this->drupalGet('user/login'); + $this->assertSession()->responseNotContains($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is not shown on form.'); + + // Enable 'recaptcha/reCAPTCHA' on login form. + captcha_set_form_id_setting('user_login_form', 'recaptcha/reCAPTCHA'); + $result = captcha_get_form_id_setting('user_login_form'); + $this->assertNotNull($result, 'A configuration has been found for CAPTCHA point: user_login_form', 'reCAPTCHA'); + $this->assertEquals($result->getCaptchaType(), 'recaptcha/reCAPTCHA', 'reCAPTCHA type has been configured for CAPTCHA point: user_login_form'); + + // Check if a Math CAPTCHA is still shown on the login form. The site key + // and security key have not yet configured for reCAPTCHA. The module need + // to fall back to math captcha. + $this->drupalGet('user/login'); + $this->assertSession()->responseContains(t('Math question'), '[testReCaptchaOnLoginForm]: Math CAPTCHA is shown on form.'); + + // Configure site key and security key to show reCAPTCHA and no fall back. + $this->config('recaptcha.settings')->set('site_key', $site_key)->save(); + $this->config('recaptcha.settings')->set('secret_key', $secret_key)->save(); + + // Check if there is a reCAPTCHA on the login form. + $this->drupalGet('user/login'); + $this->assertSession()->responseContains($grecaptcha, '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.'); + $this->assertSession()->responseContains('<script src="' . Url::fromUri('https://www.google.com/recaptcha/api.js', ['query' => ['hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId()], 'absolute' => TRUE])->toString() . '" async defer></script>', '[testReCaptchaOnLoginForm]: reCAPTCHA is shown on form.'); + $this->assertSession()->responseNotContains($grecaptcha . '<noscript>', '[testReCaptchaOnLoginForm]: NoScript code is not enabled for the reCAPTCHA.'); + + // Test if the fall back url is properly build and noscript code added. + $this->config('recaptcha.settings')->set('widget.noscript', 1)->save(); + + $this->drupalGet('user/login'); + $this->assertSession()->responseContains($grecaptcha . "\n" . '<noscript>', '[testReCaptchaOnLoginForm]: NoScript for reCAPTCHA is shown on form.'); + $options = [ + 'query' => [ + 'k' => $site_key, + 'hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId(), + ], + 'absolute' => TRUE, + ]; + $this->assertSession()->responseContains(Html::escape(Url::fromUri('https://www.google.com/recaptcha/api/fallback', $options)->toString()), '[testReCaptchaOnLoginForm]: Fallback URL with IFRAME has been found.'); + + // Check if there is a reCAPTCHA with global url on the login form. + $this->config('recaptcha.settings')->set('use_globally', TRUE)->save(); + $this->drupalGet('user/login'); + $this->assertSession()->responseContains('<script src="' . Url::fromUri('https://www.recaptcha.net/recaptcha/api.js', ['query' => ['hl' => \Drupal::service('language_manager')->getCurrentLanguage()->getId()], 'absolute' => TRUE])->toString() . '" async defer></script>', '[testReCaptchaOnLoginForm]: Global reCAPTCHA is shown on form.'); + $this->assertSession()->responseContains(Html::escape(Url::fromUri('https://www.recaptcha.net/recaptcha/api/fallback', $options)->toString()), '[testReCaptchaOnLoginForm]: Global fallback URL with IFRAME has been found.'); + + // Check that data-size attribute does not exists. + $this->config('recaptcha.settings')->set('widget.size', '')->save(); + $this->drupalGet('user/login'); + $element = $this->xpath('//div[@class=:class and @data-size=:size]', [':class' => 'g-recaptcha', ':size' => 'small']); + $this->assertFalse(!empty($element), 'Tag contains no data-size attribute.'); + + // Check that data-size attribute exists. + $this->config('recaptcha.settings')->set('widget.size', 'small')->save(); + $this->drupalGet('user/login'); + $element = $this->xpath('//div[@class=:class and @data-size=:size]', [':class' => 'g-recaptcha', ':size' => 'small']); + $this->assertTrue(!empty($element), 'Tag contains data-size attribute and value.'); + + // Check that data-tabindex attribute does not exists. + $this->config('recaptcha.settings')->set('widget.tabindex', 0)->save(); + $this->drupalGet('user/login'); + $element = $this->xpath('//div[@class=:class and @data-tabindex=:index]', [':class' => 'g-recaptcha', ':index' => 0]); + $this->assertFalse(!empty($element), 'Tag contains no data-tabindex attribute.'); + + // Check that data-tabindex attribute exists. + $this->config('recaptcha.settings')->set('widget.tabindex', 5)->save(); + $this->drupalGet('user/login'); + $element = $this->xpath('//div[@class=:class and @data-tabindex=:index]', [':class' => 'g-recaptcha', ':index' => 5]); + $this->assertTrue(!empty($element), 'Tag contains data-tabindex attribute and value.'); + + // Try to log in, which should fail. + $edit['name'] = $this->normalUser->getAccountName(); + $edit['pass'] = $this->normalUser->getPassword(); + $this->assertSession()->responseContains('captcha_response'); + $this->assertSession() + ->hiddenFieldExists('captcha_response') + ->setValue('?'); + + $this->drupalPostForm('user/login', $edit, t('Log in')); + // Check for error message. + $this->assertSession()->pageTextContains(t('The answer you entered for the CAPTCHA was not correct.'), 'CAPTCHA should block user login form', 'reCAPTCHA'); + + // And make sure that user is not logged in: check for name and password + // fields on "?q=user". + $this->drupalGet('user/login'); + $this->assertSession()->fieldExists('name'); + $this->assertSession()->fieldExists('pass'); + } + +}