From fe33b7c471380c19de3e5e1d6d7ae76d2f3d3cb7 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 20 Aug 2025 16:18:58 +0100 Subject: [PATCH 1/9] Initial creation of the admin functionality. Added config for basic configuration. --- plugins/wpgraphql-logging/.gitignore | 3 +- .../settings/wp-graphql-logging-settings.css | 86 +++++++ .../wpgraphql-logging/assets/icons/doc.svg | 3 + plugins/wpgraphql-logging/composer.json | 5 +- plugins/wpgraphql-logging/phpcs.xml | 2 +- plugins/wpgraphql-logging/phpstan.neon.dist | 4 + plugins/wpgraphql-logging/psalm.xml | 1 + .../src/Admin/Settings/.gitkeep | 0 .../Fields/Field/Abstract_Settings_Field.php | 145 ++++++++++++ .../Settings/Fields/Field/Checkbox_Field.php | 49 ++++ .../Settings/Fields/Field/Select_Field.php | 122 ++++++++++ .../Fields/Field/Text_Input_Field.php | 92 ++++++++ .../Fields/Settings_Field_Collection.php | 109 +++++++++ .../Fields/Settings_Field_Interface.php | 51 +++++ .../Fields/Tab/Basic_Configuration_Tab.php | 168 ++++++++++++++ .../Fields/Tab/Settings_Tab_Interface.php | 38 ++++ .../Settings/Logging_Settings_Service.php | 95 ++++++++ .../src/Admin/Settings/Menu/Menu_Page.php | 109 +++++++++ .../Admin/Settings/Settings_Form_Manager.php | 155 +++++++++++++ .../src/Admin/Settings/Templates/admin.php | 121 ++++++++++ .../src/Admin/Settings_Page.php | 212 ++++++++++++++++++ .../wpgraphql-logging/src/Admin/View/.gitkeep | 0 .../wpgraphql-logging/src/Events/Events.php | 7 - plugins/wpgraphql-logging/src/Plugin.php | 2 + .../wpgraphql-logging/wpgraphql-logging.php | 8 + 25 files changed, 1575 insertions(+), 12 deletions(-) create mode 100644 plugins/wpgraphql-logging/assets/css/settings/wp-graphql-logging-settings.css create mode 100644 plugins/wpgraphql-logging/assets/icons/doc.svg create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/.gitkeep create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Abstract_Settings_Field.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Checkbox_Field.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Text_Input_Field.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Collection.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Settings_Field_Interface.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Settings_Tab_Interface.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Logging_Settings_Service.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Menu/Menu_Page.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Settings_Form_Manager.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php create mode 100644 plugins/wpgraphql-logging/src/Admin/Settings_Page.php create mode 100644 plugins/wpgraphql-logging/src/Admin/View/.gitkeep 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/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/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/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/.gitkeep b/plugins/wpgraphql-logging/src/Admin/Settings/.gitkeep new file mode 100644 index 00000000..e69de29b 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..623c0605 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php @@ -0,0 +1,122 @@ + $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( string $value ): string { + $sanitized_value = sanitize_text_field( $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 ) { + $sanitized[] = $this->sanitize_single_value( $value ); + } + 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..c061af8f --- /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 $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..3fdff260 --- /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/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/wpgraphql-logging.php b/plugins/wpgraphql-logging/wpgraphql-logging.php index 5eeb07d1..6199dca6 100644 --- a/plugins/wpgraphql-logging/wpgraphql-logging.php +++ b/plugins/wpgraphql-logging/wpgraphql-logging.php @@ -85,6 +85,14 @@ function wpgraphql_logging_constants(): void { if ( ! defined( 'WPGRAPHQL_LOGGING_PLUGIN_URL' ) ) { define( 'WPGRAPHQL_LOGGING_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); } + + if ( ! defined( 'WPGRAPHQL_LOGGING_SETTINGS_KEY' ) ) { + define( 'WPGRAPHQL_LOGGING_SETTINGS_KEY', 'wpgraphql_logging_settings' ); + } + + if ( ! defined( 'WPGRAPHQL_LOGGING_SETTINGS_GROUP' ) ) { + define( 'WPGRAPHQL_LOGGING_SETTINGS_GROUP', 'wpgraphql_logging_settings_group' ); + } } } From 810a5ac53f51bd239fad6819e8487a0fb46c0f68 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 20 Aug 2025 17:41:34 +0100 Subject: [PATCH 2/9] Improve settings field sanitization and add tests Refactored Select_Field sanitization to handle non-string values and filter out invalid options. Removed redundant is_admin() check in Settings_Page::init. Added comprehensive unit tests for Settings_Page to verify initialization, hook registration, tab selection, and asset loading behaviors. --- .../Settings/Fields/Field/Select_Field.php | 9 +- .../src/Admin/Settings_Page.php | 2 +- .../src/Events/QueryEventLifecycle.php | 2 - .../Fields/Field/CheckboxFieldTest.php | 69 +++++++ .../Settings/Fields/Field/SelectFieldTest.php | 109 ++++++++++ .../Fields/Field/TextInputFieldTest.php | 190 ++++++++++++++++++ .../tests/wpunit/Admin/Settings_Page_Test.php | 113 +++++++++++ 7 files changed, 488 insertions(+), 6 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings_Page_Test.php 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 index 623c0605..1f25b812 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/Select_Field.php @@ -100,8 +100,8 @@ public function sanitize_field( $value ): mixed { * * @return string The sanitized value. */ - protected function sanitize_single_value( string $value ): string { - $sanitized_value = sanitize_text_field( $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 : ''; } @@ -115,7 +115,10 @@ protected function sanitize_single_value( string $value ): string { protected function sanitize_multiple_value( array $values ): array { $sanitized = []; foreach ( $values as $value ) { - $sanitized[] = $this->sanitize_single_value( $value ); + $single = $this->sanitize_single_value( $value ); + if ( '' !== $single ) { + $sanitized[] = $single; + } } return $sanitized; } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings_Page.php b/plugins/wpgraphql-logging/src/Admin/Settings_Page.php index 3fdff260..f49650a8 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings_Page.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings_Page.php @@ -42,7 +42,7 @@ class Settings_Page { * Initializes the settings page. */ public static function init(): ?Settings_Page { - if ( ! current_user_can( 'manage_options' ) || ! is_admin() ) { + if ( ! current_user_can( 'manage_options' ) ) { return null; } diff --git a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php index 5104b7dc..5b5b6034 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php +++ b/plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php @@ -133,8 +133,6 @@ public function log_graphql_before_execute(Request $request ): void { } } - - /** * Before the GraphQL response is returned to the client. * 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( '