Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 85 additions & 8 deletions assets/src/settings/procaptcha/procaptchaSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
type ProcaptchaAccount,
procaptchaAccountSchema,
} from "#settings/procaptcha/procaptchaAccount.js";
import { z, type ZodType } from "zod";
import { z, type ZodType, type ZodTypeDef } from "zod";

export interface ProcaptchaSite {
account: ProcaptchaAccount;
Expand All @@ -16,7 +16,22 @@ 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[];
Expand All @@ -28,12 +43,74 @@ export interface CaptchaUsage {
total: number;
}

export const siteSettingsSchema = z.object({
frictionlessThreshold: z.number(),
powDifficulty: z.number(),
captchaType: z.string(),
domains: z.string().array(),
}) satisfies ZodType<SiteSettings>;
/**
* `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<ProcaptchaSite, "settings"> {
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<SiteSettings, ZodTypeDef, SiteSettingsInput>;

export const captchaUsageSchema = z.object({
submissions: z.number(),
Expand All @@ -50,4 +127,4 @@ export const procaptchaSiteSchema = z.object({
image: captchaUsageSchema,
pow: captchaUsageSchema,
}),
}) satisfies ZodType<ProcaptchaSite>;
}) satisfies ZodType<ProcaptchaSite, ZodTypeDef, ProcaptchaSiteInput>;
43 changes: 43 additions & 0 deletions assets/src/settings/statistics/components/appComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ class AppComponent extends React.Component<AppComponentProperties, AppState> {
.frictionlessThreshold,
value: "...",
},
{
label: this.config.getCaptchaSettingsLabels()
.frictionlessImageThreshold,
value: "...",
},
{
label: this.config.getCaptchaSettingsLabels()
.powDifficulty,
Expand Down Expand Up @@ -221,6 +226,37 @@ class AppComponent extends React.Component<AppComponentProperties, AppState> {
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;

Expand Down Expand Up @@ -260,6 +296,13 @@ class AppComponent extends React.Component<AppComponentProperties, AppState> {
siteSettings.frictionlessThreshold,
),
},
{
label: this.config.getCaptchaSettingsLabels()
.frictionlessImageThreshold,
value: this.getFrictionlessImageThresholdLabel(
siteSettings.frictionlessImageThreshold,
),
},
{
label: this.config.getCaptchaSettingsLabels()
.powDifficulty,
Expand Down
4 changes: 4 additions & 0 deletions assets/src/settings/statistics/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface CaptchaSettingsLabels {
title: string;
type: string;
frictionlessThreshold: string;
frictionlessImageThreshold: string;
powDifficulty: string;
level: {
low: string;
Expand Down Expand Up @@ -151,6 +152,9 @@ class ConfigClass implements Config {
frictionlessThreshold: captchaSettingsLabels.getString(
"frictionlessThreshold",
),
frictionlessImageThreshold: captchaSettingsLabels.getString(
"frictionlessImageThreshold",
),
powDifficulty: captchaSettingsLabels.getString("powDifficulty"),
level: {
low: level.getString("low"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,17 @@ function ( Upgrade_Tier_Banner $model ) {
),
'callToUpgradeElementMarkup' => $call_to_upgrade_element_markup,
'captchaSettingsLabels' => array(
'frictionlessThreshold' => __( 'Frictionless Threshold:', 'prosopo-procaptcha' ),
'level' => 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(
'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' ),
Expand Down
Loading