Feat: Replace Google ReCAPTCHA with ALTCHA using API - non-tracking, GDPR compliant without cookies or fingerprinting.

This commit is contained in:
2025-03-13 15:36:41 +00:00
parent 29205de12f
commit b843849af9
12 changed files with 2856 additions and 722 deletions

View File

@@ -81,9 +81,15 @@ class Config:
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
MAIL_CONTACT_PUBLIC = os.getenv('MAIL_CONTACT_PUBLIC')
"""
# Recaptcha
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
"""
# ALTCHA
ALTCHA_API_KEY = os.getenv('ALTCHA_API_KEY')
ALTCHA_SECRET_KEY = os.getenv('ALTCHA_SECRET_KEY')
ALTCHA_REGION = 'eu'
class DevelopmentConfig(Config):
is_development = True

View File

@@ -18,14 +18,17 @@ from models.model_view_contact import Model_View_Contact
from models.model_view_home import Model_View_Home
import lib.argument_validation as av
# external
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session, Blueprint, current_app
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session, Blueprint, current_app, flash
from flask_mail import Mail, Message
from extensions import db, oauth, mail
from urllib.parse import quote_plus, urlencode
from authlib.integrations.flask_client import OAuth
from authlib.integrations.base_client import OAuthError
from urllib.parse import quote, urlparse, parse_qs
import json
import base64
import hmac
import hashlib
routes_core = Blueprint('routes_core', __name__)
@@ -54,7 +57,32 @@ def contact_post():
try:
form = Form_Contact()
if form.validate_on_submit():
# Handle form submission
altcha_payload = form.altcha.data
if not altcha_payload:
flash('Please complete the ALTCHA challenge', 'danger')
return "Invalid. ALTCHA challenge failed."
# Decode and verify the ALTCHA payload
try:
decoded_payload = json.loads(base64.b64decode(altcha_payload))
# Verify the signature
if verify_altcha_signature(decoded_payload):
# Parse the verification data
verification_data = urllib.parse.parse_qs(decoded_payload['verificationData'])
# Check if the verification was successful
if verification_data.get('verified', ['false'])[0] == 'true':
# If spam filter is enabled, check the classification
if 'classification' in verification_data:
classification = verification_data.get('classification', [''])[0]
score = float(verification_data.get('score', ['0'])[0])
# If the classification is BAD and score is high, reject the submission
if classification == 'BAD' and score > 5:
flash('Your submission was flagged as potential spam', 'error')
return render_template('contact.html', form=form)
# Process the form submission
email = form.email.data
CC = form.CC.data # not in use
contact_name = form.contact_name.data
@@ -66,8 +94,45 @@ def contact_post():
mailItem = Message("PARTS Website Contact Us Message", recipients=[current_app.config['MAIL_CONTACT_PUBLIC']])
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n{receive_marketing_text}\nKind regards,\n{contact_name}\n{company_name}\n{email}"
mail.send(mailItem)
flash('Thank you for your message. We will get back to you soon!', 'success')
return "Submitted."
else:
flash('CAPTCHA verification failed', 'error')
else:
flash('Invalid verification signature', 'error')
except Exception as e:
flash(f'Error verifying CAPTCHA: {str(e)}', 'error')
return "Invalid. Failed to submit."
# html_body = render_template('pages/core/_contact.html', model = model)
except Exception as e:
return jsonify(error=str(e)), 403
def verify_altcha_signature(payload):
"""Verify the ALTCHA signature"""
if 'algorithm' not in payload or 'signature' not in payload or 'verificationData' not in payload:
return False
algorithm = payload['algorithm']
signature = payload['signature']
verification_data = payload['verificationData']
# Calculate SHA hash of the verification data
if algorithm == 'SHA-256':
hash_func = hashlib.sha256
else:
# Fallback to SHA-256 if algorithm not specified
hash_func = hashlib.sha256
# Calculate the hash of verification_data
data_hash = hash_func(verification_data.encode('utf-8')).digest()
# Calculate the HMAC signature
calculated_signature = hmac.new(
current_app.config["ALTCHA_SECRET_KEY"].encode('utf-8'),
data_hash,
hash_func
).hexdigest()
# Compare the calculated signature with the provided signature
return hmac.compare_digest(calculated_signature, signature)

View File

