diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a50b9..2de2bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ 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`, `password`, `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. Grounded in + `includes/fields/class-acf-field-text.php:71` (which `password` reaches via + `class-acf-field-password.php:52`, delegating its whole render to `text`), + `class-acf-field-select.php:283`, and the `$keys2` list each remaining type + passes to `acf_esc_attrs()`. 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. Found + downstream on `fellows`, whose `flat` group marks two import-owned fields + read-only. + Note for future ACF upgrades: this is a per-type list and it will need + re-checking when ACF changes which types render these attributes. + ## [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-password.schema.json b/schemas/refs/field-password.schema.json index 5739679..e875f92 100644 --- a/schemas/refs/field-password.schema.json +++ b/schemas/refs/field-password.schema.json @@ -2,5 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.parisek.dev/acf/refs/field-password.schema.json", "title": "ACF Field — Password", - "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-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-password.schema.json b/src/templates/refs/field-password.schema.json index 5739679..e875f92 100644 --- a/src/templates/refs/field-password.schema.json +++ b/src/templates/refs/field-password.schema.json @@ -2,5 +2,9 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.parisek.dev/acf/refs/field-password.schema.json", "title": "ACF Field — Password", - "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-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/image-readonly-prop/acf.json b/tests/fixtures/invalid/image-readonly-prop/acf.json new file mode 100644 index 0000000..439d443 --- /dev/null +++ b/tests/fixtures/invalid/image-readonly-prop/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/image-readonly-prop/assert.json b/tests/fixtures/invalid/image-readonly-prop/assert.json new file mode 100644 index 0000000..266e9d2 --- /dev/null +++ b/tests/fixtures/invalid/image-readonly-prop/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..31309bf --- /dev/null +++ b/tests/fixtures/valid/starter_theme/readonly-inputs/acf.json @@ -0,0 +1,130 @@ +{ + "key": "group_test_readonly_inputs", + "title": "Test: readonly / disabled on every input type that renders them", + "fields": [ + { + "key": "field_test_ro_text", + "label": "text", + "name": "ro_text", + "type": "text", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 0, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_textarea", + "label": "textarea", + "name": "ro_textarea", + "type": "textarea", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 1, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_number", + "label": "number", + "name": "ro_number", + "type": "number", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 0, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_range", + "label": "range", + "name": "ro_range", + "type": "range", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 1, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_email", + "label": "email", + "name": "ro_email", + "type": "email", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 0, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_url", + "label": "url", + "name": "ro_url", + "type": "url", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 1, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_password", + "label": "password", + "name": "ro_password", + "type": "password", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 0, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_select", + "label": "select", + "name": "ro_select", + "type": "select", + "allow_in_bindings": 0, + "return_format": "value", + "readonly": 1, + "disabled": 1, + "wpml_cf_preferences": 1 + }, + { + "key": "field_test_ro_date", + "label": "date_picker", + "name": "ro_date", + "type": "date_picker", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 0, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_date_time", + "label": "date_time_picker", + "name": "ro_date_time", + "type": "date_time_picker", + "allow_in_bindings": 0, + "readonly": 1, + "disabled": 1, + "wpml_cf_preferences": 2 + }, + { + "key": "field_test_ro_time", + "label": "time_picker", + "name": "ro_time", + "type": "time_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" +}