From 4b6f41e0a590afb92e1413d961d95be814696183 Mon Sep 17 00:00:00 2001 From: Petr Parimucha Date: Fri, 4 Sep 2026 15:13:56 +0200 Subject: [PATCH 1/2] fix(schema): accept readonly and disabled where ACF renders them ACF Pro passes `readonly` and `disabled` into the rendered input for ten field types, but no schema declared either key and `field-item.schema.json` sets `unevaluatedProperties: false`. A field group using ACF's own read-only inputs therefore failed validation, and the package has no local ignore to fall back on -- so the consuming project had no lint-clean option short of removing configuration that works. Verified against ACF Pro 6.x rather than the documentation, which does not list either setting: text class-acf-field-text.php:71 textarea class-acf-field-textarea.php:59 number class-acf-field-number.php:61 range class-acf-field-range.php:60 email class-acf-field-email.php:58 url class-acf-field-url.php:52 select class-acf-field-select.php:283-287 date_picker class-acf-field-date-picker.php:128 date_time_picker class-acf-field-date-time-picker.php:141 time_picker class-acf-field-time-picker.php:78 Added to those ten refs only, NOT to `refs/field.schema.json`. The base schema would accept the keys on `image`, `repeater`, `true_false` and every other type, where ACF silently ignores them -- exactly the dead configuration this package reports elsewhere (the WPML repeater message says so in as many words). `enum: [0, 1]` matches `required` and `allow_in_bindings` in the base schema; ACF truthy-checks the value and serialises 0/1. Both copies edited, template and distribution, per the source-of-truth rule in AGENTS.md. No new field type, so `FIELD_TYPE_ORDER` and the generated root schemas are untouched. Fixtures cover both halves of the decision: a valid group carrying `readonly`/`disabled` on text, number, select and date_picker, and an invalid one putting `readonly` on an image. The second is the load-bearing one -- without it a later move of these keys into the base schema would pass every test. Checked by negative control that the invalid fixture fails ONLY on this key: removing `readonly` from it makes the same document validate. Found while linting a real project (fellows), whose `flat` field group marks two import-owned fields read-only; that file validates against the patched schemas. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012GJnbedTAFFLZX5Z8MEnTB (petr@pari.cz) --- CHANGELOG.md | 12 ++++ schemas/refs/field-date_picker.schema.json | 4 +- .../refs/field-date_time_picker.schema.json | 4 +- schemas/refs/field-email.schema.json | 6 +- schemas/refs/field-number.schema.json | 4 +- schemas/refs/field-range.schema.json | 4 +- schemas/refs/field-select.schema.json | 4 +- schemas/refs/field-text.schema.json | 4 +- schemas/refs/field-textarea.schema.json | 4 +- schemas/refs/field-time_picker.schema.json | 4 +- schemas/refs/field-url.schema.json | 6 +- .../refs/field-date_picker.schema.json | 4 +- .../refs/field-date_time_picker.schema.json | 4 +- src/templates/refs/field-email.schema.json | 6 +- src/templates/refs/field-number.schema.json | 4 +- src/templates/refs/field-range.schema.json | 4 +- src/templates/refs/field-select.schema.json | 4 +- src/templates/refs/field-text.schema.json | 4 +- src/templates/refs/field-textarea.schema.json | 4 +- .../refs/field-time_picker.schema.json | 4 +- src/templates/refs/field-url.schema.json | 6 +- .../invalid/readonly-on-image/acf.json | 29 ++++++++++ .../invalid/readonly-on-image/assert.json | 5 ++ .../starter_theme/readonly-inputs/acf.json | 57 +++++++++++++++++++ 24 files changed, 171 insertions(+), 20 deletions(-) create mode 100644 tests/fixtures/invalid/readonly-on-image/acf.json create mode 100644 tests/fixtures/invalid/readonly-on-image/assert.json create mode 100644 tests/fixtures/valid/starter_theme/readonly-inputs/acf.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a50b9..68d2499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **`readonly` and `disabled` are now valid on the field types that render + them.** ACF Pro passes both into the rendered input for `text`, `textarea`, + `number`, `range`, `email`, `url`, `select`, `date_picker`, + `date_time_picker` and `time_picker`, but no schema declared either key and + `field-item.schema.json` sets `unevaluatedProperties: false` — so a field + group using ACF's own read-only inputs failed validation, with no local + ignore to fall back on. Added per type rather than to the base field schema: + ACF ignores both on `image`, `repeater`, `true_false` and the rest, and + accepting them there would let dead configuration ship silently. + ## [0.7.5] - 2026-08-12 ### Fixed diff --git a/schemas/refs/field-date_picker.schema.json b/schemas/refs/field-date_picker.schema.json index af6587e..dea1317 100644 --- a/schemas/refs/field-date_picker.schema.json +++ b/schemas/refs/field-date_picker.schema.json @@ -6,6 +6,8 @@ "properties": { "display_format": { "type": "string" }, "return_format": { "type": "string" }, - "first_day": { "type": ["integer", "string"] } + "first_day": { "type": ["integer", "string"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-date_time_picker.schema.json b/schemas/refs/field-date_time_picker.schema.json index 484d11d..939bb36 100644 --- a/schemas/refs/field-date_time_picker.schema.json +++ b/schemas/refs/field-date_time_picker.schema.json @@ -6,6 +6,8 @@ "properties": { "display_format": { "type": "string" }, "return_format": { "type": "string" }, - "first_day": { "type": ["integer", "string"] } + "first_day": { "type": ["integer", "string"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-email.schema.json b/schemas/refs/field-email.schema.json index 6859f10..1ee739b 100644 --- a/schemas/refs/field-email.schema.json +++ b/schemas/refs/field-email.schema.json @@ -2,5 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.parisek.dev/acf/refs/field-email.schema.json", "title": "ACF Field — Email", - "type": "object" + "type": "object", + "properties": { + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } + } } diff --git a/schemas/refs/field-number.schema.json b/schemas/refs/field-number.schema.json index 7f51485..3e3d10b 100644 --- a/schemas/refs/field-number.schema.json +++ b/schemas/refs/field-number.schema.json @@ -6,6 +6,8 @@ "properties": { "min": { "type": ["number", "string", "null"], "description": "ACF stores an unset bound as an empty string." }, "max": { "type": ["number", "string", "null"] }, - "step": { "type": ["number", "string", "null"] } + "step": { "type": ["number", "string", "null"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-range.schema.json b/schemas/refs/field-range.schema.json index ed457fb..c683883 100644 --- a/schemas/refs/field-range.schema.json +++ b/schemas/refs/field-range.schema.json @@ -6,6 +6,8 @@ "properties": { "min": { "type": ["number", "string", "null"], "description": "ACF stores an unset bound as an empty string." }, "max": { "type": ["number", "string", "null"] }, - "step": { "type": ["number", "string", "null"] } + "step": { "type": ["number", "string", "null"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-select.schema.json b/schemas/refs/field-select.schema.json index 473b7a1..164fc31 100644 --- a/schemas/refs/field-select.schema.json +++ b/schemas/refs/field-select.schema.json @@ -19,6 +19,8 @@ "return_format": { "enum": ["value", "label", "array"] }, "placeholder": { "type": "string" }, "create_options": { "enum": [0, 1], "description": "Allow editors to create new option values." }, - "save_options": { "enum": [0, 1], "description": "Save newly created options to choices." } + "save_options": { "enum": [0, 1], "description": "Save newly created options to choices." }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-text.schema.json b/schemas/refs/field-text.schema.json index 3f9dbdb..d27efb2 100644 --- a/schemas/refs/field-text.schema.json +++ b/schemas/refs/field-text.schema.json @@ -4,6 +4,8 @@ "title": "ACF Field — Text", "type": "object", "properties": { - "maxlength": { "type": ["string", "integer", "null"] } + "maxlength": { "type": ["string", "integer", "null"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-textarea.schema.json b/schemas/refs/field-textarea.schema.json index 4df00c2..64e412e 100644 --- a/schemas/refs/field-textarea.schema.json +++ b/schemas/refs/field-textarea.schema.json @@ -6,6 +6,8 @@ "properties": { "maxlength": { "type": ["string", "integer", "null"] }, "rows": { "type": ["string", "integer", "null"] }, - "new_lines": { "enum": ["wpautop", "br", ""] } + "new_lines": { "enum": ["wpautop", "br", ""] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-time_picker.schema.json b/schemas/refs/field-time_picker.schema.json index 77a5ea8..9261ef9 100644 --- a/schemas/refs/field-time_picker.schema.json +++ b/schemas/refs/field-time_picker.schema.json @@ -5,6 +5,8 @@ "type": "object", "properties": { "display_format": { "type": "string" }, - "return_format": { "type": "string" } + "return_format": { "type": "string" }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/schemas/refs/field-url.schema.json b/schemas/refs/field-url.schema.json index 0d227ab..348ca37 100644 --- a/schemas/refs/field-url.schema.json +++ b/schemas/refs/field-url.schema.json @@ -2,5 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.parisek.dev/acf/refs/field-url.schema.json", "title": "ACF Field — URL", - "type": "object" + "type": "object", + "properties": { + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } + } } diff --git a/src/templates/refs/field-date_picker.schema.json b/src/templates/refs/field-date_picker.schema.json index af6587e..dea1317 100644 --- a/src/templates/refs/field-date_picker.schema.json +++ b/src/templates/refs/field-date_picker.schema.json @@ -6,6 +6,8 @@ "properties": { "display_format": { "type": "string" }, "return_format": { "type": "string" }, - "first_day": { "type": ["integer", "string"] } + "first_day": { "type": ["integer", "string"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-date_time_picker.schema.json b/src/templates/refs/field-date_time_picker.schema.json index 484d11d..939bb36 100644 --- a/src/templates/refs/field-date_time_picker.schema.json +++ b/src/templates/refs/field-date_time_picker.schema.json @@ -6,6 +6,8 @@ "properties": { "display_format": { "type": "string" }, "return_format": { "type": "string" }, - "first_day": { "type": ["integer", "string"] } + "first_day": { "type": ["integer", "string"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-email.schema.json b/src/templates/refs/field-email.schema.json index 6859f10..1ee739b 100644 --- a/src/templates/refs/field-email.schema.json +++ b/src/templates/refs/field-email.schema.json @@ -2,5 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.parisek.dev/acf/refs/field-email.schema.json", "title": "ACF Field — Email", - "type": "object" + "type": "object", + "properties": { + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } + } } diff --git a/src/templates/refs/field-number.schema.json b/src/templates/refs/field-number.schema.json index 7f51485..3e3d10b 100644 --- a/src/templates/refs/field-number.schema.json +++ b/src/templates/refs/field-number.schema.json @@ -6,6 +6,8 @@ "properties": { "min": { "type": ["number", "string", "null"], "description": "ACF stores an unset bound as an empty string." }, "max": { "type": ["number", "string", "null"] }, - "step": { "type": ["number", "string", "null"] } + "step": { "type": ["number", "string", "null"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-range.schema.json b/src/templates/refs/field-range.schema.json index ed457fb..c683883 100644 --- a/src/templates/refs/field-range.schema.json +++ b/src/templates/refs/field-range.schema.json @@ -6,6 +6,8 @@ "properties": { "min": { "type": ["number", "string", "null"], "description": "ACF stores an unset bound as an empty string." }, "max": { "type": ["number", "string", "null"] }, - "step": { "type": ["number", "string", "null"] } + "step": { "type": ["number", "string", "null"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-select.schema.json b/src/templates/refs/field-select.schema.json index 473b7a1..164fc31 100644 --- a/src/templates/refs/field-select.schema.json +++ b/src/templates/refs/field-select.schema.json @@ -19,6 +19,8 @@ "return_format": { "enum": ["value", "label", "array"] }, "placeholder": { "type": "string" }, "create_options": { "enum": [0, 1], "description": "Allow editors to create new option values." }, - "save_options": { "enum": [0, 1], "description": "Save newly created options to choices." } + "save_options": { "enum": [0, 1], "description": "Save newly created options to choices." }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-text.schema.json b/src/templates/refs/field-text.schema.json index 3f9dbdb..d27efb2 100644 --- a/src/templates/refs/field-text.schema.json +++ b/src/templates/refs/field-text.schema.json @@ -4,6 +4,8 @@ "title": "ACF Field — Text", "type": "object", "properties": { - "maxlength": { "type": ["string", "integer", "null"] } + "maxlength": { "type": ["string", "integer", "null"] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-textarea.schema.json b/src/templates/refs/field-textarea.schema.json index 4df00c2..64e412e 100644 --- a/src/templates/refs/field-textarea.schema.json +++ b/src/templates/refs/field-textarea.schema.json @@ -6,6 +6,8 @@ "properties": { "maxlength": { "type": ["string", "integer", "null"] }, "rows": { "type": ["string", "integer", "null"] }, - "new_lines": { "enum": ["wpautop", "br", ""] } + "new_lines": { "enum": ["wpautop", "br", ""] }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-time_picker.schema.json b/src/templates/refs/field-time_picker.schema.json index 77a5ea8..9261ef9 100644 --- a/src/templates/refs/field-time_picker.schema.json +++ b/src/templates/refs/field-time_picker.schema.json @@ -5,6 +5,8 @@ "type": "object", "properties": { "display_format": { "type": "string" }, - "return_format": { "type": "string" } + "return_format": { "type": "string" }, + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } } } diff --git a/src/templates/refs/field-url.schema.json b/src/templates/refs/field-url.schema.json index 0d227ab..348ca37 100644 --- a/src/templates/refs/field-url.schema.json +++ b/src/templates/refs/field-url.schema.json @@ -2,5 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.parisek.dev/acf/refs/field-url.schema.json", "title": "ACF Field — URL", - "type": "object" + "type": "object", + "properties": { + "readonly": { "enum": [0, 1], "description": "ACF renders this into the input's readonly attribute." }, + "disabled": { "enum": [0, 1], "description": "ACF renders this into the input's disabled attribute." } + } } diff --git a/tests/fixtures/invalid/readonly-on-image/acf.json b/tests/fixtures/invalid/readonly-on-image/acf.json new file mode 100644 index 0000000..439d443 --- /dev/null +++ b/tests/fixtures/invalid/readonly-on-image/acf.json @@ -0,0 +1,29 @@ +{ + "key": "group_test_readonly_on_image", + "title": "Test: readonly on an image field (forbidden)", + "fields": [ + { + "key": "field_test_image_readonly", + "label": "Image", + "name": "image", + "type": "image", + "allow_in_bindings": 0, + "return_format": "array", + "readonly": 1, + "wpml_cf_preferences": 1 + } + ], + "location": [ + [ + { + "param": "post_type", + "operator": "==", + "value": "page" + } + ] + ], + "menu_order": 0, + "active": true, + "modified": 1716000000, + "acfml_field_group_mode": "advanced" +} diff --git a/tests/fixtures/invalid/readonly-on-image/assert.json b/tests/fixtures/invalid/readonly-on-image/assert.json new file mode 100644 index 0000000..266e9d2 --- /dev/null +++ b/tests/fixtures/invalid/readonly-on-image/assert.json @@ -0,0 +1,5 @@ +{ + "description": "readonly and disabled are accepted only on the field types whose ACF render method passes them into the input (text, textarea, number, range, email, url, select, date_picker, date_time_picker, time_picker). ACF ignores them on an image field, so accepting the key here would let dead configuration ship silently.", + "schema": "https://schemas.parisek.dev/acf/acf.schema.json", + "min_violations": 1 +} diff --git a/tests/fixtures/valid/starter_theme/readonly-inputs/acf.json b/tests/fixtures/valid/starter_theme/readonly-inputs/acf.json new file mode 100644 index 0000000..9c6fb76 --- /dev/null +++ b/tests/fixtures/valid/starter_theme/readonly-inputs/acf.json @@ -0,0 +1,57 @@ +{ + "key": "group_test_readonly_inputs", + "title": "Test: readonly / disabled on input field types", + "fields": [ + { + "key": "field_test_readonly_text", + "label": "Text", + "name": "readonly_text", + "type": "text", + "allow_in_bindings": 0, + "readonly": 1, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_readonly_number", + "label": "Number", + "name": "readonly_number", + "type": "number", + "allow_in_bindings": 0, + "readonly": 1, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_disabled_select", + "label": "Select", + "name": "disabled_select", + "type": "select", + "allow_in_bindings": 0, + "return_format": "value", + "disabled": 1, + "wpml_cf_preferences": 1 + }, + { + "key": "field_test_readonly_date", + "label": "Date", + "name": "readonly_date", + "type": "date_picker", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 0, + "wpml_cf_preferences": 2 + } + ], + "location": [ + [ + { + "param": "post_type", + "operator": "==", + "value": "page" + } + ] + ], + "menu_order": 0, + "active": true, + "modified": 1716000000, + "acfml_field_group_mode": "advanced" +} From ccf9c29d0f1e8ee23a5b9a4f3deda8159a279e06 Mon Sep 17 00:00:00 2001 From: Petr Parimucha Date: Fri, 4 Sep 2026 16:02:24 +0200 Subject: [PATCH 2/2] fix(schema): add password, and cover every accepted type in the fixture Two independent reviews of the first commit. Both findings below were verified against the ACF source before acting. PASSWORD WAS MISSING. `class-acf-field-password.php:52` delegates its whole render to the text field, which is where `readonly`/`disabled` are passed into the input -- so password honours both and the schema still rejected them. The first commit's evidence was gathered by grepping each field class for the literal `'readonly'`, which cannot see a type that inherits the behaviour. Checked the whole directory for that pattern afterwards: `password` is the only delegating type, so the blind spot cost exactly one. Control run rather than assumed: a password field with `readonly: 1` validates against the patched schemas and fails against the previous commit's, with the error naming the key. FIXTURE now covers all eleven accepted types instead of four. The previous fixture would have stayed green with password missing, which is how the gap survived the first round. Invalid fixture renamed `readonly-on-image` -> `image-readonly-prop`, to match the `-` convention every sibling uses (`image-return-format-url`, `file-unknown-prop`, `button_group-bad-layout`). CHANGELOG now cites the ACF source behind the type list and names the downstream project, as the 0.7.3 and 0.7.5 entries do, and warns that the list is a claim about ACF internals that needs re-checking on upgrades. CONSIDERED AND NOT CHANGED, both raised as findings: - `readonly` on `select` is inert in the browser: ACF sets the attribute (`class-acf-field-select.php:283`) but HTML ignores `readonly` on `