@@ -16,12 +16,63 @@ Defines Flask-WTF form for handling user input on Contact Us page.
# from models.model_view_store import Model_View_Store # circular
from forms.base import Form_Base
# external
from flask import Flask, render_template, request, flash, redirect, url_for, current_app
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, BooleanField, IntegerField, SelectField, FloatField
from wtforms.validators import InputRequired, NumberRange, Regexp, DataRequired, Optional
from wtforms import StringField, TextAreaField, SubmitField, HiddenField, BooleanField
from wtforms.validators import DataRequired, Email, ValidationError
from flask_wtf.recaptcha import RecaptchaField
from abc import ABCMeta, abstractmethod
import requests
import json
import hmac
import hashlib
import base64
import urllib.parse
"""
def validate_altcha(form, field):
if not field.data:
raise ValidationError('Please complete the ALTCHA challenge')
try:
# Decode the base64-encoded payload
payload_json = base64.b64decode(field.data).decode('utf-8')
payload = json.loads(payload_json)
# Verify ALTCHA response
if not payload.get('verified', False):
raise ValidationError('ALTCHA verification failed')
# Verify signature
verification_data = payload.get('verificationData', '')
received_signature = payload.get('signature', '')
algorithm = payload.get('algorithm', 'SHA-256')
# Calculate the hash of verification data
verification_hash = hashlib.sha256(verification_data.encode()).digest()
# Calculate HMAC signature
hmac_key = current_app.config['ALTCHA_SECRET_KEY'].encode()
calculated_signature = hmac.new(
hmac_key,
verification_hash,
getattr(hashlib, algorithm.lower().replace('-', ''))
).hexdigest()
if calculated_signature != received_signature:
raise ValidationError('Invalid ALTCHA signature')
# Optional: If using the spam filter, you could parse verification_data
# and reject submissions classified as spam
# Example:
parsed_data = dict(urllib.parse.parse_qsl(verification_data))
if parsed_data.get('classification') == 'BAD':
raise ValidationError('This submission was classified as spam')
except Exception as e:
current_app.logger.error(f"ALTCHA validation error: {str(e)}")
raise ValidationError('ALTCHA validation failed')
"""
class Form_Contact(FlaskForm):
email = StringField('Email')
@@ -29,5 +80,6 @@ class Form_Contact(FlaskForm):
company_name = StringField('Company')
message = TextAreaField('Message')
receive_marketing = BooleanField('I would like to receive marketing emails.')
recaptcha = RecaptchaField()
# recaptcha = RecaptchaField()
altcha = HiddenField('ALTCHA') # , validators=[validate_altcha]
submit = SubmitField('Send Message')

View File

@@ -133,7 +133,7 @@ class Model_View_Base(BaseModel, ABC):
FLAG_PAGE_BODY: ClassVar[str] = 'page-body'
FLAG_PHONE_NUMBER: ClassVar[str] = Base.FLAG_PHONE_NUMBER
FLAG_POSTCODE: ClassVar[str] = Base.FLAG_POSTCODE
FLAG_RECAPTCHA: ClassVar[str] = 'recaptcha'
FLAG_CAPTCHA: ClassVar[str] = 'recaptcha'
FLAG_RIGHT_HAND_SIDE: ClassVar[str] = 'rhs'
FLAG_ROW: ClassVar[str] = 'row'
FLAG_ROW_NEW: ClassVar[str] = 'row-new'

View File

