recranet/craft-secure-forms
Composer 安装命令:
composer require recranet/craft-secure-forms
包简介
Contact forms for Craft CMS 5 with spam protection (reCAPTCHA, Turnstile, honeypot), persisted spam scores, stored submissions, proper error reporting and an SMTP test utility
README 文档
README
Contact forms for Craft CMS 5 with spam protection, stored submissions and proper error reporting. Replaces craftcms/contact-form, hybridinteractive/craft-contact-form-extensions and recranet/craft-contact-form-recaptcha in one plugin.
Design principles
- Spam is not an error. Real spam is stored with its score and reason, silently accepted (configurable), and never logged as an error.
- Misconfiguration is a real error. Missing/invalid captcha keys, a domain missing from the allowlist, an unreachable verification API or an SMTP failure are shown to the visitor, logged under the
secure-formscategory, and forwarded to Sentry when the SDK is installed. They never silently classify submissions as spam. - Nothing is ever lost. Submissions are persisted before any email is attempted; send failures are recorded on the submission (
failedstatus) instead of dropping the message.
Features
- Submission storage as elements with a fixed schema — dynamic form fields are stored as JSON and expanded back into columns on CSV export
- Persisted spam classification:
isSpam,spamScore(reCAPTCHA v3 score),spamReason - Spam protection: honeypot + Google reCAPTCHA v2/v3/Enterprise or Cloudflare Turnstile (experimental)
- Notification + optional confirmation emails, rendered from site templates
- Email / SMTP test utility in the control panel (works with
allowAdminChangesdisabled) that surfaces full SMTP transport errors - Control panel section with per-form sources, statuses (sent / spam / failed) and search
Installation
composer require recranet/craft-secure-forms php craft plugin/install secure-forms
Usage
<form method="post" accept-charset="UTF-8"> {{ csrfInput() }} {{ actionInput('secure-forms/submit') }} {{ redirectInput(craft.app.request.pathInfo ~ '?submitted=true') }} {{ hiddenInput('formName', 'contact') }} {# optional template overrides (hashed, tamper-proof) #} {{ hiddenInput('notificationTemplate', '_emails/notifications/contact'|hash) }} {{ hiddenInput('confirmationTemplate', '_emails/confirmations/contact'|hash) }} {{ hiddenInput('confirmationSubject', 'Thanks!'|hash) }} <input type="text" name="fromName" required> <input type="email" name="fromEmail" required> <textarea name="message[body]" required></textarea> {# any other message[...] fields are stored as dynamic fields #} {{ craft.secureForms.honeypot() }} {{ craft.secureForms.captcha() }} <button type="submit">Send</button> </form>
Error handling in the template:
{% set submission = craft.app.urlManager.getRouteParams().submission ?? null %}
{% if submission %}
{{ ul(submission.getErrorSummary(true)) }}
{% endif %}
Configuration
All settings are read from the environment with sensible defaults, so one config file works across dev/staging/production. Create config/secure-forms.php:
<?php use craft\helpers\App; return [ // Email 'toEmail' => App::env('SECURE_FORMS_TO_EMAIL') ?: null, // null = system email address 'prependSubject' => App::env('SECURE_FORMS_PREPEND_SUBJECT') ?: 'New contact form submission', 'notificationTemplate' => App::env('SECURE_FORMS_NOTIFICATION_TEMPLATE') ?: '_emails/notifications/contact', 'enableConfirmationEmail' => App::env('SECURE_FORMS_CONFIRMATION_ENABLED') ?? true, 'confirmationTemplate' => App::env('SECURE_FORMS_CONFIRMATION_TEMPLATE') ?: '_emails/confirmations/contact', // Storage 'saveSubmissions' => App::env('SECURE_FORMS_SAVE_SUBMISSIONS') ?? true, 'saveSpamSubmissions' => App::env('SECURE_FORMS_SAVE_SPAM_SUBMISSIONS') ?? true, // Spam protection — 'silent' pretends success so bots learn nothing 'spamAction' => App::env('SECURE_FORMS_SPAM_ACTION') ?: 'silent', // or 'error' 'honeypotEnabled' => App::env('SECURE_FORMS_HONEYPOT_ENABLED') ?? true, 'honeypotParam' => App::env('SECURE_FORMS_HONEYPOT_PARAM') ?: 'honeyPotProtection', // Captcha: '', recaptcha-v2, recaptcha-v3, recaptcha-enterprise, // turnstile (experimental). Defaults to reCAPTCHA v3 when a site key is // configured, off otherwise. 'captchaProvider' => App::env('SECURE_FORMS_CAPTCHA_PROVIDER') ?? (App::env('RECAPTCHA_SITE_KEY') ? 'recaptcha-v3' : ''), 'recaptchaSiteKey' => '$RECAPTCHA_SITE_KEY', 'recaptchaSecretKey' => '$RECAPTCHA_SECRET_KEY', 'recaptchaScoreThreshold' => App::env('RECAPTCHA_SCORE_THRESHOLD') ?? 0.5, // Below this score: definite spam, rejected and not stored; between the // two thresholds: stored as spam for review 'recaptchaRejectThreshold' => App::env('RECAPTCHA_REJECT_THRESHOLD') ?? 0.3, 'recaptchaHideBadge' => App::env('RECAPTCHA_HIDE_BADGE') ?? true, // Only used by the recaptcha-enterprise provider (createAssessment API) 'recaptchaProjectId' => '$RECAPTCHA_PROJECT_ID', 'recaptchaApiKey' => '$RECAPTCHA_API_KEY', 'turnstileSiteKey' => '$TURNSTILE_SITE_KEY', 'turnstileSecretKey' => '$TURNSTILE_SECRET_KEY', ];
When recaptchaHideBadge is enabled, Google requires the reCAPTCHA attribution to be visible in the form, e.g.:
Protected by Google reCAPTCHA. The Google <a href="https://policies.google.com/privacy">Privacy Policy</a> and <a href="https://policies.google.com/terms">Terms of Service</a> apply.
Environment variables
Captcha keys live in .env:
# RECAPTCHA (leave empty to disable the captcha in local dev) # Use the *legacy secret key* from the Google Cloud console — see the # "Google reCAPTCHA deprecation notes" section below RECAPTCHA_SITE_KEY= RECAPTCHA_SECRET_KEY= #RECAPTCHA_SCORE_THRESHOLD=0.5 #RECAPTCHA_REJECT_THRESHOLD=0.3 #RECAPTCHA_HIDE_BADGE=true # Only for the recaptcha-enterprise provider (createAssessment API) #RECAPTCHA_PROJECT_ID= #RECAPTCHA_API_KEY= # TURNSTILE (Cloudflare, experimental alternative to reCAPTCHA) TURNSTILE_SITE_KEY= TURNSTILE_SECRET_KEY=
All other settings can optionally be overridden per environment:
#SECURE_FORMS_TO_EMAIL= #SECURE_FORMS_PREPEND_SUBJECT="New contact form submission" #SECURE_FORMS_NOTIFICATION_TEMPLATE=_emails/notifications/contact #SECURE_FORMS_CONFIRMATION_ENABLED=true #SECURE_FORMS_CONFIRMATION_TEMPLATE=_emails/confirmations/contact #SECURE_FORMS_SAVE_SUBMISSIONS=true #SECURE_FORMS_SAVE_SPAM_SUBMISSIONS=true #SECURE_FORMS_SPAM_ACTION=silent #SECURE_FORMS_HONEYPOT_ENABLED=true #SECURE_FORMS_HONEYPOT_PARAM=honeyPotProtection #SECURE_FORMS_CAPTCHA_PROVIDER=recaptcha-v3
Google reCAPTCHA deprecation notes
Google deprecated classic reCAPTCHA and migrated all keys to Google Cloud projects (automated migration completed Q1 2026; keys without a Cloud project have API access locked). What this means for this plugin:
- Key management — keys live in a Google Cloud project, created either in the Cloud console or programmatically via the reCAPTCHA Enterprise API (
projects.keys.createwithwebSettings.integrationTypeSCORE(v3) orCHECKBOX(v2) and theallowedDomainslist). The key ID is what goes intoRECAPTCHA_SITE_KEY. - Server-side verification — this plugin verifies tokens via the legacy
siteverifyendpoint, which Google keeps supported for migrated and Enterprise-created keys. Use the legacy secret key asRECAPTCHA_SECRET_KEY, available in the Cloud console under Integration → Use legacy key or via theretrieveLegacySecretKeyAPI method. - Domain allowlist — keep the key's
allowedDomainsin sync with the site's domains. A domain missing from the allowlist stops the widget from producing tokens, which this plugin reports as a visible error (never as silent spam). - Free quota — without a billing account (Essentials tier) reCAPTCHA includes 10,000 free assessments per month, aggregated across the whole Google Cloud organization: all sites and keys share the pool. Beyond it,
siteverifyfails open (returnssuccess: truewith a fixed0.9score instead of verifying), silently degrading spam protection. This plugin only generates assessments on actual form submits (not page views), so the quota maps 1:1 to submission attempts. See reCAPTCHA billing and tier comparison. - Shared quota warning (agencies) — with many client sites' keys in one Cloud project, one busy or bot-targeted site can exhaust the shared pool and silently disable spam filtering on every other site in the organization. Link a billing account or use the
recaptcha-enterpriseprovider to avoid this. - Billing — linking a billing account (Standard tier) keeps the same 10,000 free assessments but replaces the fail-open cliff with paid verification: a flat $8 covers up to 100,000 assessments/month, then $1 per 1,000.
- Enterprise API — Google's long-term replacement for
siteverifyis thecreateAssessmentAPI, supported by this plugin as therecaptcha-enterpriseprovider (requiresRECAPTCHA_PROJECT_ID+RECAPTCHA_API_KEY). It returns the real risk score and fails closed on quota (HTTP 429, surfaced as a visible error) instead of silently passing. - Cloudflare Turnstile — free, no assessment quota and no Google dependency; available as the
turnstileprovider (experimental) if moving away from reCAPTCHA entirely.
Failure handling & manual delivery
Every submission is stored before any email is attempted, so nothing is ever lost. What happens per failure mode:
| Failure | Visitor sees | Stored as | Admin alert |
|---|---|---|---|
| Definite spam (honeypot hit, score below the reject threshold) | Success by default (spamAction: silent) |
Not stored — rejected outright | Info log only |
| Gray-zone spam (score between reject and score thresholds, invalid/expired token) | Success by default (spamAction: silent) |
spam + score/reason |
Reviewable in the Submissions index (spam is not an error) |
| Captcha misconfiguration (missing keys, domain not allowlisted, API unreachable, Enterprise quota) | "Could not be verified" error | failed + cause in sendError |
Nav badge + error log (Sentry when installed) |
| SMTP / transport failure | "Could not be sent" error | failed + cause in sendError |
Nav badge + error log (Sentry when installed) |
Nav badge: the Secure Forms item in the control panel sidebar shows a badge with the number of submissions whose notification email never went out — visible from every CP page, no email dependency (an email alert would be useless when SMTP is the thing that's down).
Manual delivery: the submission detail view has a send button that covers both cases — Retry sending for failed submissions, and Not spam — send for reviewed spam (delivering it marks it as not spam; the score and reason are kept for the record). Re-scoring is impossible by design: captcha tokens are single-use and expire within minutes, so a stored submission can never be re-verified — a human decision replaces the score.
Email templates
Site templates receive submission (the element) and message (the decoded dynamic fields):
<p>{{ submission.fromName }} ({{ submission.fromEmail }}) wrote:</p> <p>{{ message.body|nl2br }}</p>
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: proprietary
- 更新时间: 2026-07-02