diff --git a/schemas/ui-options-page.schema.json b/schemas/ui-options-page.schema.json new file mode 100644 index 00000000..0bf81304 --- /dev/null +++ b/schemas/ui-options-page.schema.json @@ -0,0 +1,187 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/WordPress/secure-custom-fields/trunk/schemas/ui-options-page.schema.json", + "title": "SCF UI Options Page(s)", + "description": "Schema for Secure Custom Fields UI Options Page definitions - accepts single object or array. Properties marked '[SCF Export Only]' are preserved during export but not used functionally during import.", + "oneOf": [ + { + "description": "Single UI Options Page object", + "$ref": "#/definitions/uiOptionsPage" + }, + { + "description": "Array of UI Options Page objects (export format)", + "type": "array", + "items": { "$ref": "#/definitions/uiOptionsPage" }, + "minItems": 1 + } + ], + "definitions": { + "uiOptionsPage": { + "type": "object", + "required": [ "key", "title", "menu_slug" ], + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "pattern": "^ui_options_page_.+$", + "minLength": 1, + "description": "Unique identifier for the options page with ui_options_page_ prefix (e.g. 'ui_options_page_site_settings')" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "The title/name of the options page" + }, + "menu_slug": { + "type": "string", + "pattern": "^[a-z0-9_-]+$", + "minLength": 1, + "description": "The menu slug used in the admin URL (e.g. 'site-settings'). Lowercase letters, numbers, underscores, and dashes only." + }, + + "page_title": { + "type": "string", + "description": "The page title displayed in the browser tab and page heading" + }, + "parent_slug": { + "type": "string", + "description": "The parent menu slug. Use 'none' for top-level menu, or a WordPress admin menu slug (e.g. 'options-general.php') for submenu." + }, + "menu_title": { + "type": "string", + "description": "The title displayed in the admin menu" + }, + + "active": { + "type": "boolean", + "default": true, + "description": "[SCF] Whether this options page is active" + }, + "advanced_configuration": { + "type": [ "boolean", "integer" ], + "default": false, + "description": "[SCF Export Only] Whether advanced configuration options are enabled. Accepts boolean or integer (0/1)." + }, + "import_source": { + "type": "string", + "description": "[SCF Export Only] Source of import if this options page was imported" + }, + "import_date": { + "type": "string", + "description": "[SCF Export Only] Date when this options page was imported" + }, + "modified": { + "type": "integer", + "minimum": 0, + "description": "[SCF Export Only] Unix timestamp of last modification" + }, + "menu_order": { + "type": "integer", + "minimum": 0, + "default": 0, + "description": "[SCF Export Only] The order of this options page in the admin menu" + }, + + "icon_url": { + "type": "string", + "description": "[Legacy] Icon URL or dashicon class. Prefer menu_icon object format for new configurations." + }, + "menu_icon": { + "oneOf": [ + { + "type": "string", + "description": "Icon as string: Dashicon name (e.g. 'dashicons-admin-generic') or full URL to image file" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ "dashicons", "url", "media_library" ], + "description": "Icon source type: 'dashicons' for WordPress dashicons, 'url' for custom image URL, 'media_library' for media library attachment" + }, + "value": { + "type": [ "string", "integer" ], + "description": "The icon value: dashicon class name, image URL, or media library attachment ID" + } + }, + "required": [ "type", "value" ], + "additionalProperties": false, + "description": "[SCF] SCF icon object format: {\"type\": \"dashicons\", \"value\": \"dashicons-admin-generic\"}" + } + ], + "description": "The menu icon. Can be a string (URL or dashicon name) or SCF object format with type and value properties." + }, + "position": { + "type": [ "integer", "string", "null" ], + "description": "The position in the menu where this page should appear. SCF exports as empty string or null when not set, integer when set." + }, + "redirect": { + "type": "boolean", + "default": true, + "description": "When child pages exist for this parent page, whether to redirect to the first child page" + }, + "description": { + "type": "string", + "description": "A descriptive summary of the options page" + }, + + "update_button": { + "type": "string", + "description": "The label used for the submit button which updates the fields on the options page" + }, + "updated_message": { + "type": "string", + "description": "The message displayed after successfully updating the options page" + }, + + "capability": { + "type": "string", + "default": "edit_posts", + "description": "The capability required for this menu to be displayed to the user" + }, + "data_storage": { + "type": "string", + "enum": [ "options", "post_id" ], + "default": "options", + "description": "Where to store field data. 'options' uses the options table, 'post_id' uses custom storage." + }, + "post_id": { + "type": [ "string", "integer" ], + "description": "Custom storage location when data_storage is 'post_id'. Can be a numeric post ID (123) or a string ('user_2')." + }, + "autoload": { + "type": "boolean", + "default": false, + "description": "Whether to autoload the options when WordPress loads. Improves performance for frequently accessed options." + } + } + } + }, + "examples": [ + { + "key": "ui_options_page_site_settings", + "title": "Site Settings", + "menu_slug": "site-settings", + "page_title": "Site Settings", + "parent_slug": "none", + "menu_title": "Site Settings", + "active": true, + "menu_order": 0, + "advanced_configuration": false, + "menu_icon": { + "type": "dashicons", + "value": "dashicons-admin-generic" + }, + "position": 80, + "redirect": true, + "description": "Global site configuration options", + "update_button": "Save Settings", + "updated_message": "Settings saved successfully", + "capability": "manage_options", + "data_storage": "options", + "autoload": true + } + ] +} diff --git a/tests/php/fixtures/schemas/ui-options-pages/invalid/ui-options-page-wrong-format.json b/tests/php/fixtures/schemas/ui-options-pages/invalid/ui-options-page-wrong-format.json new file mode 100644 index 00000000..3c5833b7 --- /dev/null +++ b/tests/php/fixtures/schemas/ui-options-pages/invalid/ui-options-page-wrong-format.json @@ -0,0 +1,15 @@ +{ + "key": "group_theme_settings_options_page", + "title": "Theme Settings", + "type": "options_page", + "menu_slug": "theme-settings", + "menu_title": "Theme Settings", + "parent_slug": "", + "position": "", + "icon_url": "", + "redirect": false, + "post_id": "option", + "autoload": false, + "capability": "manage_options", + "updated_at": "2025-12-03 00:00:00" +} diff --git a/tests/php/fixtures/schemas/ui-options-pages/valid/site-settings-export.json b/tests/php/fixtures/schemas/ui-options-pages/valid/site-settings-export.json new file mode 100644 index 00000000..c5b59dff --- /dev/null +++ b/tests/php/fixtures/schemas/ui-options-pages/valid/site-settings-export.json @@ -0,0 +1,27 @@ +{ + "key": "ui_options_page_682abc123def", + "title": "Site Settings", + "menu_order": 0, + "active": true, + "page_title": "Site Settings", + "menu_slug": "site-settings", + "parent_slug": "none", + "advanced_configuration": 0, + "import_source": "", + "import_date": "", + "icon_url": "", + "menu_title": "Site Settings", + "position": 80, + "redirect": true, + "description": "Global site configuration options", + "menu_icon": { + "type": "dashicons", + "value": "dashicons-admin-generic" + }, + "update_button": "Save Settings", + "updated_message": "Settings saved successfully", + "capability": "manage_options", + "data_storage": "options", + "post_id": "", + "autoload": true +} diff --git a/tests/php/schemas/UIOptionsPageSchemaTest.php b/tests/php/schemas/UIOptionsPageSchemaTest.php new file mode 100644 index 00000000..2f25770d --- /dev/null +++ b/tests/php/schemas/UIOptionsPageSchemaTest.php @@ -0,0 +1,525 @@ + array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + ), + 'Minimal options page should validate successfully', + ), + 'array with two items' => array( + array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + ), + array( + 'key' => 'ui_options_page_advanced', + 'title' => 'Advanced', + 'menu_slug' => 'advanced', + ), + ), + 'Array of two options pages should validate successfully', + ), + 'with dashes in menu_slug' => array( + array( + 'key' => 'ui_options_page_site_settings', + 'title' => 'Site Settings', + 'menu_slug' => 'site-settings', + ), + 'Options page with dashes in menu_slug should be valid', + ), + 'with underscores in menu_slug' => array( + array( + 'key' => 'ui_options_page_site_settings', + 'title' => 'Site Settings', + 'menu_slug' => 'site_settings', + ), + 'Options page with underscores in menu_slug should be valid', + ), + 'with numbers in menu_slug' => array( + array( + 'key' => 'ui_options_page_settings2', + 'title' => 'Settings 2', + 'menu_slug' => 'settings2', + ), + 'Options page with numbers in menu_slug should be valid', + ), + 'parent page with menu_icon object' => array( + array( + 'key' => 'ui_options_page_parent', + 'title' => 'Parent Page', + 'menu_slug' => 'parent-page', + 'parent_slug' => 'none', + 'menu_icon' => array( + 'type' => 'dashicons', + 'value' => 'dashicons-admin-settings', + ), + 'position' => 80, + ), + 'Parent page with menu_icon object should be valid', + ), + 'menu_icon as string' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'menu_icon' => 'dashicons-admin-generic', + ), + 'Options page with menu_icon as string should be valid', + ), + 'menu_icon with url type' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'menu_icon' => array( + 'type' => 'url', + 'value' => 'https://example.com/icon.png', + ), + ), + 'Options page with menu_icon url type should be valid', + ), + 'menu_icon with media_library type' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'menu_icon' => array( + 'type' => 'media_library', + 'value' => 123, + ), + ), + 'Options page with menu_icon media_library type should be valid', + ), + 'child page with parent_slug' => array( + array( + 'key' => 'ui_options_page_child', + 'title' => 'Child Page', + 'menu_slug' => 'child-page', + 'parent_slug' => 'parent-page', + ), + 'Child page with parent_slug should be valid', + ), + 'with custom capability' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'capability' => 'manage_options', + ), + 'Options page with custom capability should be valid', + ), + 'with data_storage options' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'data_storage' => 'options', + ), + 'Options page with data_storage options should be valid', + ), + 'with data_storage post_id' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'data_storage' => 'post_id', + 'post_id' => 'user_2', + ), + 'Options page with custom post_id storage should be valid', + ), + 'with post_id as integer' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'data_storage' => 'post_id', + 'post_id' => 123, + ), + 'Options page with numeric post_id should be valid', + ), + 'redirect true' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'redirect' => true, + ), + 'Options page with redirect true should be valid', + ), + 'redirect false' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'redirect' => false, + ), + 'Options page with redirect false should be valid', + ), + 'autoload true' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'autoload' => true, + ), + 'Options page with autoload true should be valid', + ), + 'position as integer' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'position' => 80, + ), + 'Options page with position as integer should be valid', + ), + 'position as empty string' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'position' => '', + ), + 'Options page with position as empty string should be valid', + ), + 'position as null' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'position' => null, + ), + 'Options page with position as null should be valid', + ), + 'advanced_configuration as integer' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'advanced_configuration' => 1, + ), + 'Options page with advanced_configuration as integer should be valid', + ), + 'advanced_configuration as boolean' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'advanced_configuration' => true, + ), + 'Options page with advanced_configuration as boolean should be valid', + ), + 'with all optional string fields' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'page_title' => 'Site Settings Page', + 'menu_title' => 'Settings', + 'description' => 'A description of this options page', + 'update_button' => 'Save Changes', + 'updated_message' => 'Settings saved successfully', + 'icon_url' => 'dashicons-admin-settings', + ), + 'Options page with all optional string fields should be valid', + ), + 'with export metadata fields' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'import_source' => 'local', + 'import_date' => '2024-01-15', + 'modified' => 1705334400, + ), + 'Options page with export metadata fields should be valid', + ), + 'full options page with all fields' => array( + array( + 'key' => 'ui_options_page_full_example', + 'title' => 'Full Example', + 'menu_slug' => 'full-example', + 'page_title' => 'Full Example Page', + 'parent_slug' => 'none', + 'menu_title' => 'Full Example', + 'active' => true, + 'advanced_configuration' => 0, + 'import_source' => '', + 'import_date' => '', + 'modified' => 1701619200, + 'menu_order' => 5, + 'icon_url' => '', + 'menu_icon' => array( + 'type' => 'dashicons', + 'value' => 'dashicons-admin-settings', + ), + 'position' => 80, + 'redirect' => true, + 'description' => 'A complete options page', + 'update_button' => 'Save Changes', + 'updated_message' => 'Options saved successfully', + 'capability' => 'manage_options', + 'data_storage' => 'options', + 'post_id' => '', + 'autoload' => true, + ), + 'Full options page with all fields should be valid', + ), + ); + } + + /** + * Data provider for invalid UI Options Pages. + * + * @return array + */ + public function invalidEntitiesProvider(): array { + return array( + 'missing key' => array( + array( + 'title' => 'Settings', + 'menu_slug' => 'settings', + ), + 'Options page missing key should fail validation', + ), + 'missing title' => array( + array( + 'key' => 'ui_options_page_settings', + 'menu_slug' => 'settings', + ), + 'Options page missing title should fail validation', + ), + 'missing menu_slug' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + ), + 'Options page missing menu_slug should fail validation', + ), + 'empty key' => array( + array( + 'key' => '', + 'title' => 'Settings', + 'menu_slug' => 'settings', + ), + 'Options page with empty key should fail validation', + ), + 'empty title' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => '', + 'menu_slug' => 'settings', + ), + 'Options page with empty title should fail validation', + ), + 'empty menu_slug' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => '', + ), + 'Options page with empty menu_slug should fail validation', + ), + 'wrong key prefix' => array( + array( + 'key' => 'post_type_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + ), + 'Options page with wrong key prefix should fail validation', + ), + 'key without prefix' => array( + array( + 'key' => 'settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + ), + 'Options page with key without prefix should fail validation', + ), + 'menu_slug with uppercase' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'Site-Settings', + ), + 'Options page with uppercase menu_slug should fail validation', + ), + 'menu_slug with special chars' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings!page', + ), + 'Options page with special characters in menu_slug should fail validation', + ), + 'menu_slug with spaces' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'site settings', + ), + 'Options page with spaces in menu_slug should fail validation', + ), + 'invalid menu_icon type enum' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'menu_icon' => array( + 'type' => 'invalid_type', + 'value' => 'some-value', + ), + ), + 'Options page with invalid menu_icon type should fail validation', + ), + 'menu_icon object missing type' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'menu_icon' => array( + 'value' => 'dashicons-admin-settings', + ), + ), + 'Options page with menu_icon missing type should fail validation', + ), + 'menu_icon object missing value' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'menu_icon' => array( + 'type' => 'dashicons', + ), + ), + 'Options page with menu_icon missing value should fail validation', + ), + 'invalid data_storage enum' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'data_storage' => 'invalid_storage', + ), + 'Options page with invalid data_storage value should fail validation', + ), + 'additional properties' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'invalid_property' => 'some value', + ), + 'Options page with additional properties should fail validation', + ), + 'modified as negative integer' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'modified' => -1, + ), + 'Options page with negative modified timestamp should fail validation', + ), + 'menu_order as negative integer' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'menu_order' => -1, + ), + 'Options page with negative menu_order should fail validation', + ), + 'redirect as string' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'redirect' => 'true', + ), + 'Options page with redirect as string should fail validation', + ), + 'autoload as string' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'autoload' => 'yes', + ), + 'Options page with autoload as string should fail validation', + ), + 'active as string' => array( + array( + 'key' => 'ui_options_page_settings', + 'title' => 'Settings', + 'menu_slug' => 'settings', + 'active' => 'true', + ), + 'Options page with active as string should fail validation', + ), + ); + } +}