Sync master from upstream#107
Conversation
Detect the visitor's country server-side and apply it to the phone field: - Helper::get_geo_country(): prefer a CDN/server country header (Cloudflare/CloudFront/mod_geoip) — free, instant, cache-safe and independent of the connecting IP — before any API call; then ipapi.co with an optional filterable key (srfm_ipapi_api_key) for reliable IP detection; degrade to the field's configured default country. - Reject private/reserved IPs, detect ipapi 200-with-error rate-limit bodies, short failure-cache TTL (srfm_geo_failure_ttl) so a blip self-heals, and a site-wide hourly cap on outbound calls. - Public REST route sureforms/v1/geo-country so detection is per-visitor and correct on full-page-cached sites (not baked into cached HTML). - Frontend applyAutoCountry() fetches the endpoint and applies the country via intl-tel-input, cached in sessionStorage per session. - phone-markup.php emits data-auto-country and passes the configured default as the fallback; frontend-assets.php localizes the endpoint URL.
Server-side IP detection can't work on localhost (loopback) and ipapi.co's
keyless tier is rate-limited, so auto-country fell back to US. Add a layered,
privacy-safe fallback:
- REST geo-country endpoint now returns { country, detected }; detected is
false when the server has no confident result (no CDN header, no successful
IP lookup).
- Frontend detectCountryFromBrowser() derives the ISO region from the browser
locale via Intl.Locale — purely on-device, no network/IP/third-party, so it
works offline, on localhost and on full-page-cached pages (none of the
CORS/rate-limit/privacy issues that moved the IP lookup server-side).
- applyAutoCountry() applies the local guess immediately and only lets a
confident server result override it; sessionStorage key bumped so stale
fallback values don't mask the local guess.
Intl.Locale region reflects the browser *language*, not physical location, so an en-US browser in India guessed US. Make the device timezone the primary on-device signal via an IANA timezone -> country map (e.g. Asia/Kolkata -> in), falling back to the locale region only when the timezone is unknown. Still purely on-device: no network, no IP, no third party.
Reorder the new private static geo helpers after the public methods (PHP Insights OrderedClassElements), and cover the two flagged functions: Helper::get_geo_country() — CDN header preference with no outbound call, placeholder rejection with private-IP fallback, srfm_cdn_country filter short-circuit, API resolution with transient caching, and rate-limit error-body fallback with failure caching — and Rest_Api::get_geo_country() — detected and not-detected response shapes.
Master to Dev 2.11.0
Dev to Next Release 2.11.0
The 'Get Keys' button pointed to https://www.cloudflare.com/en-gb/products/turnstile/, a marketing page whose locale path now returns a 404 and never led to key creation. Point it to the Cloudflare dashboard Turnstile page where users can create a widget and retrieve their site and secret keys.
Closes an unauthenticated payment amount bypass (WPScan, CVSS 5.9) on variable-amount payment forms. - Variant B (legacy/empty source): validate_payment_intent_amount() no longer returns valid unconditionally when the amount-source field is unidentifiable; it now always falls through to the configured minimum-amount floor. - Hidden / calculation-driven sources: the expected amount is derived server-side via the new 'srfm_server_side_variable_amount' filter (implemented in Pro) or the field's stored default value, never from the value submitted with the request. Plain 'name your price' number fields remain user-determined and floor-protected. - Defense-in-depth: the amount Stripe actually charged/invoiced is re-validated against the form configuration on both the one-time and subscription paths. Fixed-price, dropdown and multi-choice sources are unchanged (already validated server-side). Capture is only requested after validation, so a rejected submission never charges.
Strengthen the Variant B (legacy form) fix. Falling back to the minimum-amount floor was insufficient: legacy forms often have a zero floor, so an underpayment was still accepted (verified end-to-end). Now, when the variable amount source field is not recorded in the stored config, first refresh the block config from the form's current content to recover it (self-heals legacy forms whose source field still exists), then validate normally. If the source still cannot be identified, reject the payment instead of accepting an attacker-supplied amount.
fix: update Cloudflare Turnstile 'Get Keys' link to dashboard URL
- Store calculationRound as null when unset and round only when a precision is configured, so a non-integer calculated total (e.g. 29.99) is no longer rounded up and rejected as a mismatch.
…ests - Reorder Payment_Helper class elements (PHP Insights ordered-elements) - Add test_get_submitted_value_by_slug and test_validate_amount_against_config to satisfy check-test-coverage for the new public methods
Version is filled in on the release PR; do not hardcode 2.11.1.
…le source When a variable-amount form's amount-source field can no longer be identified (e.g. deleted while amount_type stays 'variable'), enforce the configured minimum amount as the authoritative lower bound instead of hard-rejecting every payment. Only reject when no positive floor is configured (the genuinely unsafe case), keeping the underpayment bypass closed while letting legacy forms keep working. Also document the hidden-field amount-source behavior: smart-tag (non-numeric) defaults fall through to the floor (dynamic prefill keeps working); a literal numeric default is authoritative — custom JS-driven pricing must use the srfm_server_side_variable_amount filter or a calculation field. Adds test_validate_amount_against_config_unrecoverable_source_floor.
- phone.js: don't let the async REST refine override an explicit dropdown country pick (track lastAutoApplied; bail if selection changed). (High #1) - phone.js validateCountryWithFilters: normalize + ISO2-validate fallbacks; include -> first valid included country; exclude -> first non-excluded candidate instead of a hardcoded gb/us that may itself be excluded. (High #2) - phone.js setCountry: /^[a-z]{2}$/ guard before setCountry() so a malformed REST country value can't reach iti. (Medium #4) - helper.php get_geo_country: don't write a per-IP transient in the quota-cap branch (the only write path not bounded by the cap; prevents spoofed X-Forwarded-For cache churn). (Medium #3) - helper.php: bump per-IP cache key srfm_geo_ -> srfm_geo_v2_ so stale failure transients from older code don't shadow the new logic; note CDN header spoofability in get_cdn_country docblock. (Low) - phone-markup.php: update stale constructor comment to the per-visitor REST flow (baked default + client-side Intl guess + REST refine). (Medium #5) - tests: bump geo cache key to v2; add cap-reached no-cache test.
Mirrors the pro hardening (sureforms-pro#1298): - Pass the PR head_ref via env (HEAD_REF) and reference it quoted in the create-branch and create-PR run blocks, so a branch name with shell metacharacters can't achieve RCE / exfiltrate the workflow secrets. - Add an author_association gate (OWNER/MEMBER/COLLABORATOR) to the /i18n trigger so only trusted users can run the secret-bearing build job.
…ned bypass) - HIGH: srfm/number branch now rejects when enableCalculation is set and the server amount is null, instead of falling through to name-your-price and trusting the client-submitted amount (the re-opened underpayment bypass). - Medium: dropdown branch rejects a non-numeric/null expected amount, so abs($payment_amount - null) can never coerce to 0. - Test: calc-number with an unresolvable formula (server null) is rejected, isolated with minimum_amount=0 so it can't pass via the floor.
Squashes chore/node-24-bump onto current dev as a single signed commit (the original branch couldn't merge dev in: signed-commits rule + 5 unsigned dev commits + no force-push). Same changes: Node 24.16.0 (Volta/CI/docs), drop --legacy-peer-deps, and gpt-po 1.1.1 -> 1.3.0 with --model gpt-4o on all 20 i18n:gptpo:* scripts (drops vulnerable axios; avoids 1.3.0's gpt-5-nano 400).
…per-visitor Phone: reliable per-visitor auto country detection
Adds a SureDonation entry as the first item in Helper::sureforms_get_integration(), so it leads the rotating recommended-plugin banner on the SureForms Extend dashboard. Ships the suredonation.svg brand asset and follows the existing integration entry shape (title, descriptions, status via get_plugin_status(), slug/path, encoded SVG logo).
…ration-banner feat: add SureDonation to the integrations banner
fix(payments): unauthenticated payment amount bypass on variable-amount forms
chore: standardize Node to 24.16.0 + gpt-po 1.3.0 (replaces #2821)
fix(ci): harden /i18n workflow — block head_ref injection + gate trigger
…into master-dev-2.11.1-pre
Master to Dev 2.11.1
…to dev-nr-2.11.1-pre
Dev to Next Release 2.11.1
Version Bump 2.11.1
…mization Optimize readme.txt for form / contact form search ranking
…lists, drop filler) Collapses the integrations, themes, and plugins bullet lists into inline sentences and removes pain-point/closing filler. Feature descriptions are unchanged. Description: 2,291 -> 2,141 words. Note: this is a tidy-up only and does not by itself bring the section under WordPress.org's 1,500-word limit. Regenerate README.md.
Light readme cleanup — inline compatibility/integration lists
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
|
Closing — this v1 sync branch still contained internal docs/ and CLAUDE.md files. Superseded by sync/master-20260619-v2 with an expanded strip list. |
Syncs
brainstormforce/sureforms@masterinto the public mirror.Upstream range:
4ad4d6541a44..00d513cb89aa(315 commits)Strip applied: yes — internal-only paths (.claude, CLAUDE.md, internal-docs, internal release CI workflows, bin build scripts, etc.) removed.
Diff base: merge-capped against
mirror/master, so this PR shows only real upstream changes — no internal-file deletions.Signatures: all synced commits re-signed; verified by GitHub.
Highlights (merge commits)