diff --git a/plugins/wpgraphql-logging/.gitignore b/plugins/wpgraphql-logging/.gitignore index ab99ea50..f0bc5578 100644 --- a/plugins/wpgraphql-logging/.gitignore +++ b/plugins/wpgraphql-logging/.gitignore @@ -48,10 +48,11 @@ c3.php # Cache phpcs-cache.json +.psalm-cache/ tests/_support/ tests/_output/ tests/_generated/ tests/_data/ # Playwright outputs -artifacts \ No newline at end of file +artifacts diff --git a/plugins/wpgraphql-logging/README.md b/plugins/wpgraphql-logging/README.md index 84484323..f1d6ed0b 100644 --- a/plugins/wpgraphql-logging/README.md +++ b/plugins/wpgraphql-logging/README.md @@ -23,6 +23,7 @@ A WPGraphQL logging plugin that provides visibility into request lifecycle to he - [Features](#features) - [Usage](#usage) - [Configuration](#configuration) +- [Admin & Settings](#admin--settings) - [Extending the Functionality](#extending-the-functionality) - [Testing](#testing) @@ -59,6 +60,7 @@ wpgraphql-logging/ ├── docs/ # Docs for extending the plugin. ├── src/ # Main plugin source code │ ├── Admin/ # Admin settings, menu, and settings page logic +│ ├── Settings/ # Admin settings functionality for displaying and saving data. │ ├── Events/ # Event logging, pub/sub event manager for extending the logging. │ ├── Logging/ # Logging logic, logger service, Monolog handlers & processors │ ├── Plugin.php # Main plugin class (entry point) @@ -110,6 +112,9 @@ The following documentation is available in the `docs/` directory: - [Logging](docs/Logging.md): Learn about the logging system, Monolog integration, handlers, processors, and how to use or extend the logger. +- [Admin](docs/admin.md): + Learn how the admin settings page works, all available hooks, and how to add tabs/fields via actions and filters. + --- @@ -126,6 +131,10 @@ The following documentation is available in the `docs/` directory: --- +## Admin & Settings + +See `docs/admin.md` for a full overview of the admin/settings architecture, hooks, and examples for adding tabs and fields. + ## Testing See [Testing.md](TESTING.md) for details on how to test the plugin. diff --git a/plugins/wpgraphql-logging/assets/css/settings/wp-graphql-logging-settings.css b/plugins/wpgraphql-logging/assets/css/settings/wp-graphql-logging-settings.css new file mode 100644 index 00000000..556fc2e6 --- /dev/null +++ b/plugins/wpgraphql-logging/assets/css/settings/wp-graphql-logging-settings.css @@ -0,0 +1,86 @@ +settings_page_wpgraphql-logging #poststuff .postbox .inside h2 { + font-size: 1.3em; + font-weight: 600; + padding-left: 0; +} + + +.form-table td input[type="text"] { + width: calc(99% - 24px); + display: inline-block; +} +.form-table td select { + width: calc(99% - 24px); + display: inline-block; +} + +.wpgraphql-logging-tooltip { + position: relative; + vertical-align: middle; + display: inline-block; + margin-right: 0.25rem; +} + +.wpgraphql-logging-tooltip .dashicons { + color: #787c82; + vertical-align: middle; +} + +.wpgraphql-logging-tooltip .tooltip-text.description { + opacity: 0; + visibility: hidden; + text-align: center; + color: #fff; + background-color: #1d2327; + border-radius: 4px; + position: absolute; + z-index: 1; + width: 180px; + padding: 0.5rem; + top: 50%; + transform: translateY(-50%); + vertical-align: middle; + margin-left: 0.25rem; + transition: opacity 0.12s ease; +} + +.wpgraphql-logging-tooltip .tooltip-text::after { + content: ""; + position: absolute; + top: 0; + left: -10px; + border-width: 6px; + border-style: solid; + border-color: transparent #1d2327 transparent transparent; + top: 50%; + transform: translateY(-50%); +} + +.wpgraphql-logging-tooltip:hover .tooltip-text, +.wpgraphql-logging-tooltip:focus-within .tooltip-text { + visibility: visible; + opacity: 1; +} + +.wpgraphql-logging-docs ul li { + list-style-type: none; + margin-left: 30px; + padding-bottom: 16px; +} + +.wpgraphql-logging-docs ul li:before { + content: url(../../icons/doc.svg); + height: 1em; + margin-left: -29px; + margin-top: -2px; + position: absolute; + width: 0.5em; +} + + +.wpgraphql-logging-feature-list { + list-style-type: disc; + font-size: 1.1em; + margin-left: 30px; + padding-bottom: 16px; +} diff --git a/plugins/wpgraphql-logging/assets/icons/doc.svg b/plugins/wpgraphql-logging/assets/icons/doc.svg new file mode 100644 index 00000000..2bc45f26 --- /dev/null +++ b/plugins/wpgraphql-logging/assets/icons/doc.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/wpgraphql-logging/bin/local/setup-docker-env.sh b/plugins/wpgraphql-logging/bin/local/setup-docker-env.sh old mode 100644 new mode 100755 diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json index 801da78f..d4552747 100644 --- a/plugins/wpgraphql-logging/composer.json +++ b/plugins/wpgraphql-logging/composer.json @@ -143,9 +143,8 @@ "phpstan": [ "vendor/bin/phpstan analyze --ansi --memory-limit=1G" ], - "php:psalm": "psalm", - "php:psalm:info": "psalm --show-info=true", - "php:psalm:fix": "psalm --alter", + "php:psalm": "psalm --output-format=text --no-progress", + "php:psalm:fix": "psalm --alter --output-format=text --no-progress", "qa": "sh bin/local/run-qa.sh", "test": [ "sh bin/local/run-unit-tests.sh coverage", diff --git a/plugins/wpgraphql-logging/docs/admin.md b/plugins/wpgraphql-logging/docs/admin.md new file mode 100644 index 00000000..09e35b47 --- /dev/null +++ b/plugins/wpgraphql-logging/docs/admin.md @@ -0,0 +1,193 @@ +### Admin and Settings + +This document explains how the WPGraphQL Logging admin settings UI is built and how to extend it with your own tabs and fields. + +--- + +## Architecture Overview + +- **Settings page entry**: `WPGraphQL\Logging\Admin\Settings_Page` + - Registers the submenu page and orchestrates fields and tabs + - Hooks added: `init` (init fields), `admin_menu` (page), `admin_init` (fields), `admin_enqueue_scripts` (assets) +- **Menu page**: `WPGraphQL\Logging\Admin\Settings\Menu\Menu_Page` + - Adds a submenu under Settings → WPGraphQL Logging (`wpgraphql-logging`) + - Renders template `src/Admin/Settings/Templates/admin.php` +- **Form manager**: `WPGraphQL\Logging\Admin\Settings\Settings_Form_Manager` + - Registers the settings (`register_setting`) and sections/fields per tab + - Sanitizes and saves values per tab; unknown fields are pruned +- **Field collection**: `WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Collection` + - Holds all tabs and fields. A default `Basic_Configuration_Tab` is registered +- **Tabs**: Implement `Settings_Tab_Interface` with `get_name()`, `get_label()`, `get_fields()` +- **Fields**: Implement `Settings_Field_Interface` or use built-ins: + - `Field\Checkbox_Field` + - `Field\Text_Input_Field` + - `Field\Select_Field` + +Settings are stored in an array option. Keys are filterable: + +- Option key: `wpgraphql_logging_settings` (filter `wpgraphql_logging_settings_group_option_key`) +- Settings group: `wpgraphql_logging_settings_group` (filter `wpgraphql_logging_settings_group_settings_group`) + +To read values at runtime, use `WPGraphQL\Logging\Admin\Settings\Logging_Settings_Service`: + +```php +use WPGraphQL\Logging\Admin\Settings\Logging_Settings_Service; + +$settings = new Logging_Settings_Service(); +$enabled = $settings->get_setting('basic_configuration', 'enabled', false); +``` + +--- + +## Hooks Reference (Admin) + +- Action: `wpgraphql_logging_settings_init( Settings_Page $instance )` + - Fired after the settings page is initialized +- Action: `wpgraphql_logging_settings_field_collection_init( Settings_Field_Collection $collection )` + - Fired after default tabs/fields are registered; primary extension point to add tabs/fields +- Action: `wpgraphql_logging_settings_form_manager_init( Settings_Form_Manager $manager )` + - Fired when the form manager is constructed +- Filter: `wpgraphql_logging_settings_group_option_key( string $option_key )` + - Change the option key used to store settings +- Filter: `wpgraphql_logging_settings_group_settings_group( string $group )` + - Change the settings group name used in `register_setting` + +- Filter: `wpgraphql_logging_basic_configuration_fields( array $fields )` + - Modify the default fields rendered in the `basic_configuration` tab. You can add, remove, or replace fields by returning a modified associative array of `field_id => Settings_Field_Interface`. + - Example: + ```php + use WPGraphQL\Logging\Admin\Settings\Fields\Field\Checkbox_Field; + + add_filter('wpgraphql_logging_basic_configuration_fields', function(array $fields): array { + // Add a custom toggle into the Basic Configuration tab + $fields['enable_feature_x'] = new Checkbox_Field( + 'enable_feature_x', + 'basic_configuration', + 'Enable Feature X', + '', + 'Turn on extra logging for Feature X.' + ); + + // Optionally remove an existing field + // unset($fields[ WPGraphQL\Logging\Admin\Settings\Fields\Tab\Basic_Configuration_Tab::DATA_SAMPLING ]); + + return $fields; + }); + ``` + +Related (non-admin) hooks for context: + +- Action: `wpgraphql_logging_init( Plugin $instance )` (plugin initialized) +- Action: `wpgraphql_logging_activate` / `wpgraphql_logging_deactivate` + +--- + +## Add a New Tab + +Create a tab class implementing `Settings_Tab_Interface` and register it during `wpgraphql_logging_settings_field_collection_init`. + +```php + new Text_Input_Field( + 'my_setting', + $this->get_name(), + 'My Setting', + '', + 'Describe what this setting does.', + 'e.g., value' + ), + ]; + } +} + +add_action('wpgraphql_logging_settings_field_collection_init', function (Settings_Field_Collection $collection): void { + $collection->add_tab(new My_Custom_Tab()); +}); +``` + +Notes: + +- `get_name()` must be a unique slug; it is used in the admin page URL (`tab` query arg) and section IDs +- Fields returned by `get_fields()` must set their `tab` to this slug so they render on the tab + +--- + +## Add a Field to an Existing Tab + +You can add fields directly to the shared field collection. Ensure the field’s `tab` matches the target tab name. + +```php +add_field( + 'enable_feature_x', + new Checkbox_Field( + 'enable_feature_x', + 'basic_configuration', // target the built-in Basic Configuration tab + 'Enable Feature X', + '', + 'Turn on extra logging for Feature X.' + ) + ); +}); +``` + +Tips: + +- Only fields present in the collection are saved; unknown keys are pruned during sanitize +- Field input names follow: `{$option_key}[{$tab}][{$field_id}]` + +--- + +## Reading/Saving Behavior + +- Each submit saves only the current tab’s fields +- Sanitization is delegated to each field via `sanitize_field($value)` +- Unknown fields or tabs are ignored/pruned + +Example of reading a value elsewhere: + +```php +use WPGraphQL\Logging\Admin\Settings\Logging_Settings_Service; + +$settings = new Logging_Settings_Service(); +$thresholdSeconds = (float) $settings->get_setting('basic_configuration', 'performance_metrics', '0'); +``` + +--- + +## Common Use Cases + +- Add organization-specific logging toggles (privacy, PII redaction) +- Integrate with other plugins by exposing their settings under a new tab +- Provide presets for log points (e.g., only log slow queries) via a custom select field + +--- + +## Admin Page Details + +- Menu: Settings → WPGraphQL Logging (`admin.php?page=wpgraphql-logging`) +- Tabs: `admin.php?page=wpgraphql-logging&tab={tab_slug}` +- Sections and fields are rendered with `do_settings_sections('wpgraphql-logging-{tab_slug}')` diff --git a/plugins/wpgraphql-logging/phpcs.xml b/plugins/wpgraphql-logging/phpcs.xml index c5ad6dde..82c557f3 100644 --- a/plugins/wpgraphql-logging/phpcs.xml +++ b/plugins/wpgraphql-logging/phpcs.xml @@ -326,7 +326,7 @@ - + diff --git a/plugins/wpgraphql-logging/phpstan.neon.dist b/plugins/wpgraphql-logging/phpstan.neon.dist index 05f71e10..58fb14d1 100644 --- a/plugins/wpgraphql-logging/phpstan.neon.dist +++ b/plugins/wpgraphql-logging/phpstan.neon.dist @@ -30,3 +30,7 @@ parameters: paths: - wpgraphql-logging.php - src/ + ignoreErrors: + - identifier: empty.notAllowed + - + message: '#Constant WPGRAPHQL_LOGGING.* not found\.#' diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml index 2b45895f..1bfaf14c 100644 --- a/plugins/wpgraphql-logging/psalm.xml +++ b/plugins/wpgraphql-logging/psalm.xml @@ -8,6 +8,7 @@ findUnusedBaselineEntry="true" findUnusedCode="false" phpVersion="8.1" + cacheDirectory=".psalm-cache" > diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Abstract_Settings_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Abstract_Settings_Field.php new file mode 100644 index 00000000..f42c0276 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Abstract_Settings_Field.php @@ -0,0 +1,145 @@ +id; + } + + /** + * Whether the field should be rendered for a specific tab. + * + * @param string $tab_key The tab key. + */ + public function should_render_for_tab( string $tab_key ): bool { + return $tab_key === $this->tab; + } + + /** + * Register the settings field. + * + * @param string $section The section ID. + * @param string $page The page URI. + * @param array $args The field arguments. + */ + public function add_settings_field( string $section, string $page, array $args ): void { + /** @psalm-suppress InvalidArgument */ + add_settings_field( + $this->get_id(), + $this->title, + [ $this, 'render_field_callback' ], + $page, + $section, + array_merge( + $args, + [ + 'class' => $this->css_class, + 'description' => $this->description, + ] + ) + ); + } + + /** + * Callback function to render the field. + * + * @param array $args The field arguments. + */ + public function render_field_callback( array $args ): void { + $tab_key = (string) ( $args['tab_key'] ?? '' ); + $settings_key = (string) ( $args['settings_key'] ?? '' ); + + $option_value = (array) get_option( $settings_key, [] ); + + $id = $this->get_field_name( $settings_key, $tab_key, $this->get_id() ); + + printf( + ' + + %1$s + ', + esc_attr( $this->description ), + esc_attr( $id ), + ); + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_field method handles escaping internally + echo $this->render_field( $option_value, $settings_key, $tab_key ); + } + + /** + * Generate a field name for form inputs. + * + * @param string $settings_key The settings key. + * @param string $tab_key The tab key. + * @param string $field_id The field ID. + */ + protected function get_field_name( string $settings_key, string $tab_key, string $field_id ): string { + return "{$settings_key}[{$tab_key}][{$field_id}]"; + } + + /** + * Get the current field value. + * + * @param array $option_value The option value. + * @param string $tab_key The tab key. + * @param mixed $default_value The default value. + */ + protected function get_field_value( array $option_value, string $tab_key, $default_value = '' ): mixed { + if ( ! array_key_exists( $tab_key, $option_value ) ) { + return $default_value; + } + + /** @var array $tab_value */ + $tab_value = $option_value[ $tab_key ]; // @phpstan-ignore varTag.nativeType + $id = $this->get_id(); + if ( empty( $id ) ) { + return $default_value; + } + + if ( ! array_key_exists( $id, $tab_value ) ) { + return $default_value; + } + + $field_value = $tab_value[ $id ]; + + if ( is_null( $field_value ) ) { + return $default_value; + } + return $field_value; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Checkbox_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Checkbox_Field.php new file mode 100644 index 00000000..ba3a5f13 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Checkbox_Field.php @@ -0,0 +1,49 @@ + $option_value The option value. + * @param string $setting_key The setting key. + * @param string $tab_key The tab key. + * + * @return string The rendered field HTML. + */ + public function render_field( array $option_value, string $setting_key, string $tab_key ): string { + $field_name = $this->get_field_name( $setting_key, $tab_key, $this->get_id() ); + $field_value = $this->get_field_value( $option_value, $tab_key, false ); + $is_checked = is_bool( $field_value ) ? $field_value : false; + + return sprintf( + '', + esc_attr( $field_name ), + checked( 1, $is_checked, false ), + sanitize_html_class( $this->css_class ) + ); + } + + /** + * Sanitize the checkbox field value. + * + * @param mixed $value The field value to sanitize. + * + * @return bool The sanitized boolean value. + */ + public function sanitize_field( $value ): bool { + return (bool) $value; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php new file mode 100644 index 00000000..1f25b812 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php @@ -0,0 +1,125 @@ + $options The options for the select field. + * @param string $css_class The settings field class. + * @param string $description The description field to show in the tooltip. + * @param bool $multiple Whether multiple selections are allowed. + * + * @phpstan-ignore-next-line constructor.missingParentCall + */ + public function __construct( + readonly string $id, + readonly string $tab, + readonly string $title, + readonly array $options, + readonly string $css_class = '', + readonly string $description = '', + readonly bool $multiple = false + ) { + } + + /** + * Render the select field. + * + * @param array $option_value The option value. + * @param string $setting_key The setting key. + * @param string $tab_key The tab key. + * + * @return string The rendered field HTML. + */ + public function render_field( array $option_value, string $setting_key, string $tab_key ): string { + $field_name = $this->get_field_name( $setting_key, $tab_key, $this->get_id() ); + $field_value = $this->get_field_value( $option_value, $tab_key, $this->multiple ? [] : '' ); + + // Ensure we have the correct format for comparison. + $selected_values = $this->multiple ? (array) $field_value : [ (string) $field_value ]; + + $html = ''; + + return $html; + } + + /** + * Sanitize the select field value. + * + * @param mixed $value The field value to sanitize. + * + * @return mixed The sanitized value. + */ + public function sanitize_field( $value ): mixed { + if ( ! $this->multiple ) { + return $this->sanitize_single_value( $value ); + } + + return $this->sanitize_multiple_value( (array) $value ); + } + + /** + * Sanitize a single value. + * + * @param string $value The value to sanitize. + * + * @return string The sanitized value. + */ + protected function sanitize_single_value( $value ): string { + $sanitized_value = sanitize_text_field( (string) $value ); + return array_key_exists( $sanitized_value, $this->options ) ? $sanitized_value : ''; + } + + /** + * Sanitize a multiple value. + * + * @param array $values The values to sanitize. + * + * @return array The sanitized values. + */ + protected function sanitize_multiple_value( array $values ): array { + $sanitized = []; + foreach ( $values as $value ) { + $single = $this->sanitize_single_value( $value ); + if ( '' !== $single ) { + $sanitized[] = $single; + } + } + return $sanitized; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Text_Input_Field.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Text_Input_Field.php new file mode 100644 index 00000000..2c4d79ad --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Text_Input_Field.php @@ -0,0 +1,92 @@ + $option_value The option value. + * @param string $setting_key The setting key. + * @param string $tab_key The tab key. + * + * @return string The rendered field HTML. + */ + public function render_field( array $option_value, string $setting_key, string $tab_key ): string { + $field_name = $this->get_field_name( $setting_key, $tab_key, $this->get_id() ); + $field_value = $this->get_field_value( $option_value, $tab_key, $this->default_value ); + + + return sprintf( + '', + esc_attr( $this->get_input_type() ), + esc_attr( $field_name ), + esc_attr( $field_value ), + esc_attr( $this->placeholder ), + esc_attr( $this->css_class ) + ); + } + + /** + * Sanitize the text input field value. + * + * @param mixed $value The field value to sanitize. + * + * @return string The sanitized string value. + */ + public function sanitize_field( $value ): string { + if ( 'email' === $this->get_input_type() ) { + return sanitize_email( (string) $value ); + } + + if ( 'url' === $this->get_input_type() ) { + return esc_url_raw( (string) $value ); + } + + return sanitize_text_field( (string) $value ); + } + + /** + * Get the input type. + * + * @return string The input type. + */ + protected function get_input_type(): string { + return 'text'; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Collection.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Collection.php new file mode 100644 index 00000000..ead2bbfe --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Collection.php @@ -0,0 +1,109 @@ + + */ + protected array $fields = []; + + /** + * Array of tabs + * + * @var array<\WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface> + */ + protected array $tabs = []; + + /** + * Constructor to initialize the fields. + */ + public function __construct() { + $this->add_tab( new Basic_Configuration_Tab() ); + do_action( 'wpgraphql_logging_settings_field_collection_init', $this ); + } + + /** + * @return array<\WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface> + */ + public function get_fields(): array { + return $this->fields; + } + + /** + * Get a specific field by its key. + * + * @param string $key The key of the field to retrieve. + * + * @return \WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface|null The field if found, null otherwise. + */ + public function get_field( string $key ): ?Settings_Field_Interface { + return $this->fields[ $key ] ?? null; + } + + /** + * Add a field to the collection. + * + * @param string $key The key for the field. + * @param \WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface $field The field to add. + */ + public function add_field( string $key, Settings_Field_Interface $field ): void { + $this->fields[ $key ] = $field; + } + + /** + * Remove a field from the collection. + * + * @param string $key The key of the field to remove. + */ + public function remove_field( string $key ): void { + unset( $this->fields[ $key ] ); + } + + /** + * Add a tab to the collection. + * + * @param \WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface $tab The tab to add. + */ + public function add_tab( Settings_Tab_Interface $tab ): void { + $this->tabs[ $tab->get_name() ] = $tab; + + foreach ( $tab->get_fields() as $field_key => $field ) { + $this->add_field( $field_key, $field ); + } + } + + /** + * Get all tabs. + * + * @return array<\WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface> + */ + public function get_tabs(): array { + return $this->tabs; + } + + /** + * Get a specific tab by its name. + * + * @param string $tab_name The name of the tab to retrieve. + * + * @return \WPGraphQL\Logging\Admin\Settings\Fields\Tab\Settings_Tab_Interface|null The tab if found, null otherwise. + */ + public function get_tab( string $tab_name ): ?Settings_Tab_Interface { + return $this->tabs[ $tab_name ] ?? null; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Interface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Interface.php new file mode 100644 index 00000000..0332e3b0 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Interface.php @@ -0,0 +1,51 @@ + $option_value The option value. + * @param string $setting_key The setting key. + * @param string $tab_key The tab key. + */ + public function render_field( array $option_value, string $setting_key, string $tab_key ): string; + + /** + * Get the field ID + */ + public function get_id(): string; + + /** + * Whether the field should be rendered for a specific tab + * + * @param string $tab_key The tab key. + */ + public function should_render_for_tab( string $tab_key ): bool; + + /** + * Add the settings field + * + * @param string $section The section ID. + * @param string $page The page ID. + * @param array $args The field arguments. + */ + public function add_settings_field( string $section, string $page, array $args ): void; + + /** + * Sanitize field value + * + * @param mixed $value + */ + public function sanitize_field( $value ): mixed; +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php new file mode 100644 index 00000000..bbf8555e --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php @@ -0,0 +1,168 @@ + Array of fields keyed by field ID. + */ + public function get_fields(): array { + $fields = []; + + $fields[ self::ENABLED ] = new Checkbox_Field( + self::ENABLED, + $this->get_name(), + __( 'Enabled', 'wpgraphql-logging' ), + '', + __( 'Enable or disable WPGraphQL logging.', 'wpgraphql-logging' ), + ); + + $fields[ self::IP_RESTRICTIONS ] = new Text_Input_Field( + self::IP_RESTRICTIONS, + $this->get_name(), + __( 'IP Restrictions', 'wpgraphql-logging' ), + '', + __( 'Comma-separated list of IPv4/IPv6 addresses to restrict logging to. Leave empty to log from all IPs.', 'wpgraphql-logging' ), + __( 'e.g., 192.168.1.1, 10.0.0.1', 'wpgraphql-logging' ) + ); + + $fields[ self::ADMIN_USER_LOGGING ] = new Checkbox_Field( + self::ADMIN_USER_LOGGING, + $this->get_name(), + __( 'Log only for admin users', 'wpgraphql-logging' ), + '', + __( 'Log only for admin users.', 'wpgraphql-logging' ) + ); + + $fields[ self::WPGRAPHQL_FILTERING ] = new Text_Input_Field( + self::WPGRAPHQL_FILTERING, + $this->get_name(), + __( 'WPGraphQL Query Filtering', 'wpgraphql-logging' ), + '', + __( 'Comma-separated list of query names or patterns to log. Leave empty to log all queries.', 'wpgraphql-logging' ), + __( 'e.g., GetPost, GetPosts, introspection', 'wpgraphql-logging' ) + ); + + $fields[ self::DATA_SAMPLING ] = new Select_Field( + self::DATA_SAMPLING, + $this->get_name(), + __( 'Data Sampling Rate', 'wpgraphql-logging' ), + [ + '100' => __( '100% (All requests)', 'wpgraphql-logging' ), + '50' => __( '50% (Every other request)', 'wpgraphql-logging' ), + '25' => __( '25% (Every 4th request)', 'wpgraphql-logging' ), + '10' => __( '10% (Every 10th request)', 'wpgraphql-logging' ), + ], + '', + __( 'Percentage of requests to log for performance optimization.', 'wpgraphql-logging' ), + false + ); + + $fields[ self::PERFORMANCE_METRICS ] = new Text_Input_Field( + self::PERFORMANCE_METRICS, + $this->get_name(), + __( 'Performance Threshold (seconds)', 'wpgraphql-logging' ), + '', + __( 'Only log requests that take longer than this threshold. 0 logs all requests. Calculated in seconds.', 'wpgraphql-logging' ), + __( 'e.g., 1.5', 'wpgraphql-logging' ) + ); + + + $fields[ self::EVENT_LOG_SELECTION ] = new Select_Field( + self::EVENT_LOG_SELECTION, + $this->get_name(), + __( 'Log Points', 'wpgraphql-logging' ), + [ + Events::PRE_REQUEST => __( 'Pre Request', 'wpgraphql-logging' ), + Events::BEFORE_GRAPHQL_EXECUTION => __( 'Before Query Execution', 'wpgraphql-logging' ), + Events::BEFORE_RESPONSE_RETURNED => __( 'Before Response Returned', 'wpgraphql-logging' ), + ], + '', + __( 'Select which points in the request lifecycle to log. By default, all points are logged.', 'wpgraphql-logging' ), + true + ); + + return apply_filters( 'wpgraphql_logging_basic_configuration_fields', $fields ); + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Settings_Tab_Interface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Settings_Tab_Interface.php new file mode 100644 index 00000000..7c102479 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Settings_Tab_Interface.php @@ -0,0 +1,38 @@ + Array of fields keyed by field ID. + */ + public function get_fields(): array; +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Logging_Settings_Service.php b/plugins/wpgraphql-logging/src/Admin/Settings/Logging_Settings_Service.php new file mode 100644 index 00000000..833d447d --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Logging_Settings_Service.php @@ -0,0 +1,95 @@ + + */ + protected array $settings_values = []; + + /** + * Initialize the settings service. + */ + public function __construct() { + $this->setup(); + } + + /** + * Get the settings values. + * + * @return array + */ + public function get_settings_values(): array { + return $this->settings_values; + } + + /** + * Get the configuration for a specific tab. + * + * @param string $tab_key The tab key. + * + * @return array|null + */ + public function get_tab_config( string $tab_key ): ?array { + return $this->settings_values[ $tab_key ] ?? null; + } + + /** + * Get a specific setting value from a tab. + * + * @param string $tab_key The tab key. + * @param string $setting_key The setting key. + * @param mixed $default_value The default value if not found. + */ + public function get_setting( string $tab_key, string $setting_key, $default_value = null ): mixed { + $tab_config = $this->get_tab_config( $tab_key ); + return $tab_config[ $setting_key ] ?? $default_value; + } + + /** + * The option key for the settings group. + */ + public static function get_option_key(): string { + return (string) apply_filters( 'wpgraphql_logging_settings_group_option_key', WPGRAPHQL_LOGGING_SETTINGS_KEY ); + } + + /** + * The settings group for the options. + */ + public static function get_settings_group(): string { + return (string) apply_filters( 'wpgraphql_logging_settings_group_settings_group', WPGRAPHQL_LOGGING_SETTINGS_GROUP ); + } + + /** + * Set up the settings values by retrieving them from the database or cache. + * This method is called in the constructor to ensure settings are available. + */ + protected function setup(): void { + $option_key = self::get_option_key(); + $settings_group = self::get_settings_group(); + + $value = wp_cache_get( $option_key, $settings_group ); + if ( is_array( $value ) ) { + $this->settings_values = $value; + + return; + } + + $this->settings_values = (array) get_option( $option_key, [] ); + wp_cache_set( $option_key, $this->settings_values, $settings_group ); + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Menu/Menu_Page.php b/plugins/wpgraphql-logging/src/Admin/Settings/Menu/Menu_Page.php new file mode 100644 index 00000000..99e5052e --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Menu/Menu_Page.php @@ -0,0 +1,109 @@ +> + */ + protected array $args; + + /** + * Constructor. + * + * @param string $page_title The text to be displayed in the title tags of the page when the menu is selected. + * @param string $menu_title The text to be used for the menu. + * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu and only include lowercase alphanumeric, dashes, and underscores characters to be compatible with sanitize_key(). + * @param string $template The template that will be included in the callback. + * @param array> $args The args array for the template. + */ + public function __construct( + string $page_title, + string $menu_title, + string $menu_slug, + string $template, + array $args = [] + ) { + $this->page_title = $page_title; + $this->menu_title = $menu_title; + $this->menu_slug = $menu_slug; + $this->template = $template; + $this->args = $args; + } + + /** + * Registers the menu page in the WordPress admin. + */ + public function register_page(): void { + add_submenu_page( + 'options-general.php', + $this->page_title, + $this->menu_title, + 'manage_options', + $this->menu_slug, + [ $this, 'registration_callback' ] + ); + } + + /** + * Callback function to display the content of the menu page. + */ + public function registration_callback(): void { + if ( empty( $this->template ) || ! file_exists( $this->template ) ) { + printf( + '

