From 8985e5c24a1046fc103538ee7068194a2d136580 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Mon, 23 Jan 2023 21:50:10 +0530 Subject: [PATCH] Add hCaptcha.com support to public subscription form. (#1152) Bots easily bypass the simple `nonce` hack. This commit adds support for the hcaptcha.com widget. - New `Security` tab in the admin settings UI. - Enable/disable CAPTCHA. - Render CAPTCHA on the public subscription form. Closes #1116. --- cmd/init.go | 15 ++++ cmd/main.go | 3 + cmd/public.go | 20 ++++- cmd/settings.go | 4 + cmd/upgrade.go | 1 + frontend/src/views/Settings.vue | 11 +++ frontend/src/views/settings/security.vue | 42 ++++++++++ i18n/ca.json | 7 ++ i18n/cs-cz.json | 7 ++ i18n/cy.json | 7 ++ i18n/de.json | 7 ++ i18n/en.json | 7 ++ i18n/es.json | 7 ++ i18n/fi.json | 7 ++ i18n/fr.json | 7 ++ i18n/hu.json | 7 ++ i18n/it.json | 7 ++ i18n/jp.json | 7 ++ i18n/ml.json | 7 ++ i18n/nl.json | 7 ++ i18n/pl.json | 7 ++ i18n/pt-BR.json | 7 ++ i18n/pt.json | 7 ++ i18n/ro.json | 7 ++ i18n/ru.json | 7 ++ i18n/sk.json | 7 ++ i18n/tr.json | 7 ++ i18n/vi.json | 7 ++ i18n/zh-CN.json | 7 ++ i18n/zh-TW.json | 7 ++ internal/captcha/captcha.go | 76 +++++++++++++++++++ internal/migrations/v2.4.0.go | 23 ++++++ models/settings.go | 4 + schema.sql | 3 + static/public/static/style.css | 5 +- .../public/templates/subscription-form.html | 9 ++- 36 files changed, 374 insertions(+), 3 deletions(-) create mode 100644 frontend/src/views/settings/security.vue create mode 100644 internal/captcha/captcha.go create mode 100644 internal/migrations/v2.4.0.go diff --git a/cmd/init.go b/cmd/init.go index 0cb4e148..99b7929e 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -24,6 +24,7 @@ import ( "github.com/knadh/koanf/providers/posflag" "github.com/knadh/listmonk/internal/bounce" "github.com/knadh/listmonk/internal/bounce/mailbox" + "github.com/knadh/listmonk/internal/captcha" "github.com/knadh/listmonk/internal/i18n" "github.com/knadh/listmonk/internal/manager" "github.com/knadh/listmonk/internal/media" @@ -69,6 +70,11 @@ type constants struct { Exportable map[string]bool `koanf:"-"` DomainBlocklist map[string]bool `koanf:"-"` } `koanf:"privacy"` + Security struct { + EnableCaptcha bool `koanf:"enable_captcha"` + CaptchaKey string `koanf:"captcha_key"` + CaptchaSecret string `koanf:"captcha_secret"` + } `koanf:"security"` AdminUsername []byte `koanf:"admin_username"` AdminPassword []byte `koanf:"admin_password"` @@ -351,6 +357,9 @@ func initConstants() *constants { if err := ko.Unmarshal("privacy", &c.Privacy); err != nil { lo.Fatalf("error loading app.privacy config: %v", err) } + if err := ko.Unmarshal("security", &c.Security); err != nil { + lo.Fatalf("error loading app.security config: %v", err) + } if err := ko.UnmarshalWithConf("appearance", &c.Appearance, koanf.UnmarshalConf{FlatPaths: true}); err != nil { lo.Fatalf("error loading app.appearance config: %v", err) } @@ -735,6 +744,12 @@ func initHTTPServer(app *App) *echo.Echo { return srv } +func initCaptcha() *captcha.Captcha { + return captcha.New(captcha.Opt{ + CaptchaSecret: ko.String("security.captcha_secret"), + }) +} + func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) chan bool { // The blocking signal handler that main() waits on. out := make(chan bool) diff --git a/cmd/main.go b/cmd/main.go index 801a195c..e6b29301 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,6 +17,7 @@ import ( "github.com/knadh/koanf/providers/env" "github.com/knadh/listmonk/internal/bounce" "github.com/knadh/listmonk/internal/buflog" + "github.com/knadh/listmonk/internal/captcha" "github.com/knadh/listmonk/internal/core" "github.com/knadh/listmonk/internal/i18n" "github.com/knadh/listmonk/internal/manager" @@ -47,6 +48,7 @@ type App struct { i18n *i18n.I18n bounce *bounce.Manager paginator *paginator.Paginator + captcha *captcha.Captcha notifTpls *notifTpls log *log.Logger bufLog *buflog.BufLog @@ -168,6 +170,7 @@ func main() { messengers: make(map[string]messenger.Messenger), log: lo, bufLog: bufLog, + captcha: initCaptcha(), paginator: paginator.New(paginator.Opt{ DefaultPerPage: 20, diff --git a/cmd/public.go b/cmd/public.go index 3931a7b3..9c159172 100644 --- a/cmd/public.go +++ b/cmd/public.go @@ -79,7 +79,8 @@ type msgTpl struct { type subFormTpl struct { publicTpl - Lists []models.List + Lists []models.List + CaptchaKey string } var ( @@ -418,6 +419,10 @@ func handleSubscriptionFormPage(c echo.Context) error { out.Title = app.i18n.T("public.sub") out.Lists = lists + if app.constants.Security.EnableCaptcha { + out.CaptchaKey = app.constants.Security.CaptchaKey + } + return c.Render(http.StatusOK, "subscription-form", out) } @@ -433,6 +438,19 @@ func handleSubscriptionForm(c echo.Context) error { return echo.NewHTTPError(http.StatusBadGateway, app.i18n.T("public.invalidFeature")) } + // Process CAPTCHA. + if app.constants.Security.EnableCaptcha { + err, ok := app.captcha.Verify(c.FormValue("h-captcha-response")) + if err != nil { + app.log.Printf("Captcha request failed: %v", err) + } + + if !ok { + return c.Render(http.StatusBadRequest, tplMessage, + makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.invalidCaptcha"))) + } + } + hasOptin, err := processSubForm(c) if err != nil { e, ok := err.(*echo.HTTPError) diff --git a/cmd/settings.go b/cmd/settings.go index 5f86a290..06435f3f 100644 --- a/cmd/settings.go +++ b/cmd/settings.go @@ -44,6 +44,7 @@ func handleGetSettings(c echo.Context) error { } s.UploadS3AwsSecretAccessKey = "" s.SendgridKey = "" + s.SecurityCaptchaSecret = "" return c.JSON(http.StatusOK, okResp{s}) } @@ -158,6 +159,9 @@ func handleUpdateSettings(c echo.Context) error { if set.SendgridKey == "" { set.SendgridKey = cur.SendgridKey } + if set.SecurityCaptchaSecret == "" { + set.SecurityCaptchaSecret = cur.SecurityCaptchaSecret + } // Domain blocklist. doms := make([]string, 0) diff --git a/cmd/upgrade.go b/cmd/upgrade.go index a65bd0bc..48f2bd3f 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -34,6 +34,7 @@ var migList = []migFunc{ {"v2.1.0", migrations.V2_1_0}, {"v2.2.0", migrations.V2_2_0}, {"v2.3.0", migrations.V2_3_0}, + {"v2.4.0", migrations.V2_4_0}, } // upgrade upgrades the database to the current version by running SQL migration files diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index b395a5ff..8dad6d96 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -34,6 +34,10 @@ + + + + @@ -66,6 +70,7 @@ import { mapState } from 'vuex'; import GeneralSettings from './settings/general.vue'; import PerformanceSettings from './settings/performance.vue'; import PrivacySettings from './settings/privacy.vue'; +import SecuritySettings from './settings/security.vue'; import MediaSettings from './settings/media.vue'; import SmtpSettings from './settings/smtp.vue'; import BounceSettings from './settings/bounces.vue'; @@ -79,6 +84,7 @@ export default Vue.extend({ GeneralSettings, PerformanceSettings, PrivacySettings, + SecuritySettings, MediaSettings, SmtpSettings, BounceSettings, @@ -136,6 +142,10 @@ export default Vue.extend({ form['bounce.sendgrid_key'] = ''; } + if (form['security.captcha_secret'] === dummyPassword) { + form['security.captcha_secret'] = ''; + } + for (let i = 0; i < form.messengers.length; i += 1) { // If it's the dummy UI password placeholder, ignore it. if (form.messengers[i].password === dummyPassword) { @@ -203,6 +213,7 @@ export default Vue.extend({ d['upload.s3.aws_secret_access_key'] = dummyPassword; } d['bounce.sendgrid_key'] = dummyPassword; + d['security.captcha_secret'] = dummyPassword; // Domain blocklist array to multi-line string. d['privacy.domain_blocklist'] = d['privacy.domain_blocklist'].join('\n'); diff --git a/frontend/src/views/settings/security.vue b/frontend/src/views/settings/security.vue new file mode 100644 index 00000000..0faf837b --- /dev/null +++ b/frontend/src/views/settings/security.vue @@ -0,0 +1,42 @@ + + + diff --git a/i18n/ca.json b/i18n/ca.json index 2031da0a..98b2fbde 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "S'ha produït un error en obtenir les llistes. Si us plau, torna-ho a provar.", "public.errorProcessingRequest": "S'ha produït un error en processar la sol·licitud. Si us plau, torna-ho a provar.", "public.errorTitle": "Error", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Aquesta funció no està disponible.", "public.invalidLink": "Enllaç no vàlid", "public.managePrefs": "Gestiona les preferències", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Inclou capçaleres de cancel·lació de subscripció que permetin als clients de correu electrònic permetre als usuaris donar-se de baixa amb un sol clic.", "settings.privacy.name": "Privadesa", "settings.restart": "Reinicia", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Capçaleres personalitzades", "settings.smtp.customHeadersHelp": "Matriu opcional de capçaleres de correu electrònic per incloure en tots els missatges enviats des d'aquest servidor. p. ex.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Habilitat", diff --git a/i18n/cs-cz.json b/i18n/cs-cz.json index 4564bf99..5fd6f0e5 100644 --- a/i18n/cs-cz.json +++ b/i18n/cs-cz.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Chyba při načítání seznamů. Zopakujte pokus.", "public.errorProcessingRequest": "Chyba při zpracování požadavku. Zopakujte pokus.", "public.errorTitle": "Chyba", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Tato funkce není k dispozici.", "public.invalidLink": "Neplatný odkaz", "public.managePrefs": "Zpráva předvoleb", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Zahrnout záhlaví zrušení odběrů, která umožňují e-mailovým klientům, aby povolili uživatelům zrušit odběr jediným klepnutím.", "settings.privacy.name": "Soukromí", "settings.restart": "Restart", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Vlastní záhlaví", "settings.smtp.customHeadersHelp": "Volitelné pole e-mailových záhlaví, která se mají zahrnout do všech zpráv odeslaných z tohoto serveru. Např.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Povoleno", diff --git a/i18n/cy.json b/i18n/cy.json index b2191289..7a7ab4ca 100644 --- a/i18n/cy.json +++ b/i18n/cy.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Gwall wrth chwilio am y rhestrau. Rhowch gynnig arall arni.", "public.errorProcessingRequest": "Gwall wrth brosesu'r cais. Rhowch gynnig arall arni.", "public.errorTitle": "Gwall", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Nid yw'r nodwedd ar gael.", "public.invalidLink": "Dolen annilys", "public.managePrefs": "Rheoli dewisiadau", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Cynnwys penynnau dad-danysgrifio sy'n caniatáu i ddefnyddwyr dad-danysgrifio drwy glicio un botwm.", "settings.privacy.name": "Preifatrwydd", "settings.restart": "Ailgychwyn", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Penynnau personol", "settings.smtp.customHeadersHelp": "Ystod eang o bennynau e-bost i'w cynnwys mewn negeseuon a anfonir gan y gweinydd hwn. ee: [{\"\"X-Custom\"\": \"\"gwerth\"\"}", "settings.smtp.enabled": "Wedi galluogi", diff --git a/i18n/de.json b/i18n/de.json index 83d29f45..1b1988bc 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Fehler beim Abrufen der Listen. Bitte probiere es nochmal.", "public.errorProcessingRequest": "Fehler bei der Anfrage. Bitte probiere es nochmal.", "public.errorTitle": "Fehler", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Dieses Feature ist nicht verfügbar", "public.invalidLink": "Ungültiger Link", "public.managePrefs": "Einstellungen verwalten", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Inkludiere Header zum einfachen Abmelden in den E-Mails. Erlaubt es, den E-Mail Clients der Nutzer eine \",Ein Klick\"-Abmeldung anzubieten.", "settings.privacy.name": "Privatsphäre", "settings.restart": "Neustarten", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Benutzerdefinierte Header", "settings.smtp.customHeadersHelp": "(Optional) Array von benutzerdefinierten E-Mail Headern, welche in die Nachricht eingefügt werden sollen. Z.B.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Aktiviert", diff --git a/i18n/en.json b/i18n/en.json index 2d8a3ae8..140bbfb0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Error fetching lists. Please retry.", "public.errorProcessingRequest": "Error processing request. Please retry.", "public.errorTitle": "Error", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "That feature is not available.", "public.invalidLink": "Invalid link", "public.managePrefs": "Manage preferences", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Include unsubscription headers that allow e-mail clients to allow users to unsubscribe in a single click.", "settings.privacy.name": "Privacy", "settings.restart": "Restart", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Custom headers", "settings.smtp.customHeadersHelp": "Optional array of e-mail headers to include in all messages sent from this server. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Enabled", diff --git a/i18n/es.json b/i18n/es.json index 9901b9cf..98a6c9c2 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Error obteniendo listas. Por favor intente nuevamente.", "public.errorProcessingRequest": "Error al procesar la petición. Por favor intente nuevamente.", "public.errorTitle": "Error", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Esta función no está disponible", "public.invalidLink": "Enlace inválido", "public.managePrefs": "Gestionar las preferencias", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Incluye los encabezados de darse de baja para habilitar a los clientes de correo para permitir a los usuarios darse de baja con un solo clic.", "settings.privacy.name": "Privacidad", "settings.restart": "Reiniciar", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Encabezados personalizados", "settings.smtp.customHeadersHelp": "Lista de encabezados opcionales a incluir en todos los mensajes enviados desde este servidor. Por ejemplo {{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Habilitado", diff --git a/i18n/fi.json b/i18n/fi.json index 8eb1d53d..30b99534 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Virhe noutaessa postituslistoja. Ole hyvä ja yritä uudestaan.", "public.errorProcessingRequest": "Virhe käsitellessä pyyntöäsi. Ole hyvä ja yritä uudestaan.", "public.errorTitle": "Tapahtui virhe", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Tämä ominaisuus ei ole saatavilla.", "public.invalidLink": "Virheellinen linkki", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Include unsubscription headers that allow e-mail clients to allow users to unsubscribe in a single click.", "settings.privacy.name": "Yksityisyys", "settings.restart": "Restart", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Custom headers", "settings.smtp.customHeadersHelp": "Optional array of e-mail headers to include in all messages sent from this server. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Enabled", diff --git a/i18n/fr.json b/i18n/fr.json index e604e08f..41dbb05b 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Erreur lors de la récupération des listes. Veuillez réessayer.", "public.errorProcessingRequest": "Erreur lors du traitement de la demande. Veuillez réessayer.", "public.errorTitle": "Erreur", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Cette fonctionnalité n'est pas disponible.", "public.invalidLink": "Lien invalide", "public.managePrefs": "Gérer les préférences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Inclure des en-têtes de désabonnement qui permettent aux utilisateurs de se désabonner en un seul clic depuis leur client de messagerie.", "settings.privacy.name": "Vie privée", "settings.restart": "Redémarrer", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "En-têtes personnalisées", "settings.smtp.customHeadersHelp": "Tableau facultatif d'en-têtes à inclure dans tous les e-mails envoyés depuis ce serveur. Par exemple : [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Activé", diff --git a/i18n/hu.json b/i18n/hu.json index 1d1a1791..52a1d784 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Hiba a listák lekérésekor. Kérjük, próbálja újra.", "public.errorProcessingRequest": "Hiba a kérelem feldolgozásakor. Kérjük, próbálja újra.", "public.errorTitle": "Hiba", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Ez a funkció nem elérhető.", "public.invalidLink": "A link érvénytelen", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Tartalmazzon leiratkozási fejléceket, amelyek lehetővé teszik az e-mail kliensek számára, hogy a felhasználók egyetlen kattintással leiratkozhassanak.", "settings.privacy.name": "Magánélet", "settings.restart": "Újraindítás", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Egyéni fejlécek", "settings.smtp.customHeadersHelp": "Az e-mail fejlécek opcionális tömbje a szerverről küldött összes üzenetben. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Engedélyez", diff --git a/i18n/it.json b/i18n/it.json index e2d5eae0..e9259df4 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Errore durante il recupero delle liste. Per favore, riprova.", "public.errorProcessingRequest": "Errore durante la gestione della richiesta. Per favore, riprova.", "public.errorTitle": "Errore", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Questa funzione non è disponibile.", "public.invalidLink": "Link non valido", "public.managePrefs": "Gestiona l'impostazinoni", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Includere intestazioni di annullamento dell'iscrizione che consentono agli utenti di annullare l'iscrizione con un clic dal proprio client di posta elettronica.", "settings.privacy.name": "Privacy", "settings.restart": "Riavviare", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Intestazioni personalizzate", "settings.smtp.customHeadersHelp": "Matrice facoltativa di intestazioni di posta elettronica da includere in tutti i messaggi inviati da questo server. Ad esempio: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Attivata", diff --git a/i18n/jp.json b/i18n/jp.json index cbc2e7bb..d251d645 100644 --- a/i18n/jp.json +++ b/i18n/jp.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "リストの取得にエラーがありました。再試行してください。", "public.errorProcessingRequest": "リクエスト中にエラーがありました。再試行してください。", "public.errorTitle": "エラー", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "その機能は使用できません。", "public.invalidLink": "無効なリンク", "public.managePrefs": "設定変更", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "メールクライアントがワンクリックで登録解除をできるように登録解除用のヘッダーを含める。", "settings.privacy.name": "プライバシー", "settings.restart": "再起動", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "カスタムヘッダー", "settings.smtp.customHeadersHelp": "このサーバーから送信する全てのメッセージに含まれる任意のメールヘッダーの配列。 例: [{\"X-カスタム\": \"バリュー\"}, {\"X-カスタム2\": \"バリュー\"}]", "settings.smtp.enabled": "有効", diff --git a/i18n/ml.json b/i18n/ml.json index 794affa9..da636f01 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "ലിസ്റ്റുകൾ വീണ്ടെടുക്കുന്നതിൽ തടസം നേരിട്ടു. വീണ്ടും ശ്രമിക്കുക.", "public.errorProcessingRequest": "അഭ്യർത്ഥനയിന്മേൽ നടപടിയെടുക്കുന്നതിൽ തടസം നേരിട്ടു. വീണ്ടും ശ്രമിക്കുക.", "public.errorTitle": "എറർ", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "ഈ ഫീച്ചർ ലഭ്യമല്ല", "public.invalidLink": "കണ്ണി അസാധുവാണ്", "public.managePrefs": "മുൻഗണനകളിൽ മാറ്റം വരുത്തുക", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "ഒറ്റ ക്ലിക്കിലൂടെ വരിക്കാനല്ലാതാക്കാൻ ഇ-മെയിൽ ക്ലൈന്റിൽ വരിക്കാരനല്ലാതാക്കാനുള്ള തലക്കെട്ട് കൂട്ടിച്ചേർക്കുക.", "settings.privacy.name": "സ്വകാര്യത", "settings.restart": "പുനരാരംഭിയ്ക്കുക", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ", "settings.smtp.customHeadersHelp": "ഈ സേർവറിൽ നിന്നും അയക്കുന്ന എല്ലാ ഈ-മെയിലിലും ഉണ്ടാകേണ്ട ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ. ഉദാഹരണം: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "പ്രവർത്തനക്ഷമമാക്കി", diff --git a/i18n/nl.json b/i18n/nl.json index 1578f588..5cda4c28 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Fout bij ophalen lijsten. Probeer opnieuw.", "public.errorProcessingRequest": "Fout bij behandelen verzoek. Probeer opnieuw.", "public.errorTitle": "Fout", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Deze functie is niet beschikbaar", "public.invalidLink": "Ongeldige link", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Voeg header toe zodat e-mailprogramma's gebruikers zich kunnen laten uitschrijven in een klik.", "settings.privacy.name": "Privacy", "settings.restart": "Herstarten", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Aangepaste headers", "settings.smtp.customHeadersHelp": "Optionele lijst met e-mail headers om toe te voegen aan alle berichten van deze server. Bv.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Ingeschakeld", diff --git a/i18n/pl.json b/i18n/pl.json index 628893e2..d1a2369e 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Błąd pobierania list. Spróbuj ponownie.", "public.errorProcessingRequest": "Błąd przetwarzania żądania. Spróbuj ponownie.", "public.errorTitle": "Błąd", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Ta funkcjonalność jest niedostępna.", "public.invalidLink": "Nieprawidłowy liny.", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Dodaj nagłówki do wypisania się z subskrypcji. Niektóre programy pocztowe umożliwiają wypisanie się jednym kliknięciem.", "settings.privacy.name": "Prywatność", "settings.restart": "Restart", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Niestandardowe nagłówki", "settings.smtp.customHeadersHelp": "Opcjonalna lista nagłówków do zamieszczania w wiadomościach we wszystkich wiadomościach wysłanych z tego serwera. np: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Włączone", diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 61c77b39..b966f256 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Erro ao obter as listas. Por favor, tente novamente.", "public.errorProcessingRequest": "Erro ao processar a solicitação. Por favor, tente novamente.", "public.errorTitle": "Erro", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Este recurso não está disponível.", "public.invalidLink": "Link inválido", "public.managePrefs": "Gerenciar preferências", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Incluir cabeçalhos de desinscrição que permitem aos clientes de e-mail cancelem a inscrição em um único clique.", "settings.privacy.name": "Privacidade", "settings.restart": "Reiniciar", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Cabeçalhos personalizados", "settings.smtp.customHeadersHelp": "Array opcional de cabeçalhos de e-mail para incluir em todas as mensagens enviadas a partir deste servidor. por exemplo: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Habilitado", diff --git a/i18n/pt.json b/i18n/pt.json index d55fe2d6..5a5ba934 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Erro ao carregar listas. Por favor tente novamente.", "public.errorProcessingRequest": "Erro ao processar pedido. Por favor tente novamente.", "public.errorTitle": "Erro", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "That feature is not available", "public.invalidLink": "Link inválido", "public.managePrefs": "Gerir preferências", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Incluir headers de cancelamento de subscrição que permite aos clientes de email permitir ao utilizadores cancelar a subscrição num único clique.", "settings.privacy.name": "Privacidade", "settings.restart": "Reiniciar", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Headers customizados", "settings.smtp.customHeadersHelp": "Array opcional de headers de email a incluir em todas as mensagens enviadas deste servidor. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Ativo", diff --git a/i18n/ro.json b/i18n/ro.json index 6c7a5291..4f96a190 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Eroare la preluarea listelor. Încearcă din nou.", "public.errorProcessingRequest": "Eroare la procesarea cererii. Încearcă din nou.", "public.errorTitle": "Eroare", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Această functie nu este disponibilă", "public.invalidLink": "Link invalid", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Include anteturi de dezabonare care permit clienților de e-mail să permită utilizatorilor să se dezaboneze printr-un singur clic.", "settings.privacy.name": "Confidențialitate", "settings.restart": "Restart", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Anteturi personalizate", "settings.smtp.customHeadersHelp": "Matrice opțională de antete de e-mail pentru a include în toate mesajele trimise de pe acest server. de exemplu: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Activat", diff --git a/i18n/ru.json b/i18n/ru.json index a3ed7319..0d4c9181 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Ошибка получения списков. Пожалуйста, повторите.", "public.errorProcessingRequest": "Ошибка обработки запроса. Пожалуйста, повторите.", "public.errorTitle": "Ошибка", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Эта функция недоступна.", "public.invalidLink": "Неверная ссылка", "public.managePrefs": "Параметры письма", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Включать заголовок отписки", "settings.privacy.name": "Конфиденциальност", "settings.restart": "Перезапустить", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Настраиваемые заголовки", "settings.smtp.customHeadersHelp": "Необязательный массив заголовков e-mail, которые будут включены во все письма, отправляемые с этого сервера. Например: [{\"X-Custom\": \"значение\"}, {\"X-Custom2\": \"значение\"}]", "settings.smtp.enabled": "Включено", diff --git a/i18n/sk.json b/i18n/sk.json index c45ed638..4c1b6508 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "Chyba pri načítání zoznamov. Zopakujte pokus.", "public.errorProcessingRequest": "Chyba pri spracovaní požiadavky. Zopakujte pokus.", "public.errorTitle": "Chyba", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Táto funkcia nie je k dispozícii.", "public.invalidLink": "Neplatný odkaz", "public.managePrefs": "Správa predvolieb", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "Nastaví hlavičku zrušenia odberov, ktorá umožňuje e-mailovým klientom, aby povolili používateľom zrušiť odber jedným kliknutím.", "settings.privacy.name": "Súkromie", "settings.restart": "Restarť", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Vlastné hlavičky", "settings.smtp.customHeadersHelp": "Voliteľné polia e-mailových hlavičiek, ktorá sa majú nastaviť do všetkých správ odoslaných z tohoto servera. Napr.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Zapnuté", diff --git a/i18n/tr.json b/i18n/tr.json index f6d7550f..ec42798a 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Listeleri getirme hatası. Lütfen tekrarla.", "public.errorProcessingRequest": "İstek işleme hatası. Lütfen tekrarla.", "public.errorTitle": "Hata", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Bu özellik geçerli değil.", "public.invalidLink": "Geçersiz link", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "E-posta istemcilerinin kullanıcıların tek bir tıklamayla abonelikten çıkmalarına olanak tanıyan abonelik iptal başlıklarını ekleyin.", "settings.privacy.name": "Gizlilik", "settings.restart": "Yeniden başlat", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Özel başlık bilgisi", "settings.smtp.customHeadersHelp": "Bu sunucudan gönderilen tüm iletilere eklenecek isteğe bağlı e-posta başlıkları dizisi. Örnek: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Etkinleştirildi", diff --git a/i18n/vi.json b/i18n/vi.json index 6f5754b1..a4072437 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "Lỗi khi tìm nạp danh sách. Xin hãy thử lại.", "public.errorProcessingRequest": "Lỗi khi xử lý yêu cầu. Xin hãy thử lại.", "public.errorTitle": "Lỗi", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "Tính năng đó không khả dụng.", "public.invalidLink": "Link không khả dụng", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "Bao gồm các tiêu đề hủy đăng ký cho phép ứng dụng e-mail cho phép người dùng hủy đăng ký chỉ bằng một cú nhấp chuột.", "settings.privacy.name": "Sự riêng tư", "settings.restart": "Khởi động lại", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "Tiêu đề tùy chỉnh", "settings.smtp.customHeadersHelp": "Mảng tiêu đề e-mail tùy chọn để bao gồm trong tất cả các thư được gửi từ máy chủ này. ví dụ: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "Đã bật", diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 23fd11f7..8ae253f1 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -304,6 +304,7 @@ "public.errorFetchingLists": "获取列表时出错。请重试。", "public.errorProcessingRequest": "处理请求时出错。请重试。", "public.errorTitle": "错误", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "该功能不可用。", "public.invalidLink": "无效的链接", "public.managePrefs": "管理偏好设置", @@ -473,6 +474,12 @@ "settings.privacy.listUnsubHeaderHelp": "包括允许电子邮件客户端允许用户通过单击取消订阅的取消订阅标题", "settings.privacy.name": "隐私", "settings.restart": "重新开始", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "自定义标头", "settings.smtp.customHeadersHelp": "要包含在从此服务器发送的所有消息中的可选电子邮件标头数组。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "已启用", diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index cdfac473..fa25946f 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -305,6 +305,7 @@ "public.errorFetchingLists": "獲取列表時出錯。請重試。", "public.errorProcessingRequest": "處理請求時出錯。請重試。", "public.errorTitle": "錯誤", + "public.invalidCaptcha": "Invalid CAPTCHA.", "public.invalidFeature": "該功能不可用。", "public.invalidLink": "無效的鏈接", "public.managePrefs": "Manage preferences", @@ -474,6 +475,12 @@ "settings.privacy.listUnsubHeaderHelp": "包括允許電子郵件客戶端允許用戶通過單擊取消訂閱的取消訂閱標題", "settings.privacy.name": "隱私", "settings.restart": "重新開始", + "settings.security.captchaKey": "hCaptcha.com SiteKey", + "settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.", + "settings.security.captchaSecret": "hCaptcha.com secret", + "settings.security.enableCaptcha": "Enable CAPTCHA", + "settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.", + "settings.security.name": "Security", "settings.smtp.customHeaders": "自定義標頭", "settings.smtp.customHeadersHelp": "要包含在從此服務器發送的所有消息中的可選電子郵件標頭數組。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.enabled": "已啟用", diff --git a/internal/captcha/captcha.go b/internal/captcha/captcha.go new file mode 100644 index 00000000..92d7c9bd --- /dev/null +++ b/internal/captcha/captcha.go @@ -0,0 +1,76 @@ +package captcha + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + rootURL = "https://hcaptcha.com/siteverify" +) + +type captchaResp struct { + Success bool `json:"success"` + ErrorCodes []string `json:"error_codes"` +} + +// Captcha is a simple Captcha client. +// It currently implements hcaptcha.com +type Captcha struct { + o Opt + client *http.Client +} + +type Opt struct { + CaptchaSecret string `json:"captcha_secret"` +} + +// New returns a new instance of the HTTP CAPTCHA client. +func New(o Opt) *Captcha { + timeout := time.Second * 5 + + return &Captcha{ + o: o, + client: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + MaxIdleConnsPerHost: 10, + MaxConnsPerHost: 100, + ResponseHeaderTimeout: timeout, + IdleConnTimeout: timeout, + }, + }} +} + +// Verify veries a CAPTCHA request. +func (c *Captcha) Verify(token string) (error, bool) { + resp, err := c.client.PostForm(rootURL, url.Values{ + "secret": {c.o.CaptchaSecret}, + "response": {token}, + }) + if err != nil { + return err, false + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err, false + } + + var r captchaResp + if json.Unmarshal(body, &r); err != nil { + return err, true + } + + if r.Success != true { + return fmt.Errorf("captcha failed: %s", strings.Join(r.ErrorCodes, ",")), false + } + + return nil, true +} diff --git a/internal/migrations/v2.4.0.go b/internal/migrations/v2.4.0.go new file mode 100644 index 00000000..85e76e3f --- /dev/null +++ b/internal/migrations/v2.4.0.go @@ -0,0 +1,23 @@ +package migrations + +import ( + "github.com/jmoiron/sqlx" + "github.com/knadh/koanf" + "github.com/knadh/stuffbin" +) + +// V2_4_0 performs the DB migrations. +func V2_4_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error { + // Insert new preference settings. + if _, err := db.Exec(` + INSERT INTO settings (key, value) VALUES + ('security.enable_captcha', 'false'), + ('security.captcha_key', '""'), + ('security.captcha_secret', '""') + ON CONFLICT DO NOTHING; + `); err != nil { + return err + } + + return nil +} diff --git a/models/settings.go b/models/settings.go index 3b8d75d8..22ad4efe 100644 --- a/models/settings.go +++ b/models/settings.go @@ -32,6 +32,10 @@ type Settings struct { PrivacyExportable []string `json:"privacy.exportable"` DomainBlocklist []string `json:"privacy.domain_blocklist"` + SecurityEnableCaptcha bool `json:"security.enable_captcha"` + SecurityCaptchaKey string `json:"security.captcha_key"` + SecurityCaptchaSecret string `json:"security.captcha_secret"` + UploadProvider string `json:"upload.provider"` UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"` UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"` diff --git a/schema.sql b/schema.sql index 35e0778f..18214fa5 100644 --- a/schema.sql +++ b/schema.sql @@ -208,6 +208,9 @@ INSERT INTO settings (key, value) VALUES ('privacy.allow_preferences', 'true'), ('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'), ('privacy.domain_blocklist', '[]'), + ('security.enable_captcha', 'false'), + ('security.captcha_key', '""'), + ('security.captcha_secret', '""'), ('upload.provider', '"filesystem"'), ('upload.filesystem.upload_path', '"uploads"'), ('upload.filesystem.upload_uri', '"/uploads"'), diff --git a/static/public/static/style.css b/static/public/static/style.css index 5075320e..87c617c3 100644 --- a/static/public/static/style.css +++ b/static/public/static/style.css @@ -122,7 +122,7 @@ input[disabled] { .lists { list-style-type: none; padding: 0; - margin-bottom: 30px; + margin: 40px 0; } .lists li { margin: 0 0 5px 0; @@ -137,6 +137,9 @@ input[disabled] { .form .nonce { display: none; } + .form .captcha { + margin-top: 30px; + } .archive { list-style-type: none; diff --git a/static/public/templates/subscription-form.html b/static/public/templates/subscription-form.html index 22669be4..a99e034f 100644 --- a/static/public/templates/subscription-form.html +++ b/static/public/templates/subscription-form.html @@ -15,7 +15,7 @@

-
+ + + {{ if .Data.CaptchaKey }} +
+
+ +
+ {{ end }}