@@ -26,6 +26,7 @@ class Model_View_Contact(Model_View_Base):
FLAG_COMPANY_NAME: ClassVar[str] = 'company_name'
FLAG_CONTACT_NAME: ClassVar[str] = 'contact_name'
FLAG_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
ID_CONTACT_FORM: ClassVar[str] = 'contact-form'
"""
ID_EMAIL: ClassVar[str] = 'email'
ID_COMPANY_NAME: ClassVar[str] = 'company_name'

View File

@@ -14,3 +14,4 @@ authlib
pydantic
# psycopg2
requests
cryptography

View File

@@ -1,603 +1,7 @@
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ 431:
/***/ (function(module) {
!function (e, t) {
true ? module.exports = t() : 0;
}(this, function () {
return function (e) {
function t(o) {
if (n[o]) return n[o].exports;
var i = n[o] = {
exports: {},
id: o,
loaded: !1
};
return e[o].call(i.exports, i, i.exports, t), i.loaded = !0, i.exports;
}
var n = {};
return t.m = e, t.c = n, t.p = "dist/", t(0);
}([function (e, t, n) {
"use strict";
function o(e) {
return e && e.__esModule ? e : {
default: e
};
}
var i = Object.assign || function (e) {
for (var t = 1; t < arguments.length; t++) {
var n = arguments[t];
for (var o in n) Object.prototype.hasOwnProperty.call(n, o) && (e[o] = n[o]);
}
return e;
},
r = n(1),
a = (o(r), n(6)),
u = o(a),
c = n(7),
s = o(c),
f = n(8),
d = o(f),
l = n(9),
p = o(l),
m = n(10),
b = o(m),
v = n(11),
y = o(v),
g = n(14),
h = o(g),
w = [],
k = !1,
x = {
offset: 120,
delay: 0,
easing: "ease",
duration: 400,
disable: !1,
once: !1,
startEvent: "DOMContentLoaded",
throttleDelay: 99,
debounceDelay: 50,
disableMutationObserver: !1
},
j = function () {
var e = arguments.length > 0 && void 0 !== arguments[0] && arguments[0];
if (e && (k = !0), k) return w = (0, y.default)(w, x), (0, b.default)(w, x.once), w;
},
O = function () {
w = (0, h.default)(), j();
},
M = function () {
w.forEach(function (e, t) {
e.node.removeAttribute("data-aos"), e.node.removeAttribute("data-aos-easing"), e.node.removeAttribute("data-aos-duration"), e.node.removeAttribute("data-aos-delay");
});
},
S = function (e) {
return e === !0 || "mobile" === e && p.default.mobile() || "phone" === e && p.default.phone() || "tablet" === e && p.default.tablet() || "function" == typeof e && e() === !0;
},
_ = function (e) {
x = i(x, e), w = (0, h.default)();
var t = document.all && !window.atob;
return S(x.disable) || t ? M() : (x.disableMutationObserver || d.default.isSupported() || (console.info('\n aos: MutationObserver is not supported on this browser,\n code mutations observing has been disabled.\n You may have to call "refreshHard()" by yourself.\n '), x.disableMutationObserver = !0), document.querySelector("body").setAttribute("data-aos-easing", x.easing), document.querySelector("body").setAttribute("data-aos-duration", x.duration), document.querySelector("body").setAttribute("data-aos-delay", x.delay), "DOMContentLoaded" === x.startEvent && ["complete", "interactive"].indexOf(document.readyState) > -1 ? j(!0) : "load" === x.startEvent ? window.addEventListener(x.startEvent, function () {
j(!0);
}) : document.addEventListener(x.startEvent, function () {
j(!0);
}), window.addEventListener("resize", (0, s.default)(j, x.debounceDelay, !0)), window.addEventListener("orientationchange", (0, s.default)(j, x.debounceDelay, !0)), window.addEventListener("scroll", (0, u.default)(function () {
(0, b.default)(w, x.once);
}, x.throttleDelay)), x.disableMutationObserver || d.default.ready("[data-aos]", O), w);
};
e.exports = {
init: _,
refresh: j,
refreshHard: O
};
}, function (e, t) {},,,,, function (e, t) {
(function (t) {
"use strict";
function n(e, t, n) {
function o(t) {
var n = b,
o = v;
return b = v = void 0, k = t, g = e.apply(o, n);
}
function r(e) {
return k = e, h = setTimeout(f, t), M ? o(e) : g;
}
function a(e) {
var n = e - w,
o = e - k,
i = t - n;
return S ? j(i, y - o) : i;
}
function c(e) {
var n = e - w,
o = e - k;
return void 0 === w || n >= t || n < 0 || S && o >= y;
}
function f() {
var e = O();
return c(e) ? d(e) : void (h = setTimeout(f, a(e)));
}
function d(e) {
return h = void 0, _ && b ? o(e) : (b = v = void 0, g);
}
function l() {
void 0 !== h && clearTimeout(h), k = 0, b = w = v = h = void 0;
}
function p() {
return void 0 === h ? g : d(O());
}
function m() {
var e = O(),
n = c(e);
if (b = arguments, v = this, w = e, n) {
if (void 0 === h) return r(w);
if (S) return h = setTimeout(f, t), o(w);
}
return void 0 === h && (h = setTimeout(f, t)), g;
}
var b,
v,
y,
g,
h,
w,
k = 0,
M = !1,
S = !1,
_ = !0;
if ("function" != typeof e) throw new TypeError(s);
return t = u(t) || 0, i(n) && (M = !!n.leading, S = "maxWait" in n, y = S ? x(u(n.maxWait) || 0, t) : y, _ = "trailing" in n ? !!n.trailing : _), m.cancel = l, m.flush = p, m;
}
function o(e, t, o) {
var r = !0,
a = !0;
if ("function" != typeof e) throw new TypeError(s);
return i(o) && (r = "leading" in o ? !!o.leading : r, a = "trailing" in o ? !!o.trailing : a), n(e, t, {
leading: r,
maxWait: t,
trailing: a
});
}
function i(e) {
var t = "undefined" == typeof e ? "undefined" : c(e);
return !!e && ("object" == t || "function" == t);
}
function r(e) {
return !!e && "object" == ("undefined" == typeof e ? "undefined" : c(e));
}
function a(e) {
return "symbol" == ("undefined" == typeof e ? "undefined" : c(e)) || r(e) && k.call(e) == d;
}
function u(e) {
if ("number" == typeof e) return e;
if (a(e)) return f;
if (i(e)) {
var t = "function" == typeof e.valueOf ? e.valueOf() : e;
e = i(t) ? t + "" : t;
}
if ("string" != typeof e) return 0 === e ? e : +e;
e = e.replace(l, "");
var n = m.test(e);
return n || b.test(e) ? v(e.slice(2), n ? 2 : 8) : p.test(e) ? f : +e;
}
var c = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) {
return typeof e;
} : function (e) {
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e;
},
s = "Expected a function",
f = NaN,
d = "[object Symbol]",
l = /^\s+|\s+$/g,
p = /^[-+]0x[0-9a-f]+$/i,
m = /^0b[01]+$/i,
b = /^0o[0-7]+$/i,
v = parseInt,
y = "object" == ("undefined" == typeof t ? "undefined" : c(t)) && t && t.Object === Object && t,
g = "object" == ("undefined" == typeof self ? "undefined" : c(self)) && self && self.Object === Object && self,
h = y || g || Function("return this")(),
w = Object.prototype,
k = w.toString,
x = Math.max,
j = Math.min,
O = function () {
return h.Date.now();
};
e.exports = o;
}).call(t, function () {
return this;
}());
}, function (e, t) {
(function (t) {
"use strict";
function n(e, t, n) {
function i(t) {
var n = b,
o = v;
return b = v = void 0, O = t, g = e.apply(o, n);
}
function r(e) {
return O = e, h = setTimeout(f, t), M ? i(e) : g;
}
function u(e) {
var n = e - w,
o = e - O,
i = t - n;
return S ? x(i, y - o) : i;
}
function s(e) {
var n = e - w,
o = e - O;
return void 0 === w || n >= t || n < 0 || S && o >= y;
}
function f() {
var e = j();
return s(e) ? d(e) : void (h = setTimeout(f, u(e)));
}
function d(e) {
return h = void 0, _ && b ? i(e) : (b = v = void 0, g);
}
function l() {
void 0 !== h && clearTimeout(h), O = 0, b = w = v = h = void 0;
}
function p() {
return void 0 === h ? g : d(j());
}
function m() {
var e = j(),
n = s(e);
if (b = arguments, v = this, w = e, n) {
if (void 0 === h) return r(w);
if (S) return h = setTimeout(f, t), i(w);
}
return void 0 === h && (h = setTimeout(f, t)), g;
}
var b,
v,
y,
g,
h,
w,
O = 0,
M = !1,
S = !1,
_ = !0;
if ("function" != typeof e) throw new TypeError(c);
return t = a(t) || 0, o(n) && (M = !!n.leading, S = "maxWait" in n, y = S ? k(a(n.maxWait) || 0, t) : y, _ = "trailing" in n ? !!n.trailing : _), m.cancel = l, m.flush = p, m;
}
function o(e) {
var t = "undefined" == typeof e ? "undefined" : u(e);
return !!e && ("object" == t || "function" == t);
}
function i(e) {
return !!e && "object" == ("undefined" == typeof e ? "undefined" : u(e));
}
function r(e) {
return "symbol" == ("undefined" == typeof e ? "undefined" : u(e)) || i(e) && w.call(e) == f;
}
function a(e) {
if ("number" == typeof e) return e;
if (r(e)) return s;
if (o(e)) {
var t = "function" == typeof e.valueOf ? e.valueOf() : e;
e = o(t) ? t + "" : t;
}
if ("string" != typeof e) return 0 === e ? e : +e;
e = e.replace(d, "");
var n = p.test(e);
return n || m.test(e) ? b(e.slice(2), n ? 2 : 8) : l.test(e) ? s : +e;
}
var u = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) {
return typeof e;
} : function (e) {
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e;
},
c = "Expected a function",
s = NaN,
f = "[object Symbol]",
d = /^\s+|\s+$/g,
l = /^[-+]0x[0-9a-f]+$/i,
p = /^0b[01]+$/i,
m = /^0o[0-7]+$/i,
b = parseInt,
v = "object" == ("undefined" == typeof t ? "undefined" : u(t)) && t && t.Object === Object && t,
y = "object" == ("undefined" == typeof self ? "undefined" : u(self)) && self && self.Object === Object && self,
g = v || y || Function("return this")(),
h = Object.prototype,
w = h.toString,
k = Math.max,
x = Math.min,
j = function () {
return g.Date.now();
};
e.exports = n;
}).call(t, function () {
return this;
}());
}, function (e, t) {
"use strict";
function n(e) {
var t = void 0,
o = void 0,
i = void 0;
for (t = 0; t < e.length; t += 1) {
if (o = e[t], o.dataset && o.dataset.aos) return !0;
if (i = o.children && n(o.children)) return !0;
}
return !1;
}
function o() {
return window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
}
function i() {
return !!o();
}
function r(e, t) {
var n = window.document,
i = o(),
r = new i(a);
u = t, r.observe(n.documentElement, {
childList: !0,
subtree: !0,
removedNodes: !0
});
}
function a(e) {
e && e.forEach(function (e) {
var t = Array.prototype.slice.call(e.addedNodes),
o = Array.prototype.slice.call(e.removedNodes),
i = t.concat(o);
if (n(i)) return u();
});
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var u = function () {};
t.default = {
isSupported: i,
ready: r
};
}, function (e, t) {
"use strict";
function n(e, t) {
if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function");
}
function o() {
return navigator.userAgent || navigator.vendor || window.opera || "";
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var i = function () {
function e(e, t) {
for (var n = 0; n < t.length; n++) {
var o = t[n];
o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, o.key, o);
}
}
return function (t, n, o) {
return n && e(t.prototype, n), o && e(t, o), t;
};
}(),
r = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i,
a = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i,
u = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i,
c = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i,
s = function () {
function e() {
n(this, e);
}
return i(e, [{
key: "phone",
value: function () {
var e = o();
return !(!r.test(e) && !a.test(e.substr(0, 4)));
}
}, {
key: "mobile",
value: function () {
var e = o();
return !(!u.test(e) && !c.test(e.substr(0, 4)));
}
}, {
key: "tablet",
value: function () {
return this.mobile() && !this.phone();
}
}]), e;
}();
t.default = new s();
}, function (e, t) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var n = function (e, t, n) {
var o = e.node.getAttribute("data-aos-once");
t > e.position ? e.node.classList.add("aos-animate") : "undefined" != typeof o && ("false" === o || !n && "true" !== o) && e.node.classList.remove("aos-animate");
},
o = function (e, t) {
var o = window.pageYOffset,
i = window.innerHeight;
e.forEach(function (e, r) {
n(e, i + o, t);
});
};
t.default = o;
}, function (e, t, n) {
"use strict";
function o(e) {
return e && e.__esModule ? e : {
default: e
};
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var i = n(12),
r = o(i),
a = function (e, t) {
return e.forEach(function (e, n) {
e.node.classList.add("aos-init"), e.position = (0, r.default)(e.node, t.offset);
}), e;
};
t.default = a;
}, function (e, t, n) {
"use strict";
function o(e) {
return e && e.__esModule ? e : {
default: e
};
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var i = n(13),
r = o(i),
a = function (e, t) {
var n = 0,
o = 0,
i = window.innerHeight,
a = {
offset: e.getAttribute("data-aos-offset"),
anchor: e.getAttribute("data-aos-anchor"),
anchorPlacement: e.getAttribute("data-aos-anchor-placement")
};
switch (a.offset && !isNaN(a.offset) && (o = parseInt(a.offset)), a.anchor && document.querySelectorAll(a.anchor) && (e = document.querySelectorAll(a.anchor)[0]), n = (0, r.default)(e).top, a.anchorPlacement) {
case "top-bottom":
break;
case "center-bottom":
n += e.offsetHeight / 2;
break;
case "bottom-bottom":
n += e.offsetHeight;
break;
case "top-center":
n += i / 2;
break;
case "bottom-center":
n += i / 2 + e.offsetHeight;
break;
case "center-center":
n += i / 2 + e.offsetHeight / 2;
break;
case "top-top":
n += i;
break;
case "bottom-top":
n += e.offsetHeight + i;
break;
case "center-top":
n += e.offsetHeight / 2 + i;
}
return a.anchorPlacement || a.offset || isNaN(t) || (o = t), n + o;
};
t.default = a;
}, function (e, t) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var n = function (e) {
for (var t = 0, n = 0; e && !isNaN(e.offsetLeft) && !isNaN(e.offsetTop);) t += e.offsetLeft - ("BODY" != e.tagName ? e.scrollLeft : 0), n += e.offsetTop - ("BODY" != e.tagName ? e.scrollTop : 0), e = e.offsetParent;
return {
top: n,
left: t
};
};
t.default = n;
}, function (e, t) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var n = function (e) {
return e = e || document.querySelectorAll("[data-aos]"), Array.prototype.map.call(e, function (e) {
return {
node: e
};
});
};
t.default = n;
}]);
});
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/************************************************************************/
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
/******/ "use strict";
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// UNUSED EXPORTS: default
@@ -1320,9 +724,6 @@ var BasePage = /*#__PURE__*/function () {
}]);
}();
// EXTERNAL MODULE: ./node_modules/aos/dist/aos.js
var aos = __webpack_require__(431);
var aos_default = /*#__PURE__*/__webpack_require__.n(aos);
;// ./static/js/pages/core/home.js
function home_typeof(o) { "@babel/helpers - typeof"; return home_typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, home_typeof(o); }
function home_classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
@@ -1344,7 +745,6 @@ function home_toPrimitive(t, r) { if ("object" != home_typeof(t) || !t) return t
// internal
// external
var PageHome = /*#__PURE__*/function (_BasePage) {
function PageHome(router) {
home_classCallCheck(this, PageHome);
@@ -1356,45 +756,7 @@ var PageHome = /*#__PURE__*/function (_BasePage) {
value: function initialize() {
this.sharedInitialize();
this.hookupButtonsNavContact();
// this.initialiseAOS();
this.initialiseAnimations();
}
/* AOS */
}, {
key: "initialiseAOS",
value: function initialiseAOS() {
aos_default().init({
duration: 1000,
once: true
});
}
/* Manual animations *
initialiseAnimations() {
// Check if IntersectionObserver is supported
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
// Observe all elements with 'reveal' class
document.querySelectorAll('.reveal').forEach((element) => {
observer.observe(element);
});
} else {
// If IntersectionObserver is not supported, make all elements visible
document.querySelectorAll('.reveal').forEach((element) => {
element.style.opacity = 1;
});
}
}
*/
}, {
key: "leave",
value: function leave() {
@@ -1430,8 +792,32 @@ var PageContact = /*#__PURE__*/function (_BasePage) {
key: "initialize",
value: function initialize() {
this.sharedInitialize();
this.hookupCaptcha();
this.hookupButtonSubmitFormContactUs();
}
}, {
key: "hookupCaptcha",
value: function hookupCaptcha() {
var form = document.querySelector(idContactForm);
var altchaWidget = form.querySelector('altcha-widget');
// Listen for verification events from the ALTCHA widget
if (altchaWidget) {
altchaWidget.addEventListener('serververification', function (event) {
// Create or update the hidden input for ALTCHA
var altchaInput = form.querySelector('input[name="altcha"]');
if (!altchaInput) {
altchaInput = document.createElement('input');
altchaInput.type = 'hidden';
altchaInput.name = 'altcha';
form.appendChild(altchaInput);
}
// Set the verification payload
altchaInput.value = event.detail.payload;
});
}
}
}, {
key: "hookupButtonSubmitFormContactUs",
value: function hookupButtonSubmitFormContactUs() {
@@ -2301,65 +1687,56 @@ window.app = app;
/* harmony default export */ const js_app = ((/* unused pure expression or super */ null && (app)));
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
"use strict";
// extracted by mini-css-extract-plugin
})();

View File

@@ -10,9 +10,32 @@ export default class PageContact extends BasePage {
initialize() {
this.sharedInitialize();
this.hookupCaptcha();
this.hookupButtonSubmitFormContactUs();
}
hookupCaptcha() {
const form = document.querySelector(idContactForm);
const altchaWidget = form.querySelector('altcha-widget');
// Listen for verification events from the ALTCHA widget
if (altchaWidget) {
altchaWidget.addEventListener('serververification', function(event) {
// Create or update the hidden input for ALTCHA
let altchaInput = form.querySelector('input[name="altcha"]');
if (!altchaInput) {
altchaInput = document.createElement('input');
altchaInput.type = 'hidden';
altchaInput.name = 'altcha';
form.appendChild(altchaInput);
}
// Set the verification payload
altchaInput.value = event.detail.payload;
});
}
}
hookupButtonSubmitFormContactUs() {
const button = document.querySelector('form input[type="submit"]');
button.classList.add(flagButton);

View File

@@ -2,7 +2,6 @@
// internal
import BasePage from "../base.js";
// external
import AOS from 'aos';
export default class PageHome extends BasePage {
@@ -15,46 +14,10 @@ export default class PageHome extends BasePage {
initialize() {
this.sharedInitialize();
this.hookupButtonsNavContact();
// this.initialiseAOS();
this.initialiseAnimations();
}
/* AOS */
initialiseAOS() {
AOS.init({
duration: 1000,
once: true,
});
}
/* Manual animations *
initialiseAnimations() {
// Check if IntersectionObserver is supported
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
// Observe all elements with 'reveal' class
document.querySelectorAll('.reveal').forEach((element) => {
observer.observe(element);
});
} else {
// If IntersectionObserver is not supported, make all elements visible
document.querySelectorAll('.reveal').forEach((element) => {
element.style.opacity = 1;
});
}
}
*/
leave() {
super.leave();
}
}

2636
static/js/vendor/altcha.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -117,7 +117,7 @@
var flagPageBody = "{{ model.FLAG_PAGE_BODY }}";
var flagPhoneNumber = "{{ model.FLAG_PHONE_NUMBER }}";
var flagPostcode = "{{ model.FLAG_POSTCODE }}";
var flagRecaptcha = "{{ model.FLAG_RECAPTCHA }}";
var flagCaptcha = "{{ model.FLAG_CAPTCHA }}";
var flagRightHandSide = "{{ model.FLAG_RIGHT_HAND_SIDE }}";
var flagRow = "{{ model.FLAG_ROW }}";
var flagRowNew = "{{ model.FLAG_ROW_NEW }}";

View File

@@ -2,6 +2,10 @@
{% block page_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='dist/css/core_contact.bundle.css') }}">
{#
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@altcha/browser@latest/dist/index.js" defer></script>
#}
<script type="module" src="{{ url_for('static', filename='js/vendor/altcha.js')}}"></script>
{% endblock %}
{% block page_nav_links %}
@@ -12,6 +16,7 @@
{% endblock %}
{% block page_body %}
{#
<script>
function loadRecaptcha() {
var script = document.createElement('script');
@@ -22,15 +27,15 @@
window.addEventListener('load', loadRecaptcha);
</script>
#}
<!-- Divs -->
{% set form = model.form_contact %}
<section class="contact-section">
<div class="contact-form">
<h1>Contact Us</h1>
<p>Please fill in the form below and we'll get back to you as soon as possible.</p>
<form id="contact-form" method="POST" action="{{ url_for('routes_core.contact') }}">
<form id="{{ model.ID_CONTACT_FORM }}" method="POST" action="{{ url_for('routes_core.contact') }}">
{{ form.csrf_token }}
<div class="form-grid">
@@ -59,8 +64,12 @@
{{ model.form_contact.receive_marketing() }}
{{ model.form_contact.receive_marketing.label }}
</div>
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_RECAPTCHA }}">
{{ model.form_contact.recaptcha() }}
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_CAPTCHA }}">
{# {{ model.form_contact.recaptcha() }} #}
<altcha-widget
challengeurl="https://eu.altcha.org/api/v1/challenge?apiKey={{ model.app.app_config.ALTCHA_API_KEY }}"
spamfilter
></altcha-widget>
</div>
<div class="{{ model.FLAG_CONTAINER_INPUT }}">
{{ model.form_contact.submit() }}
@@ -101,6 +110,7 @@
#}
<script>
var idContactForm = "#{{ model.ID_CONTACT_FORM }}";
var idEmail = "#{{ model.ID_EMAIL }}";
var idMessage = "#{{ model.ID_MESSAGE }}";
var idContactName = "#{{ model.ID_CONTACT_NAME }}";