%s

', + esc_html__( 'The WPGraphQL Logging Settings template does not exist.', 'wpgraphql-logging' ) + ); + + return; + } + foreach ( $this->args as $query_var => $args ) { + set_query_var( $query_var, $args ); + } + + // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- $this->template is validated and defined within the class + include_once $this->template; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Settings_Form_Manager.php b/plugins/wpgraphql-logging/src/Admin/Settings/Settings_Form_Manager.php new file mode 100644 index 00000000..2fc32bd2 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Settings_Form_Manager.php @@ -0,0 +1,155 @@ +get_settings_group(); + $option_name = $this->get_option_key(); + + register_setting( + $option_group, + $option_name, + [ + 'sanitize_callback' => [ $this, 'sanitize_settings' ], + 'type' => 'array', + 'default' => [], + ] + ); + + foreach ( $this->field_collection->get_tabs() as $tab ) { + $this->render_tab_section( $tab->get_name(), $tab->get_label() ); + } + } + + /** + * Sanitize and merge new settings per-tab, pruning unknown fields. + * + * @param array|null $new_input New settings input for the specific tab that comes from the form for the sanitization. + * + * @return array + */ + public function sanitize_settings( ?array $new_input ): array { + if ( is_null( $new_input ) ) { + return []; + } + + $option_name = $this->get_option_key(); + + $old_input = (array) get_option( $option_name, [] ); + + // Remove redundant tabs. + $tabs = $this->field_collection->get_tabs(); + $tab_keys = array_keys( $tabs ); + + if ( empty( $tab_keys ) ) { + return $old_input; + } + + $old_input = array_intersect_key( $old_input, array_flip( $tab_keys ) ); + + $tab = array_keys( $new_input ); + if ( ! isset( $tab[0] ) ) { + return $old_input; + } + + $tab_to_sanitize = (string) $tab[0]; + if ( ! is_array( $new_input[ $tab_to_sanitize ] ) ) { + return $old_input; + } + + $sanitized_fields = []; + foreach ( $new_input[ $tab_to_sanitize ] as $key => $value ) { + $field = $this->field_collection->get_field( $key ); + + // Skip unknown fields. + if ( is_null( $field ) ) { + continue; + } + + $sanitized_fields[ $key ] = $field->sanitize_field( $value ); + } + + // Merge the sanitized fields with the old input. + $old_input[ $tab_to_sanitize ] = $sanitized_fields; + + return $old_input; + } + + /** + * Get the option key for the settings group. + */ + public function get_option_key(): string { + return Logging_Settings_Service::get_option_key(); + } + + /** + * Get the settings group for the options. + */ + public function get_settings_group(): string { + return Logging_Settings_Service::get_settings_group(); + } + + /** + * Render the settings section for a specific tab. + * + * This method creates a settings section for the given tab and renders the fields for that section. + * + * @param string $tab_key The tab key. + * @param string $label The label for the tab section. + */ + protected function render_tab_section( string $tab_key, string $label ): void { + $fields = $this->field_collection->get_fields(); + $page_id = 'wpgraphql_logging_section_' . $tab_key; + $page_uri = 'wpgraphql-logging-' . $tab_key; + + add_settings_section( $page_id, $label, static fn() => null, $page_uri ); + + /** @var \WPGraphQL\Logging\Admin\Settings\Fields\Settings_Field_Interface $field */ + foreach ( $fields as $field ) { + if ( ! $field->should_render_for_tab( $tab_key ) ) { + continue; + } + + $field->add_settings_field( + $page_id, + $page_uri, + [ + 'tab_key' => $tab_key, + 'label' => $label, + 'settings_key' => $this->get_option_key(), + ] + ); + } + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php new file mode 100644 index 00000000..02388e6b --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php @@ -0,0 +1,121 @@ + + +
+

