Convert the bundled backend/internal/plugins/calcomblock package into a standalone reactor-mode wasm plugin, dropping every git.dev.alexdunmow.com/block/cms import (forbidden in standalone plugins): - package calcomblock -> package main; add wasip1 main.go with wasmguest.Serve(Registration). go.mod module git.dev.alexdunmow.com/block/calcomblock, block/core v0.18.2, no replace directives. - Settings persistence: replace the pool-backed poolQuerier (direct SQL against the public-schema `settings` table, which a sandboxed plugin role cannot read) with capabilityQuerier over the SDK settings.Settings / settings.Updater capabilities. GetSiteTimezone now resolves via GetSiteSettings host-side. - Vendor the small CMS-internal helpers the plugin used into internal/helpers (GetRealIP, StartCleanupLoop, MaskSecret, GetStringOr, the PluginSettingsQuerier settings helpers, PluginCrypto for tests) and internal/db (minimal Setting / UpsertSettingParams), each with a provenance header. - Inline the captcha widget: vendor blocks.CaptchaWidget as a local CaptchaWidget templ (captcha_widget.templ) and regenerate templ; drop the block/cms/blocks import from booking.templ. - blockConfig (server-authoritative captcha requirement via a page_block_snapshots scan) has no wasm-ABI capability, so it is left nil: honeypot + per-IP rate limit still apply. Documented as a follow-up. - web: @block-ninja/ui workspace:* -> ^0.1.0 registry version; eslint.config.js repointed at ../../../cms/web/eslint.config.js; rebuild web/dist. - Rewrite CLAUDE.md for the standalone wasm reality; add .gitignore. Full test suite preserved and passing; builds to calcomblock-2.0.0.bnp; check-safety passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
4.2 KiB
Go
124 lines
4.2 KiB
Go
package main
|
|
|
|
import "strings"
|
|
|
|
// regionFromLocale extracts the ISO 3166-1 alpha-2 region from a BCP-47 locale
|
|
// tag such as the value of navigator.language ("en-AU" -> "AU", "zh-Hans-CN" ->
|
|
// "CN"). The first subtag is the language unless it is itself an uppercase
|
|
// 2-letter region (a bare "AU"). Returns "" when no region subtag is present
|
|
// ("en" -> "").
|
|
func regionFromLocale(locale string) string {
|
|
locale = strings.TrimSpace(locale)
|
|
if locale == "" {
|
|
return ""
|
|
}
|
|
parts := strings.FieldsFunc(locale, func(r rune) bool { return r == '-' || r == '_' })
|
|
for i, part := range parts {
|
|
if len(part) != 2 || !isASCIILetters(part) {
|
|
continue // language (handled below), script ("Hans"), or variant
|
|
}
|
|
// Subtag 0 is the language unless it is already an uppercase region.
|
|
if i == 0 && part != strings.ToUpper(part) {
|
|
continue
|
|
}
|
|
return strings.ToUpper(part)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// isASCIILetters reports whether s is non-empty and all ASCII letters.
|
|
func isASCIILetters(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
for _, r := range s {
|
|
if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// tzRegions maps IANA timezones to ISO 3166-1 alpha-2 regions for the common,
|
|
// unambiguous cases. The device timezone is a stronger locator than the UI
|
|
// locale (an en-US browser in Australia still reports Australia/*), so it is
|
|
// preferred. Ambiguous zones (most America/*) are intentionally omitted — those
|
|
// fall through to the locale. Australia/* is matched by prefix in
|
|
// regionFromTimezone.
|
|
var tzRegions = map[string]string{
|
|
"Pacific/Auckland": "NZ", "Pacific/Chatham": "NZ",
|
|
"Europe/London": "GB", "Europe/Dublin": "IE", "Europe/Paris": "FR",
|
|
"Europe/Berlin": "DE", "Europe/Madrid": "ES", "Europe/Rome": "IT",
|
|
"Europe/Amsterdam": "NL", "Europe/Brussels": "BE", "Europe/Zurich": "CH",
|
|
"Europe/Stockholm": "SE", "Europe/Oslo": "NO", "Europe/Copenhagen": "DK",
|
|
"Asia/Singapore": "SG", "Asia/Kolkata": "IN", "Asia/Calcutta": "IN",
|
|
"Asia/Tokyo": "JP", "Asia/Hong_Kong": "HK", "Asia/Dubai": "AE",
|
|
"Asia/Shanghai": "CN", "Asia/Seoul": "KR", "Africa/Johannesburg": "ZA",
|
|
}
|
|
|
|
// regionFromTimezone maps an IANA timezone (navigator-detected, e.g.
|
|
// "Australia/Perth") to a region for the common cases. Returns "" for
|
|
// unknown/ambiguous zones (including the UTC fallback some browsers report).
|
|
func regionFromTimezone(tz string) string {
|
|
tz = strings.TrimSpace(tz)
|
|
if tz == "" {
|
|
return ""
|
|
}
|
|
if strings.HasPrefix(tz, "Australia/") {
|
|
return "AU"
|
|
}
|
|
return tzRegions[tz]
|
|
}
|
|
|
|
// resolveRegion picks the ISO region used to pre-select the phone country
|
|
// combobox and to seed E.164 normalization when the visitor has not explicitly
|
|
// picked a country. It prefers the timezone region (strongest locator), then the
|
|
// browser locale region, validated against the known country list. Returns ""
|
|
// when neither is known — the combobox then has no default and normalizePhone
|
|
// only handles already-international numbers.
|
|
func resolveRegion(locale, timezone string) string {
|
|
for _, r := range []string{regionFromTimezone(timezone), regionFromLocale(locale)} {
|
|
if r != "" {
|
|
if _, ok := countryByCode(r); ok {
|
|
return r
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// countryByCode returns the country with the given ISO alpha-2 code, or false.
|
|
func countryByCode(code string) (Country, bool) {
|
|
code = strings.ToUpper(strings.TrimSpace(code))
|
|
for _, c := range countries {
|
|
if c.Code == code {
|
|
return c, true
|
|
}
|
|
}
|
|
return Country{}, false
|
|
}
|
|
|
|
// countryFlag returns the emoji flag for a 2-letter ISO region code by mapping
|
|
// each letter to its Unicode regional-indicator symbol. "" for invalid codes.
|
|
func countryFlag(code string) string {
|
|
code = strings.ToUpper(strings.TrimSpace(code))
|
|
if len(code) != 2 || !isASCIILetters(code) {
|
|
return ""
|
|
}
|
|
r := []rune(code)
|
|
return string(rune(0x1F1E6+(r[0]-'A'))) + string(rune(0x1F1E6+(r[1]-'A')))
|
|
}
|
|
|
|
// countryLabel renders the human label shown in the combobox and its pre-fill,
|
|
// e.g. "🇦🇺 Australia (+61)". Returns "" when the code is unknown.
|
|
func countryLabel(code string) string {
|
|
c, ok := countryByCode(code)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
if flag := countryFlag(c.Code); flag != "" {
|
|
return flag + " " + c.Name + " (" + c.Dial + ")"
|
|
}
|
|
return c.Name + " (" + c.Dial + ")"
|
|
}
|