diff --git a/assets/src/settings/procaptcha/procaptchaSite.ts b/assets/src/settings/procaptcha/procaptchaSite.ts index f7982a0..e322aa2 100644 --- a/assets/src/settings/procaptcha/procaptchaSite.ts +++ b/assets/src/settings/procaptcha/procaptchaSite.ts @@ -2,12 +2,20 @@ import { type ProcaptchaAccount, procaptchaAccountSchema, } from "#settings/procaptcha/procaptchaAccount.js"; -import { z, type ZodType, type ZodTypeDef } from "zod"; +import { z, type ZodType } from "zod"; +/** + * Only the parts of the site response the statistics page actually renders: + * the monthly captcha counts and the account tier the traffic chart gates on. + * + * `settings` is deliberately absent. Zod strips keys a schema does not + * declare, so whatever shape the portal sends for the captcha settings — and + * `frictionlessThreshold` has already changed shape once — is ignored rather + * than validated. The whole response is a single parse, so a field the page + * does not display has no business being able to fail it. + */ export interface ProcaptchaSite { account: ProcaptchaAccount; - name: string; - settings: SiteSettings; monthlyUsage: { limit: number; image: CaptchaUsage; @@ -15,103 +23,12 @@ export interface ProcaptchaSite { }; } -export interface SiteSettings { - /** - * Lower rung of the frictionless score ladder: the score a session has to - * stay under to pass without being challenged. This is the value the - * plugin has always shown as "Frictionless Threshold". - */ - frictionlessThreshold: number; - /** - * Upper rung: the score at or above which a session gets an image captcha - * rather than a puzzle. Arrives nested inside `frictionlessThreshold`, not - * as a sibling of it, and is absent whenever that field is still the bare - * number the pre-ladder API sends. - * - * Not to be confused with the settings' own `imageThreshold`, which is an - * unrelated image-captcha setting on a 0..1 scale. - */ - frictionlessImageThreshold?: number; - powDifficulty: number; - captchaType: string; - domains: string[]; -} - export interface CaptchaUsage { submissions: number; verifications: number; total: number; } -/** - * `SiteSettings` as it arrives on the wire, before the ladder is split into - * the two flat fields the rest of the plugin reads. Declared separately - * because the schema below transforms on parse, so its input and output - * types differ and `ZodType` needs both. - */ -export interface SiteSettingsInput - extends Omit< - SiteSettings, - "frictionlessThreshold" | "frictionlessImageThreshold" - > { - frictionlessThreshold: - | number - | { - frictionlessPuzzleThreshold?: number; - frictionlessImageThreshold?: number; - }; -} - -export interface ProcaptchaSiteInput extends Omit { - settings: SiteSettingsInput; -} - -/** - * Fallback for a ladder object that arrives without its lower rung. Matches - * the portal's own default so the label the user sees does not change - * meaning between the two shapes. - */ -const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5; - -/** - * `frictionlessThreshold` is read as "number, or the two-rung ladder object" - * because both are live at once: the portal moved the field to the ladder, - * but records migrate in the background and the plugin ships independently - * of the portal, so an install can be talking to either shape. A bare number - * means what it always meant — the puzzle rung — and carries no image rung. - */ -const frictionlessThresholdSchema = z.union([ - z.number(), - z.object({ - frictionlessPuzzleThreshold: z.number().optional(), - frictionlessImageThreshold: z.number().optional(), - }), -]); - -/** - * Split the ladder into the two flat fields the rest of the plugin reads, so - * nothing downstream has to know which of the two wire shapes arrived. - */ -export const siteSettingsSchema = z - .object({ - frictionlessThreshold: frictionlessThresholdSchema, - powDifficulty: z.number(), - captchaType: z.string(), - domains: z.string().array(), - }) - .transform(({ frictionlessThreshold, ...settings }) => ({ - ...settings, - frictionlessThreshold: - "number" === typeof frictionlessThreshold - ? frictionlessThreshold - : (frictionlessThreshold.frictionlessPuzzleThreshold ?? - DEFAULT_FRICTIONLESS_THRESHOLD), - frictionlessImageThreshold: - "number" === typeof frictionlessThreshold - ? undefined - : frictionlessThreshold.frictionlessImageThreshold, - })) satisfies ZodType; - export const captchaUsageSchema = z.object({ submissions: z.number(), verifications: z.number(), @@ -120,11 +37,9 @@ export const captchaUsageSchema = z.object({ export const procaptchaSiteSchema = z.object({ account: procaptchaAccountSchema, - name: z.string(), - settings: siteSettingsSchema, monthlyUsage: z.object({ limit: z.number(), image: captchaUsageSchema, pow: captchaUsageSchema, }), -}) satisfies ZodType; +}) satisfies ZodType; diff --git a/assets/src/settings/statistics/components/appComponent.tsx b/assets/src/settings/statistics/components/appComponent.tsx index 9f25eca..9099a97 100644 --- a/assets/src/settings/statistics/components/appComponent.tsx +++ b/assets/src/settings/statistics/components/appComponent.tsx @@ -4,7 +4,6 @@ import { AppStatusComponentProperties, StatCurrentState, } from "./appStatusComponent.js"; -import { ListComponent, ListComponentProperties } from "./listComponent.js"; import { TrafficAnalyticsComponent, TrafficAnalyticsComponentProperties, @@ -21,10 +20,7 @@ import { import type Logger from "#utils/logger/logger.js"; import { type Config, ConfigClass } from "#settings/statistics/config.js"; import CaptchaUsageNumberUtils from "#settings/statistics/captchaUsage/captchaUsageNumberUtils.js"; -import type { - ProcaptchaSite, - SiteSettings, -} from "#settings/procaptcha/procaptchaSite.js"; +import type { ProcaptchaSite } from "#settings/procaptcha/procaptchaSite.js"; import type { ProcaptchaAccount } from "#settings/procaptcha/procaptchaAccount.js"; import { @@ -39,9 +35,6 @@ interface AppComponentProperties { interface AppState { statState: AppStatusComponentProperties; usageInfo: CaptchaUsageComponentProperties; - accountInformation: ListComponentProperties; - captchaSettings: ListComponentProperties; - domains: ListComponentProperties; trafficData: TrafficAnalyticsComponentProperties; } @@ -95,50 +88,6 @@ class AppComponent extends React.Component { }, labels: this.config.getUsageLabels(), }, - accountInformation: { - title: this.config.getAccountLabels().title, - icon: "icon-[material-symbols--account-circle]", - items: [ - { - label: this.config.getAccountLabels().tier, - value: "...", - }, - { - label: this.config.getAccountLabels().name, - value: "...", - }, - ], - }, - captchaSettings: { - title: this.config.getCaptchaSettingsLabels().title, - icon: "icon-[material-symbols--settings]", - items: [ - { - label: this.config.getCaptchaSettingsLabels().type, - value: "...", - }, - { - label: this.config.getCaptchaSettingsLabels() - .frictionlessThreshold, - value: "...", - }, - { - label: this.config.getCaptchaSettingsLabels() - .frictionlessImageThreshold, - value: "...", - }, - { - label: this.config.getCaptchaSettingsLabels() - .powDifficulty, - value: "...", - }, - ], - }, - domains: { - title: this.config.getDomainLabels().title, - icon: "icon-[material-symbols--domain]", - items: [], - }, trafficData: { accountTier: "", logger: this.logger, @@ -169,22 +118,9 @@ class AppComponent extends React.Component { })); } - protected refreshUserData(site: ProcaptchaSite): void { + protected refreshUsage(site: ProcaptchaSite): void { this.setState((actualState) => ({ ...actualState, - accountInformation: { - ...actualState.accountInformation, - items: [ - { - label: this.config.getAccountLabels().tier, - value: site.account.tier.toUpperCase(), - }, - { - label: this.config.getAccountLabels().name, - value: site.name, - }, - ], - }, usageInfo: { ...actualState.usageInfo, limits: { @@ -204,121 +140,6 @@ class AppComponent extends React.Component { })); } - protected getPowDifficultyLabel(powDifficulty: number): string { - const levelLabels = this.config.getCaptchaSettingsLabels().level; - - if (powDifficulty === 4) { - return levelLabels.normal; - } - - return powDifficulty < 4 ? levelLabels.low : levelLabels.high; - } - - protected getFrictionlessThresholdLabel( - frictionlessThreshold: number, - ): string { - const levelLabels = this.config.getCaptchaSettingsLabels().level; - - if (frictionlessThreshold >= 0.4 && frictionlessThreshold <= 0.6) { - return levelLabels.normal; - } - - return frictionlessThreshold < 0.4 ? levelLabels.high : levelLabels.low; - } - - /** - * Upper rung of the score ladder, shown as a level rather than a raw - * score to match its sibling above. A lower rung means more sessions - * reach an image captcha instead of a puzzle, so it reads as stricter. - * - * Banded on the portal's 1.0 default rather than a range: unlike the - * lower rung this one is deliberately allowed above 1, because the score - * it is compared against is a total that server-side penalties add to. - * - * An API still sending the pre-ladder bare `frictionlessThreshold` gives - * no upper rung at all, in which case the row shows an em dash rather - * than inventing a value. - */ - protected getFrictionlessImageThresholdLabel( - frictionlessImageThreshold: number | undefined, - ): string { - if (undefined === frictionlessImageThreshold) { - return "—"; - } - - const levelLabels = this.config.getCaptchaSettingsLabels().level; - - if (frictionlessImageThreshold < 1) { - return levelLabels.high; - } - - return 1 === frictionlessImageThreshold - ? levelLabels.normal - : levelLabels.low; - } - - protected getTypeLabel(type: string): string { - const typeLabels = this.config.getCaptchaSettingsLabels().types; - - switch (type) { - case "image": - return typeLabels.image; - case "pow": - return typeLabels.proofOfWork; - default: - return typeLabels.frictionless; - } - } - - protected refreshSiteSettings(siteSettings: SiteSettings): void { - const domains = siteSettings.domains - .filter((domain) => "*" !== domain) - .map((domain, index) => { - return { - label: "#" + (index + 1), - value: domain, - }; - }); - - this.setState((actualState) => ({ - ...actualState, - captchaSettings: { - ...actualState.captchaSettings, - items: [ - { - label: this.config.getCaptchaSettingsLabels().type, - value: this.getTypeLabel(siteSettings.captchaType), - }, - { - label: this.config.getCaptchaSettingsLabels() - .frictionlessThreshold, - value: this.getFrictionlessThresholdLabel( - siteSettings.frictionlessThreshold, - ), - }, - { - label: this.config.getCaptchaSettingsLabels() - .frictionlessImageThreshold, - value: this.getFrictionlessImageThresholdLabel( - siteSettings.frictionlessImageThreshold, - ), - }, - { - label: this.config.getCaptchaSettingsLabels() - .powDifficulty, - value: this.getPowDifficultyLabel( - siteSettings.powDifficulty, - ), - }, - ], - }, - domains: { - ...actualState.domains, - items: domains, - }, - })); - } - protected refreshTrafficData(account: ProcaptchaAccount): void { this.setState((actualState) => ({ ...actualState, @@ -338,8 +159,7 @@ class AppComponent extends React.Component { ); if (site) { - this.refreshUserData(site); - this.refreshSiteSettings(site.settings); + this.refreshUsage(site); this.refreshTrafficData(site.account); this.markAsLoaded(); @@ -361,14 +181,7 @@ class AppComponent extends React.Component { } public render() { - const { - statState, - usageInfo, - accountInformation, - captchaSettings, - domains, - trafficData, - } = this.state; + const { statState, usageInfo, trafficData } = this.state; return (
@@ -386,21 +199,6 @@ class AppComponent extends React.Component { image={usageInfo.image} pow={usageInfo.pow} /> - - - ; -} - -class ListComponent extends React.Component { - render() { - const { title, icon, items } = this.props; - - return ( - -
- {items.map((item, index) => ( - -

- {item.label} -

-

- {item.value} -

-
- ))} -
-
- ); - } -} - -export { ListComponent, ListComponentProperties }; diff --git a/assets/src/settings/statistics/config.ts b/assets/src/settings/statistics/config.ts index 82c6ee1..5bf8d9c 100644 --- a/assets/src/settings/statistics/config.ts +++ b/assets/src/settings/statistics/config.ts @@ -1,11 +1,5 @@ import Collection from "./collection.js"; -interface AccountLabels { - title: string; - name: string; - tier: string; -} - interface UsageLabels { title: string; total: string; @@ -20,28 +14,6 @@ interface StateLabels { loading: string; } -interface CaptchaSettingsLabels { - title: string; - type: string; - frictionlessThreshold: string; - frictionlessImageThreshold: string; - powDifficulty: string; - level: { - low: string; - normal: string; - high: string; - }; - types: { - proofOfWork: string; - image: string; - frictionless: string; - }; -} - -interface DomainLabels { - title: string; -} - interface TrafficDataLabels { title: string; chartTitle: string; @@ -56,16 +28,10 @@ interface Config { getSecretKey(): string; - getAccountLabels(): AccountLabels; - getUsageLabels(): UsageLabels; getStateLabels(): StateLabels; - getCaptchaSettingsLabels(): CaptchaSettingsLabels; - - getDomainLabels(): DomainLabels; - getTrafficDataLabels(): TrafficDataLabels; getCallToUpgradeElementMarkup(): string; @@ -102,16 +68,6 @@ class ConfigClass implements Config { return this.data.getString("secretKey"); } - public getAccountLabels(): AccountLabels { - const accountLabels = this.data.getSubCollection("accountLabels"); - - return { - title: accountLabels.getString("title"), - name: accountLabels.getString("name"), - tier: accountLabels.getString("tier"), - }; - } - public getUsageLabels(): UsageLabels { const usageLabels = this.data.getSubCollection("usageLabels"); @@ -138,45 +94,6 @@ class ConfigClass implements Config { }; } - public getCaptchaSettingsLabels(): CaptchaSettingsLabels { - const captchaSettingsLabels = this.data.getSubCollection( - "captchaSettingsLabels", - ); - - const level = captchaSettingsLabels.getSubCollection("level"); - const types = captchaSettingsLabels.getSubCollection("types"); - - return { - title: captchaSettingsLabels.getString("title"), - type: captchaSettingsLabels.getString("type"), - frictionlessThreshold: captchaSettingsLabels.getString( - "frictionlessThreshold", - ), - frictionlessImageThreshold: captchaSettingsLabels.getString( - "frictionlessImageThreshold", - ), - powDifficulty: captchaSettingsLabels.getString("powDifficulty"), - level: { - low: level.getString("low"), - normal: level.getString("normal"), - high: level.getString("high"), - }, - types: { - proofOfWork: types.getString("proofOfWork"), - image: types.getString("image"), - frictionless: types.getString("frictionless"), - }, - }; - } - - public getDomainLabels(): DomainLabels { - const domainLabels = this.data.getSubCollection("domainLabels"); - - return { - title: domainLabels.getString("title"), - }; - } - public getTrafficDataLabels(): TrafficDataLabels { const trafficDataLabels = this.data.getSubCollection("trafficDataLabels"); @@ -200,11 +117,4 @@ class ConfigClass implements Config { } } -export { - Config, - ConfigClass, - AccountLabels, - UsageLabels, - StateLabels, - TrafficDataLabels, -}; +export { Config, ConfigClass, UsageLabels, StateLabels, TrafficDataLabels }; diff --git a/prosopo-procaptcha/prosopo-procaptcha.php b/prosopo-procaptcha/prosopo-procaptcha.php index f2b1601..813d411 100644 --- a/prosopo-procaptcha/prosopo-procaptcha.php +++ b/prosopo-procaptcha/prosopo-procaptcha.php @@ -2,7 +2,7 @@ /** * Plugin Name: Prosopo Procaptcha * Description: GDPR compliant, privacy friendly and better value captcha. - * Version: 1.20.5 + * Version: 1.20.6 * Author: Prosopo Team * Author URI: https://prosopo.io/ * License: GPLv2 or later diff --git a/prosopo-procaptcha/readme.txt b/prosopo-procaptcha/readme.txt index d434ff1..1393d51 100644 --- a/prosopo-procaptcha/readme.txt +++ b/prosopo-procaptcha/readme.txt @@ -4,7 +4,7 @@ Tags: Captcha, Procaptcha, antispam, anibot, spam. Requires at least: 5.5 Tested up to: 6.9 Requires PHP: 7.4 -Stable tag: 1.20.5 +Stable tag: 1.20.6 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -135,6 +135,9 @@ Absolutely! The plugin has a [public GitHub repository](https://github.com/proso == Changelog == += 1.20.6 (2026-08-31) = +* Maintenance: simplify the statistics page to the monthly captcha counts and the traffic chart + = 1.20.5 (2026-08-31) = * Feature: show the frictionless score ladder's image challenge threshold on the statistics page diff --git a/prosopo-procaptcha/src/Settings/Statistics/Statistics_Settings_Tab.php b/prosopo-procaptcha/src/Settings/Statistics/Statistics_Settings_Tab.php index b528499..155b5ee 100644 --- a/prosopo-procaptcha/src/Settings/Statistics/Statistics_Settings_Tab.php +++ b/prosopo-procaptcha/src/Settings/Statistics/Statistics_Settings_Tab.php @@ -64,32 +64,7 @@ function ( Upgrade_Tier_Banner $model ) { return array( 'accountApiEndpoint' => Procaptcha_Plugin::ACCOUNT_API_ENDPOINT_URL, - 'accountLabels' => array( - 'name' => __( 'Name:', 'prosopo-procaptcha' ), - 'tier' => __( 'Tier:', 'prosopo-procaptcha' ), - 'title' => __( 'Account Information', 'prosopo-procaptcha' ), - ), 'callToUpgradeElementMarkup' => $call_to_upgrade_element_markup, - 'captchaSettingsLabels' => array( - 'frictionlessImageThreshold' => __( 'Image Challenge Threshold:', 'prosopo-procaptcha' ), - 'frictionlessThreshold' => __( 'Frictionless Threshold:', 'prosopo-procaptcha' ), - 'level' => array( - 'high' => __( 'High', 'prosopo-procaptcha' ), - 'low' => __( 'Low', 'prosopo-procaptcha' ), - 'normal' => __( 'Normal', 'prosopo-procaptcha' ), - ), - 'powDifficulty' => __( 'Proof of Work Difficulty:', 'prosopo-procaptcha' ), - 'title' => __( 'Captcha Settings', 'prosopo-procaptcha' ), - 'type' => __( 'Type:', 'prosopo-procaptcha' ), - 'types' => array( - 'frictionless' => __( 'Frictionless', 'prosopo-procaptcha' ), - 'image' => __( 'Image', 'prosopo-procaptcha' ), - 'proofOfWork' => __( 'Proof of Work', 'prosopo-procaptcha' ), - ), - ), - 'domainLabels' => array( - 'title' => __( 'Whitelisted Domains', 'prosopo-procaptcha' ), - ), 'isDebugMode' => false, // todo move into settings as 'debug mode' option. 'secretKey' => $secret_key, 'siteKey' => $site_key,