diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b14661c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +[*.{yaml,yml}] +indent_size = 2 +indent_style = space diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..ea59560 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,52 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +jobs: + ecs: + name: ECS Check + runs-on: ubuntu-latest + env: + DEFAULT_COMPOSER_FLAGS: "--no-interaction --no-ansi --no-progress" + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + - run: composer install --prefer-dist --no-progress + - name: Run ecs check + run: | + vendor/bin/ecs --memory-limit=1G --no-progress-bar + rector: + name: Rector dry-run + runs-on: ubuntu-latest + env: + DEFAULT_COMPOSER_FLAGS: "--no-interaction --no-ansi --no-progress" + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + - run: composer install --prefer-dist --no-progress + - name: Run rector --dry-run + run: | + vendor/bin/rector --memory-limit=1G --no-progress-bar --dry-run + phpstan: + name: PHPStan + runs-on: ubuntu-latest + env: + DEFAULT_COMPOSER_FLAGS: "--no-interaction --no-ansi --no-progress" + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + - run: composer install --prefer-dist --no-progress + - name: Run phpstan + run: | + vendor/bin/phpstan --memory-limit=1G diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1b8972 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.DS_Store +*.idea/* +*.log +*Thumbs.db +.env +composer.lock +/node_modules +/vendor diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..410f01a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release Notes for honeypot + +## 1.0.0-beta.1 + +- Initial beta release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2145759 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,40 @@ +Copyright © Foster Commerce + +Permission is hereby granted to any person obtaining a copy of this software +(the “Software”) to use, copy, modify, merge, publish and/or distribute copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +1. **Don’t plagiarize.** The above copyright notice and this license shall be + included in all copies or substantial portions of the Software. + +2. **Don’t use the same license on more than one project.** Each licensed copy + of the Software shall be actively installed in no more than one production + environment at a time. + +3. **Don’t mess with the licensing features.** Software features related to + licensing shall not be altered or circumvented in any way, including (but + not limited to) license validation, payment prompts, feature restrictions, + and update eligibility. + +4. **Pay up.** Payment shall be made immediately upon receipt of any notice, + prompt, reminder, or other message indicating that a payment is owed. + +5. **Follow the law.** All use of the Software shall not violate any applicable + law or regulation, nor infringe the rights of any other person or entity. + +Failure to comply with the foregoing conditions will automatically and +immediately result in termination of the permission granted hereby. This +license does not include any right to receive updates to the Software or +technical support. Licensees bear all risk related to the quality and +performance of the Software and any modifications made or obtained to it, +including liability for actual and consequential harm, such as loss or +corruption of data, and any necessary service, repair, or correction. + +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, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..615319b --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Craft Honeypot + +```html + +
+``` + +## Requirements + +This plugin requires Craft CMS 4.12.0 or later, and PHP 8.0.2 or later. + +## Installation + +You can install this plugin from the Plugin Store or with Composer. + +#### From the Plugin Store + +Go to the Plugin Store in your project’s Control Panel and search for “honeypot”. Then press “Install”. + +#### With Composer + +Open your terminal and run the following commands: + +```bash +# go to the project directory +cd /path/to/my-project.test + +# tell Composer to load the plugin +composer require fostercommerce/craft-honeypot + +# tell Craft to install the plugin +./craft plugin/install honeypot +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ce4bea3 --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "fostercommerce/craft-honeypot", + "type": "craft-plugin", + "license": "proprietary", + "version": "1.0.0-beta.1", + "support": { + "email": "support@fostercommerce.com", + "issues": "https://github.com/fostercommerce/craft-honeypot/issues?state=open", + "source": "https://github.com/fostercommerce/craft-honeypot", + "docs": "https://github.com/fostercommerce/craft-honeypot", + "rss": "https://github.com/fostercommerce/craft-honeypot/releases.atom" + }, + "require": { + "php": ">=8.0.2", + "craftcms/cms": "^4.0.0|^5.0.0" + }, + "require-dev": { + "craftcms/phpstan": "dev-main", + "craftcms/rector": "dev-main", + "fostercommerce/ecs": "dev-main", + "fostercommerce/phpstan": "dev-main", + "fostercommerce/rector": "dev-main" + }, + "autoload": { + "psr-4": { + "fostercommerce\\honeypot\\": "src/" + } + }, + "extra": { + "handle": "honeypot", + "name": "honeypot", + "developer": "Foster Commerce", + "documentationUrl": "https://github.com/fostercommerce/craft-honeypot" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "yiisoft/yii2-composer": true, + "craftcms/plugin-installer": true + } + }, + "scripts": { + "phpstan": "phpstan --memory-limit=1G", + "ecs:check": "ecs check --ansi --memory-limit=1G", + "ecs:fix": "ecs check --ansi --fix --memory-limit=1G", + "rector:fix": "rector process --config rector.php", + "rector:dry-run": "rector process --dry-run --config rector.php" + } +} + diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..94805c1 --- /dev/null +++ b/ecs.php @@ -0,0 +1,11 @@ +withPaths([ + __DIR__ . '/src', + __FILE__, + ]); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b35eb56 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - vendor/craftcms/phpstan/phpstan.neon + +parameters: + paths: + - src + level: 9 diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..80f88db --- /dev/null +++ b/rector.php @@ -0,0 +1,13 @@ +withPaths([ + __DIR__ . '/src', + __FILE__, + ]) + ->withSets([SetList::CRAFT_CMS_40]); diff --git a/src/Plugin.php b/src/Plugin.php new file mode 100644 index 0000000..fb93eff --- /dev/null +++ b/src/Plugin.php @@ -0,0 +1,96 @@ +attachEventHandlers(); + + Craft::$app->view->registerTwigExtension(new Honeypot()); + } + + protected function createSettingsModel(): ?Model + { + return Craft::createObject(Settings::class); + } + + protected function settingsHtml(): ?string + { + return Craft::$app->view->renderTemplate('honeypot/_settings.twig', [ + 'plugin' => $this, + 'settings' => $this->getSettings(), + ]); + } + + private function attachEventHandlers(): void + { + Event::on( + Application::class, + Application::EVENT_BEFORE_REQUEST, + function (Event $event): void { + /** @var Request $request */ + $request = Craft::$app->getRequest(); + if ($request->getIsPost() || $request->getIsPut()) { + $settings = $this->getSettings(); + $honeypotValue = $request->getBodyParam($settings->honeypotFieldName); + if ($honeypotValue === null) { + // A bot simply has to remove the input field altogether to bypass this check. + return; + } + + if (! empty($honeypotValue)) { + if ($settings->logSpamSubmissions !== false) { + $userIp = $request->getUserIP(); + $userAgent = $request->getUserAgent(); + $action = implode('/', $request->getActionSegments()); + $message = sprintf('Spam submission blocked. IP: %s, Action: %s, User Agent: %s', $userIp, $action, $userAgent); + + if (in_array($settings->logSpamSubmissions, self::LOG_LEVELS, true)) { + Craft::{$settings->logSpamSubmissions}($message); + } else { + Craft::debug($message); + } + } + + if ($settings->spamDetectedResponse !== false || App::devMode()) { + ob_start(); + + if ($settings->spamDetectedResponse !== false) { + echo $settings->spamDetectedResponse; + } else { + echo 'Spam submission detected'; + } + } + + exit(0); + } + } + } + ); + } +} diff --git a/src/config.php b/src/config.php new file mode 100644 index 0000000..fda91d9 --- /dev/null +++ b/src/config.php @@ -0,0 +1,31 @@ + true, + + /** + * The name to give the hidden input field. + * + * This should be unique so that it does not conflict with any of your form inputs. + */ + 'honeypotFieldName' => 'my_password', + + /** + * `false` to disable responses on non-dev environments. + * + * Set to a string value to enable a response on non-dev environment.s + */ + 'spamDetectedResponse' => 'Spam submission recorded', + + /** + * Whether to log every spam submission. + * + * `false` to disable logs. + * + * A string value of log-level to enable and generate a log with the desired level. + */ + 'logSpamSubmissions' => 'debug', +]; diff --git a/src/models/Settings.php b/src/models/Settings.php new file mode 100644 index 0000000..24693b0 --- /dev/null +++ b/src/models/Settings.php @@ -0,0 +1,36 @@ +getSettings(); + if (! $settings->enabled) { + return false; + } + + return Html::textInput( + $settings->honeypotFieldName, + '', + [ + 'id' => $settings->honeypotFieldName, + 'autocomplete' => 'off', + 'tabindex' => '-1', + 'style' => 'display:none; visibility:hidden; position:absolute; left:-9999px;', + ], + ); + }, + [ + 'is_safe' => ['html'], + ] + ), + ]; + } +}