+
+ + +
+
+
+
+
+
+ +
+
+
+
+ + +
+
+
+

+ +

+
+

+ +

+ + +

+
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+ +

+

+ +

+

+ +

+ +
+
+ +
+

+
+
    +
  • +
  • +
  • +
+

+

HWP Toolkit on GitHub

+
+
+
+
+
+
+
+
+ +
diff --git a/plugins/wpgraphql-logging/src/Admin/Settings_Page.php b/plugins/wpgraphql-logging/src/Admin/Settings_Page.php new file mode 100644 index 00000000..f49650a8 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings_Page.php @@ -0,0 +1,212 @@ +setup(); + } + + /** + * Fire off init action. + * + * @param \WPGraphQL\Logging\Admin\Settings_Page $instance the instance of the plugin class. + */ + do_action( 'wpgraphql_logging_settings_init', self::$instance ); + + return self::$instance; + } + + /** + * Sets up the settings page by registering hooks. + */ + public function setup(): void { + add_action( 'init', [ $this, 'init_field_collection' ], 10, 0 ); + add_action( 'admin_menu', [ $this, 'register_settings_page' ], 10, 0 ); + add_action( 'admin_init', [ $this, 'register_settings_fields' ], 10, 0 ); + add_action( 'admin_enqueue_scripts', [ $this, 'load_scripts_styles' ], 10, 1 ); + } + + /** + * Initialize the field collection. + */ + public function init_field_collection(): void { + $this->field_collection = new Settings_Field_Collection(); + } + + /** + * Registers the settings page. + */ + public function register_settings_page(): void { + if ( is_null( $this->field_collection ) ) { + return; + } + + $tabs = $this->field_collection->get_tabs(); + + $tab_labels = []; + foreach ( $tabs as $tab_key => $tab ) { + if ( ! is_a( $tab, Settings_Tab_Interface::class ) ) { + continue; + } + + $tab_labels[ $tab_key ] = $tab->get_label(); + } + + $page = new Menu_Page( + __( 'WPGraphQL Logging Settings', 'wpgraphql-logging' ), + 'WPGraphQL Logging', + self::PLUGIN_MENU_SLUG, + trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'src/Admin/Settings/Templates/admin.php', + [ + 'wpgraphql_logging_main_page_config' => [ + 'tabs' => $tab_labels, + 'current_tab' => $this->get_current_tab(), + ], + ], + ); + + $page->register_page(); + } + + /** + * Registers the settings fields for each tab. + */ + public function register_settings_fields(): void { + if ( ! isset( $this->field_collection ) ) { + return; + } + $settings_manager = new Settings_Form_Manager( $this->field_collection ); + $settings_manager->render_form(); + } + + /** + * Get the current tab for the settings page. + * + * @param array $tabs Optional. The available tabs. If not provided, uses the instance tabs. + * + * @return string The current tab slug. + */ + public function get_current_tab( array $tabs = [] ): string { + $tabs = $this->get_tabs( $tabs ); + if ( empty( $tabs ) ) { + return 'basic_configuration'; + } + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for tab navigation only, no form processing + if ( ! isset( $_GET['tab'] ) || ! is_string( $_GET['tab'] ) ) { + return 'basic_configuration'; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for tab navigation only, no form processing + $tab = sanitize_text_field( $_GET['tab'] ); + + if ( ! is_string( $tab ) || '' === $tab ) { + return 'basic_configuration'; + } + + if ( array_key_exists( $tab, $tabs ) ) { + return $tab; + } + + return 'basic_configuration'; + } + + /** + * Load scripts and styles for the admin page. + * + * @param string $hook_suffix The current admin page hook suffix. + */ + public function load_scripts_styles( string $hook_suffix ): void { + // Only load on our settings page. + if ( ! str_contains( $hook_suffix, self::PLUGIN_MENU_SLUG ) ) { + return; + } + + // Enqueue admin styles if they exist. + $style_path = trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/css/settings/wp-graphql-logging-settings.css'; + if ( file_exists( trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'assets/css/settings/wp-graphql-logging-settings.css' ) ) { + wp_enqueue_style( + 'wpgraphql-logging-settings-css', + $style_path, + [], + WPGRAPHQL_LOGGING_VERSION + ); + } + + // Enqueue admin scripts if they exist. + $script_path = trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/js/settings/wp-graphql-logging-settings.js'; + if ( ! file_exists( trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'assets/js/settings/wp-graphql-logging-settings.js' ) ) { + return; + } + + wp_enqueue_script( + 'wpgraphql-logging-settings-js', + $script_path, + [], + WPGRAPHQL_LOGGING_VERSION, + true + ); + } + + /** + * Get the tabs for the settings page. + * + * @param array $tabs Optional. The available tabs. If not provided, uses the instance tabs. + * + * @return array The tabs. + */ + protected function get_tabs(array $tabs = []): array { + if ( ! empty( $tabs ) ) { + return $tabs; + } + if ( ! is_null( $this->field_collection ) ) { + return $this->field_collection->get_tabs(); + } + + return []; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/View/.gitkeep b/plugins/wpgraphql-logging/src/Admin/View/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/plugins/wpgraphql-logging/src/Events/Events.php b/plugins/wpgraphql-logging/src/Events/Events.php index 4049ed9b..6a2094f8 100644 --- a/plugins/wpgraphql-logging/src/Events/Events.php +++ b/plugins/wpgraphql-logging/src/Events/Events.php @@ -32,11 +32,4 @@ final class Events { * @var string */ public const BEFORE_RESPONSE_RETURNED = 'graphql_return_response'; - - /** - * After the request is processed. - * - * @var string - */ - public const POST_REQUEST = 'post_request'; } diff --git a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php index 5104b7dc..32c216f8 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php +++ b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php @@ -9,6 +9,7 @@ use WPGraphQL\Logging\Logger\LoggerService; use WPGraphQL\Request; use WPGraphQL\WPSchema; +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\Basic_Configuration_Tab; /** * WPGraphQL Query Event Lifecycle. @@ -39,6 +40,8 @@ class QueryEventLifecycle { */ protected function __construct( LoggerService $logger ) { $this->logger = $logger; + $full_config = get_option( WPGRAPHQL_LOGGING_SETTINGS_KEY, [] ); + $this->config = $full_config['basic_configuration'] ?? []; } /** @@ -54,6 +57,40 @@ public static function init(): QueryEventLifecycle { return self::$instance; } + /** + * Checks if logging is enabled based on user settings. + */ + protected function is_logging_enabled(): bool { + // Check the main "Enabled" checkbox first. + $is_enabled = $this->config[ Basic_Configuration_Tab::ENABLED ] ?? false; + if ( ! $is_enabled ) { + return false; + } + + // Check if the current user is an admin if that option is enabled. + $log_for_admin = $this->config[ Basic_Configuration_Tab::ADMIN_USER_LOGGING ] ?? false; + if ( $log_for_admin && ! current_user_can( 'manage_options' ) ) { + return false; + } + + // Check for IP restrictions. + $ip_restrictions = $this->config[ Basic_Configuration_Tab::IP_RESTRICTIONS ] ?? ''; + if ( ! empty( $ip_restrictions ) ) { + $allowed_ips = array_map( 'trim', explode( ',', $ip_restrictions ) ); + if ( ! in_array( $_SERVER['REMOTE_ADDR'], $allowed_ips, true ) ) { + return false; + } + } + + // Check the data sampling rate. + $sampling_rate = (int) ( $this->config[ Basic_Configuration_Tab::DATA_SAMPLING ] ?? 100 ); + if ( mt_rand( 0, 100 ) >= $sampling_rate ) { + return false; + } + + return true; + } + /** * Initial Incoming Request. * @@ -65,6 +102,10 @@ public static function init(): QueryEventLifecycle { */ public function log_pre_request( string $query, ?string $operation_name, ?array $variables ): void { try { + if ( ! $this->is_logging_enabled() ) { + return; + } + $context = [ 'query' => $query, 'variables' => $variables, @@ -102,6 +143,10 @@ public function log_pre_request( string $query, ?string $operation_name, ?array */ public function log_graphql_before_execute(Request $request ): void { try { + if ( ! $this->is_logging_enabled() ) { + return; + } + /** @var \GraphQL\Server\OperationParams $params */ $params = $request->params; $context = [ @@ -133,8 +178,6 @@ public function log_graphql_before_execute(Request $request ): void { } } - - /** * Before the GraphQL response is returned to the client. * @@ -151,6 +194,9 @@ public function log_graphql_before_execute(Request $request ): void { */ public function log_before_response_returned(array|ExecutionResult $filtered_response, array|ExecutionResult $response, WPSchema $schema, ?string $operation, string $query, ?array $variables, Request $request, ?string $query_id): void { try { + if ( ! $this->is_logging_enabled() ) { + return; + } $context = [ 'response' => $response, 'schema' => $schema, diff --git a/plugins/wpgraphql-logging/src/Plugin.php b/plugins/wpgraphql-logging/src/Plugin.php index de3d7142..8c9ad882 100644 --- a/plugins/wpgraphql-logging/src/Plugin.php +++ b/plugins/wpgraphql-logging/src/Plugin.php @@ -4,6 +4,7 @@ namespace WPGraphQL\Logging; +use WPGraphQL\Logging\Admin\Settings_Page; use WPGraphQL\Logging\Events\EventManager; use WPGraphQL\Logging\Events\QueryEventLifecycle; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; @@ -52,6 +53,7 @@ public static function init(): self { * Initialize the plugin admin, frontend & api functionality. */ public function setup(): void { + Settings_Page::init(); QueryEventLifecycle::init(); } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php new file mode 100644 index 00000000..1ff8290f --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php @@ -0,0 +1,69 @@ +field = new Checkbox_Field( + 'enable_logging', + 'basic_configuration', + 'Enable Logging', + 'custom-css-class', + 'Enable or disable query logging for WPGraphQL requests.' + ); + } + + public function test_basic_field_properties(): void { + $field = $this->field; + $this->assertEquals( 'enable_logging', $field->get_id() ); + $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) ); + $this->assertFalse( $field->should_render_for_tab( 'other_tab' ) ); + } + + public function test_sanitize_field(): void { + $field = $this->field; + $this->assertTrue( $field->sanitize_field( true ) ); + $this->assertTrue( $field->sanitize_field( 1 ) ); + $this->assertFalse( $field->sanitize_field( false ) ); + $this->assertFalse( $field->sanitize_field( 0 ) ); + $this->assertFalse( $field->sanitize_field( null ) ); + } + + public function test_render_field(): void { + $field = $this->field; + $option_value = []; + $setting_key = 'wpgraphql_logging_settings'; + $tab_key = 'basic_configuration'; + $args = [ + 'tab_key' => $tab_key, + 'settings_key' => $setting_key, + ]; + + ob_start(); + $field->render_field_callback( $args ); + $rendered_output = ob_get_contents(); + ob_end_clean(); + + $expected_output = << + + Enable or disable query logging for WPGraphQL requests. + +HTML; + $this->assertEquals( + preg_replace('/[\s\t\r\n]+/', '', $expected_output), + preg_replace('/[\s\t\r\n]+/', '', $rendered_output) + ); + + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php new file mode 100644 index 00000000..eef473b8 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php @@ -0,0 +1,109 @@ +field = new Select_Field( + 'log_level', + 'basic_configuration', + 'Log Level', + [ + 'debug' => 'Debug', + 'info' => 'Info', + 'warning' => 'Warning', + 'error' => 'Error', + ], + 'custom-css-class', + 'Select the minimum log level for WPGraphQL queries.', + false + ); + + $this->multipleField = new Select_Field( + 'query_types', + 'basic_configuration', + 'Query Types', + [ + 'query' => 'Query', + 'mutation' => 'Mutation', + 'subscription' => 'Subscription', + ], + 'multiple-select-class', + 'Select which query types to log.', + true + ); + } + + public function test_basic_field_properties(): void { + $field = $this->field; + $this->assertEquals( 'log_level', $field->get_id() ); + $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) ); + $this->assertFalse( $field->should_render_for_tab( 'other_tab' ) ); + } + + public function test_multiple_field_properties(): void { + $field = $this->multipleField; + $this->assertEquals( 'query_types', $field->get_id() ); + } + + public function test_sanitize_field_single_select(): void { + $field = $this->field; + + $this->assertEquals( 'debug', $field->sanitize_field( 'debug' ) ); + $this->assertEquals( 'info', $field->sanitize_field( 'info' ) ); + $this->assertEquals( 'warning', $field->sanitize_field( 'warning' ) ); + $this->assertEquals( 'error', $field->sanitize_field( 'error' ) ); + $this->assertEquals( '', $field->sanitize_field( 'invalid' ) ); + $this->assertEquals( '', $field->sanitize_field( 'critical' ) ); + $this->assertEquals( '', $field->sanitize_field( '' ) ); + $this->assertEquals( '', $field->sanitize_field( '' ) ); + $this->assertEquals( 'debug', $field->sanitize_field( 'debug' ) ); + $this->assertEquals( '', $field->sanitize_field( 123 ) ); + $this->assertEquals( '', $field->sanitize_field( true ) ); + $this->assertEquals( '', $field->sanitize_field( false ) ); + } + + public function test_sanitize_field_multiple_select(): void { + $field = $this->multipleField; + + $this->assertEquals( ['query'], $field->sanitize_field( ['query'] ) ); + $this->assertEquals( ['query', 'mutation'], $field->sanitize_field( ['query', 'mutation'] ) ); + $this->assertEquals( ['query', 'mutation', 'subscription'], $field->sanitize_field( ['query', 'mutation', 'subscription'] ) ); + $this->assertEquals( [], $field->sanitize_field( ['invalid', 'another_invalid'] ) ); + $this->assertEquals( ['query'], $field->sanitize_field( 'query' ) ); + $this->assertEquals( [], $field->sanitize_field( 'invalid' ) ); + } + + public function test_render_field_callback(): void { + $field = $this->field; + $option_value = []; + $setting_key = 'wpgraphql_logging_settings'; + $tab_key = 'basic_configuration'; + $args = [ + 'tab_key' => $tab_key, + 'settings_key' => $setting_key, + ]; + + // Capture the echoed output using output buffering + ob_start(); + $field->render_field_callback( $args ); + $rendered_output = ob_get_contents(); + ob_end_clean(); + + $this->assertStringContainsString( 'name="wpgraphql_logging_settings[basic_configuration][log_level]"', $rendered_output ); + $this->assertStringContainsString( 'id="log_level"', $rendered_output ); + $this->assertStringContainsString( 'class="custom-css-class"', $rendered_output ); + $this->assertStringContainsString( 'Select the minimum log level for WPGraphQL queries.', $rendered_output ); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php new file mode 100644 index 00000000..d2c877dc --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php @@ -0,0 +1,190 @@ +field = new Text_Input_Field( + 'ip_restrictions', + 'basic_configuration', + 'IP Restrictions', + 'custom-css-class', + 'A comma separated list of IP addresses to restrict logging to. Leave empty to log from all IPs.', + 'e.g. 192.168.1.1, 10.0.0.1', + '' + ); + } + + public function test_basic_field_properties(): void { + $field = $this->field; + $this->assertEquals( 'ip_restrictions', $field->get_id() ); + $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) ); + $this->assertFalse( $field->should_render_for_tab( 'other_tab' ) ); + $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) ); + } + + + public function test_sanitize_field() { + $field = $this->field; + + // Valid Input + $input = '192.168.1.1, 10.0.0.1'; + $sanitized = $field->sanitize_field( $input ); + $this->assertEquals( $input, $sanitized ); + + // XSS + $input = '192.168.1.1, 10.0.0.1'; + $sanitized = $field->sanitize_field( $input ); + $this->assertStringNotContainsString( '