From b4783339fb42df6f0a1428cb478455fcd66a28a9 Mon Sep 17 00:00:00 2001 From: Theo <328805+theodesp@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:31:19 +0100 Subject: [PATCH 01/59] chore: Internal Testing & Validation WPGraphQL Logging Plugin- Interfaces --- .../Fields/SettingsFieldInterface.php | 96 +++++++++++-------- .../Fields/Tab/SettingsTabInterface.php | 56 ++++++----- 2 files changed, 92 insertions(+), 60 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php index a2a1cd95..2ae5f618 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php @@ -5,47 +5,67 @@ namespace WPGraphQL\Logging\Admin\Settings\Fields; /** - * Interface for settings fields + * Interface for settings fields. * - * @package WPGraphQL\Logging + * Defines the contract for a settings field in WPGraphQL Logging. + * Implementing classes must handle rendering, sanitization, and integration with WordPress Settings API. * + * @package WPGraphQL\Logging * @since 0.0.1 */ interface SettingsFieldInterface { - /** - * Render the settings field - * - * @param array $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; + + /** + * Render the settings field. + * + * This should return the HTML for the field. Always escape output using esc_html(), esc_attr(), or wp_kses_post() as appropriate. + * + * @param array $option_value The option value(s) for this field. + * @param string $setting_key The setting key associated with this field. + * @param string $tab_key The tab key that this field belongs to. + * + * @return string Rendered HTML for the field. + */ + public function render_field(array $option_value, string $setting_key, string $tab_key): string; + + /** + * Get the unique field ID. + * + * Must be unique within the tab/page to avoid conflicts. + * + * @return string The field ID. + */ + public function get_id(): string; + + /** + * Determine if the field should render for a specific tab. + * + * @param string $tab_key The tab key to check. + * + * @return bool True if the field should render for this tab. + */ + public function should_render_for_tab(string $tab_key): bool; + + /** + * Add the field to WordPress Settings API. + * + * Implementing classes should call add_settings_field() internally with proper sanitization callback. + * + * @param string $section The settings section ID. + * @param string $page The settings page ID. + * @param array $args Additional field arguments. + */ + public function add_settings_field(string $section, string $page, array $args): void; + + /** + * Sanitize the field value. + * + * Must ensure that all output saved to the database is safe. + * For text: use sanitize_text_field(), for HTML: wp_kses_post(), etc. + * + * @param mixed $value The raw value from user input. + * + * @return mixed The sanitized value to store in the database. + */ + public function sanitize_field(mixed $value): mixed; } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php index 438a3dfc..37a8f61a 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php @@ -4,35 +4,47 @@ namespace WPGraphQL\Logging\Admin\Settings\Fields\Tab; +use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; + /** - * Interface for settings field tabs. + * Interface for settings tabs. * - * This interface defines the contract for tab classes that group related settings fields together. - * Each tab implementation should provide a name and a collection of fields. + * Defines the contract for a settings tab that groups related fields. + * Each tab must provide its metadata and fields for registration in the WordPress Settings API. * * @package WPGraphQL\Logging - * * @since 0.0.1 */ interface SettingsTabInterface { - /** - * Get the fields for this tab. - * - * @return array Array of fields keyed by field ID. - */ - public function get_fields(): array; - /** - * Get the name of the tab. - * - * @return string The tab name/identifier. - */ - public static function get_name(): string; + /** + * Get the settings fields for this tab. + * + * The returned array should be keyed by field ID and contain instances + * implementing SettingsFieldInterface. These fields will be rendered + * and registered automatically in the admin settings page. + * + * @return array Fields keyed by their unique ID. + */ + public function get_fields(): array; + + /** + * Get the unique name/slug for this tab. + * + * Must be unique within the plugin to avoid conflicts between tabs. + * This name is used in URLs, queries, and as the array key for field storage. + * + * @return string The tab name/slug. + */ + public static function get_name(): string; - /** - * Get the label of the tab. - * - * @return string The tab label. - */ - public static function get_label(): string; + /** + * Get the human-readable label for this tab. + * + * The label is displayed in the admin UI as the tab title. + * Should be internationalized using esc_html__(). + * + * @return string The tab label. + */ + public static function get_label(): string; } From 34ce41fbd59cc87bbc2cbc47b95b57f868bb1f58 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 22 Oct 2025 18:34:10 +0100 Subject: [PATCH 02/59] Updated packages. Fixed some QA issues. --- plugins/wpgraphql-logging/composer.lock | 286 +++++++++--------- .../Settings/Fields/Field/CheckboxField.php | 6 +- .../Settings/Fields/Field/SelectField.php | 6 +- .../Settings/Fields/Field/TextInputField.php | 6 +- .../Fields/SettingsFieldInterface.php | 100 +++--- .../Fields/Tab/SettingsTabInterface.php | 60 ++-- .../src/Admin/View/List/ListTable.php | 2 +- .../src/Logger/Database/DatabaseEntity.php | 4 +- 8 files changed, 240 insertions(+), 230 deletions(-) diff --git a/plugins/wpgraphql-logging/composer.lock b/plugins/wpgraphql-logging/composer.lock index 188e898c..41f74ea1 100644 --- a/plugins/wpgraphql-logging/composer.lock +++ b/plugins/wpgraphql-logging/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "be7c7a0aff5b08c432f2b70dc142b6f5", + "content-hash": "a6a9646cd6d3b077e724996ab2bf648e", "packages": [ { "name": "league/csv", - "version": "9.25.0", + "version": "9.27.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "f856f532866369fb1debe4e7c5a1db185f40ef86" + "reference": "cb491b1ba3c42ff2bcd0113814f4256b42bae845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/f856f532866369fb1debe4e7c5a1db185f40ef86", - "reference": "f856f532866369fb1debe4e7c5a1db185f40ef86", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/cb491b1ba3c42ff2bcd0113814f4256b42bae845", + "reference": "cb491b1ba3c42ff2bcd0113814f4256b42bae845", "shasum": "" }, "require": { @@ -95,7 +95,7 @@ "type": "github" } ], - "time": "2025-09-11T08:29:08+00:00" + "time": "2025-10-16T08:22:09+00:00" }, { "name": "monolog/monolog", @@ -3092,16 +3092,16 @@ }, { "name": "johnpbloch/wordpress-core", - "version": "6.8.2", + "version": "6.8.3", "source": { "type": "git", "url": "https://github.com/johnpbloch/wordpress-core.git", - "reference": "316a8fe38b6dde4e4f399946809f040462038403" + "reference": "0641ab5518c94c1ab094ad4ccdc46aa9c4657fc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/johnpbloch/wordpress-core/zipball/316a8fe38b6dde4e4f399946809f040462038403", - "reference": "316a8fe38b6dde4e4f399946809f040462038403", + "url": "https://api.github.com/repos/johnpbloch/wordpress-core/zipball/0641ab5518c94c1ab094ad4ccdc46aa9c4657fc1", + "reference": "0641ab5518c94c1ab094ad4ccdc46aa9c4657fc1", "shasum": "" }, "require": { @@ -3109,7 +3109,7 @@ "php": ">=7.2.24" }, "provide": { - "wordpress/core-implementation": "6.8.2" + "wordpress/core-implementation": "6.8.3" }, "type": "wordpress-core", "notification-url": "https://packagist.org/downloads/", @@ -3136,20 +3136,20 @@ "source": "https://core.trac.wordpress.org/browser", "wiki": "https://codex.wordpress.org/" }, - "time": "2025-07-15T15:32:59+00:00" + "time": "2025-09-30T18:14:19+00:00" }, { "name": "justinrainbow/json-schema", - "version": "6.5.2", + "version": "6.6.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8" + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ac0d369c09653cf7af561f6d91a705bc617a87b8", - "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/68ba7677532803cc0c5900dd5a4d730537f2b2f3", + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3", "shasum": "" }, "require": { @@ -3209,9 +3209,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.5.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.0" }, - "time": "2025-09-09T09:42:27+00:00" + "time": "2025-10-10T11:34:09+00:00" }, { "name": "lucatume/wp-browser", @@ -3455,16 +3455,16 @@ }, { "name": "mck89/peast", - "version": "v1.17.2", + "version": "v1.17.4", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "465810689c477fbba17f4f949b75e4d0bdab13ea" + "reference": "c6a63f32410d2e4ee2cd20fe94b35af147fb852d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/465810689c477fbba17f4f949b75e4d0bdab13ea", - "reference": "465810689c477fbba17f4f949b75e4d0bdab13ea", + "url": "https://api.github.com/repos/mck89/peast/zipball/c6a63f32410d2e4ee2cd20fe94b35af147fb852d", + "reference": "c6a63f32410d2e4ee2cd20fe94b35af147fb852d", "shasum": "" }, "require": { @@ -3477,7 +3477,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17.2-dev" + "dev-master": "1.17.4-dev" } }, "autoload": { @@ -3498,9 +3498,9 @@ "description": "Peast is PHP library that generates AST for JavaScript code", "support": { "issues": "https://github.com/mck89/peast/issues", - "source": "https://github.com/mck89/peast/tree/v1.17.2" + "source": "https://github.com/mck89/peast/tree/v1.17.4" }, - "time": "2025-07-01T09:30:45+00:00" + "time": "2025-10-10T12:53:17+00:00" }, { "name": "mockery/mockery", @@ -4176,12 +4176,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "771535284869cd4f3ff178c292c032b804738771" + "reference": "de29923c98ce1d7d35df862c51d5dc0062c95401" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/771535284869cd4f3ff178c292c032b804738771", - "reference": "771535284869cd4f3ff178c292c032b804738771", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/de29923c98ce1d7d35df862c51d5dc0062c95401", + "reference": "de29923c98ce1d7d35df862c51d5dc0062c95401", "shasum": "" }, "require": { @@ -4262,20 +4262,20 @@ "type": "thanks_dev" } ], - "time": "2025-09-09T21:00:45+00:00" + "time": "2025-10-20T21:11:54+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.3", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", "shasum": "" }, "require": { @@ -4332,22 +4332,26 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:30:46+00:00" + "time": "2025-09-19T17:43:28+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.7", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c" + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/5bfbbfbabb3df2b9a83e601de9153e4a7111962c", - "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", "shasum": "" }, "require": { @@ -4409,7 +4413,7 @@ "type": "thanks_dev" } ], - "time": "2025-05-12T16:38:37+00:00" + "time": "2025-10-18T00:05:59+00:00" }, { "name": "phpcsstandards/phpcsextra", @@ -4495,16 +4499,16 @@ }, { "name": "phpcsstandards/phpcsutils", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae" + "reference": "8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", - "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9", + "reference": "8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9", "shasum": "" }, "require": { @@ -4584,7 +4588,7 @@ "type": "thanks_dev" } ], - "time": "2025-09-05T00:00:03+00:00" + "time": "2025-10-16T16:39:32+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4885,16 +4889,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "578fa296a166605d97b94091f724f1257185d278" - }, + "version": "2.1.31", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278", - "reference": "578fa296a166605d97b94091f724f1257185d278", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", + "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", "shasum": "" }, "require": { @@ -4939,25 +4938,25 @@ "type": "github" } ], - "time": "2025-09-19T08:58:49+00:00" + "time": "2025-10-10T14:14:11+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.6", + "version": "2.0.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094" + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094", - "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0.4" + "phpstan/phpstan": "^2.1.29" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -4985,9 +4984,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7" }, - "time": "2025-07-21T12:19:29+00:00" + "time": "2025-09-26T11:19:08+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5312,16 +5311,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.55", + "version": "10.5.58", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4b2d546b336876bd9562f24641b08a25335b06b6" + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b2d546b336876bd9562f24641b08a25335b06b6", - "reference": "4b2d546b336876bd9562f24641b08a25335b06b6", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", "shasum": "" }, "require": { @@ -5345,7 +5344,7 @@ "sebastian/comparator": "^5.0.4", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.2", + "sebastian/exporter": "^5.1.4", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", "sebastian/recursion-context": "^5.0.1", @@ -5393,7 +5392,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.55" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" }, "funding": [ { @@ -5417,7 +5416,7 @@ "type": "tidelift" } ], - "time": "2025-09-14T06:19:20+00:00" + "time": "2025-09-28T12:04:46+00:00" }, { "name": "psr/container", @@ -5684,16 +5683,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.10", + "version": "v0.12.13", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + "reference": "d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e", + "reference": "d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e", "shasum": "" }, "require": { @@ -5708,9 +5707,11 @@ "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2" + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" }, "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", "ext-pdo-sqlite": "The doc command requires SQLite to work.", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." @@ -5756,9 +5757,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.13" }, - "time": "2025-08-04T12:39:37+00:00" + "time": "2025-10-20T22:48:29+00:00" }, { "name": "ralouphie/getallheaders", @@ -6325,16 +6326,16 @@ }, { "name": "sebastian/exporter", - "version": "5.1.2", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { @@ -6343,7 +6344,7 @@ "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -6391,15 +6392,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T07:17:12+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", @@ -6993,28 +7006,27 @@ }, { "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.12.0", + "version": "v2.13.0", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "4debf5383d9ade705e0a25121f16c3fecaf433a7" + "reference": "a15e970b8a0bf64cfa5e86d941f5e6b08855f369" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/4debf5383d9ade705e0a25121f16c3fecaf433a7", - "reference": "4debf5383d9ade705e0a25121f16c3fecaf433a7", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/a15e970b8a0bf64cfa5e86d941f5e6b08855f369", + "reference": "a15e970b8a0bf64cfa5e86d941f5e6b08855f369", "shasum": "" }, "require": { "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" + "squizlabs/php_codesniffer": "^3.5.7 || ^4.0.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", + "phpstan/phpstan": "^1.7 || ^2.0", "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0 || ^6.0 || ^7.0" }, "type": "phpcodesniffer-standard", "autoload": { @@ -7046,7 +7058,7 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2025-03-17T16:17:38+00:00" + "time": "2025-09-30T22:22:48+00:00" }, { "name": "slevomat/coding-standard", @@ -7408,16 +7420,16 @@ }, { "name": "symfony/console", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" + "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", - "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "url": "https://api.github.com/repos/symfony/console/zipball/492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f", + "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f", "shasum": "" }, "require": { @@ -7482,7 +7494,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.25" + "source": "https://github.com/symfony/console/tree/v6.4.26" }, "funding": [ { @@ -7502,7 +7514,7 @@ "type": "tidelift" } ], - "time": "2025-08-22T10:21:53+00:00" + "time": "2025-09-26T12:13:46+00:00" }, { "name": "symfony/css-selector", @@ -8673,16 +8685,16 @@ }, { "name": "symfony/string", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1" + "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", - "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", + "url": "https://api.github.com/repos/symfony/string/zipball/5621f039a71a11c87c106c1c598bdcd04a19aeea", + "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea", "shasum": "" }, "require": { @@ -8696,7 +8708,6 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", "symfony/http-client": "^5.4|^6.0|^7.0", "symfony/intl": "^6.2|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -8739,7 +8750,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.25" + "source": "https://github.com/symfony/string/tree/v6.4.26" }, "funding": [ { @@ -8759,20 +8770,20 @@ "type": "tidelift" } ], - "time": "2025-08-22T12:33:20+00:00" + "time": "2025-09-11T14:32:46+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c" + "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6cd92486e9fc32506370822c57bc02353a5a92c", - "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfae1497a2f1eaad78dbc0590311c599c7178d4a", + "reference": "cfae1497a2f1eaad78dbc0590311c599c7178d4a", "shasum": "" }, "require": { @@ -8827,7 +8838,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.25" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.26" }, "funding": [ { @@ -8847,20 +8858,20 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2025-09-25T15:37:27+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.25", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565" + "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e54b060bc9c3dc3d4258bf0d165d0064e755f565", - "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", + "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", "shasum": "" }, "require": { @@ -8903,7 +8914,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.25" + "source": "https://github.com/symfony/yaml/tree/v6.4.26" }, "funding": [ { @@ -8923,20 +8934,20 @@ "type": "tidelift" } ], - "time": "2025-08-26T16:59:00+00:00" + "time": "2025-09-26T15:07:38+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v2.0.2", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "963887b04c21fe7ac78e61c1351f8b00fff9f8f8" + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/963887b04c21fe7ac78e61c1351f8b00fff9f8f8", - "reference": "963887b04c21fe7ac78e61c1351f8b00fff9f8f8", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", "shasum": "" }, "require": { @@ -8946,6 +8957,7 @@ }, "require-dev": { "composer/composer": "^2.1.14", + "composer/semver": "^3.4", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.1", "phpstan/phpstan-strict-rules": "^2.0", @@ -8983,9 +8995,9 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.2" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.3" }, - "time": "2025-02-12T18:43:37+00:00" + "time": "2025-09-14T02:58:22+00:00" }, { "name": "theseer/tokenizer", @@ -9233,28 +9245,28 @@ }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "541057574806f942c94662b817a50f63f7345360" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/541057574806f942c94662b817a50f63f7345360", + "reference": "541057574806f942c94662b817a50f63f7345360", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -9285,9 +9297,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.0" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-20T12:43:39+00:00" }, { "name": "wp-cli/cache-command", @@ -11301,12 +11313,12 @@ "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "4d0741eb1050b4b939808410a44812127d680fce" + "reference": "04f9932abfa2ed71dd16e88964f6ac6cd64c37dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/4d0741eb1050b4b939808410a44812127d680fce", - "reference": "4d0741eb1050b4b939808410a44812127d680fce", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/04f9932abfa2ed71dd16e88964f6ac6cd64c37dd", + "reference": "04f9932abfa2ed71dd16e88964f6ac6cd64c37dd", "shasum": "" }, "require": { @@ -11365,7 +11377,7 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2025-09-08T07:33:55+00:00" + "time": "2025-10-01T15:55:36+00:00" }, { "name": "wp-cli/wp-cli-bundle", diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/CheckboxField.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/CheckboxField.php index 711d55b7..a3b88987 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/CheckboxField.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/CheckboxField.php @@ -17,9 +17,9 @@ class CheckboxField extends AbstractSettingsField { /** * Render the checkbox field. * - * @param array $option_value The option value. - * @param string $setting_key The setting key. - * @param string $tab_key The tab key. + * @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. */ diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/SelectField.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/SelectField.php index 68a061e8..ab7580c2 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/SelectField.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/SelectField.php @@ -41,9 +41,9 @@ public function __construct( /** * 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. + * @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. */ diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php index 971d5ced..57e2fae7 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php @@ -41,9 +41,9 @@ public function __construct( /** * Render the text input field. * - * @param array $option_value The option value. - * @param string $setting_key The setting key. - * @param string $tab_key The tab key. + * @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. */ diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php index 2ae5f618..b64ae812 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldInterface.php @@ -11,61 +11,61 @@ * Implementing classes must handle rendering, sanitization, and integration with WordPress Settings API. * * @package WPGraphQL\Logging + * * @since 0.0.1 */ interface SettingsFieldInterface { + /** + * Render the settings field. + * + * This should return the HTML for the field. Always escape output using esc_html(), esc_attr(), or wp_kses_post() as appropriate. + * + * @param array $option_value The option value(s) for this field. + * @param string $setting_key The setting key associated with this field. + * @param string $tab_key The tab key that this field belongs to. + * + * @return string Rendered HTML for the field. + */ + public function render_field(array $option_value, string $setting_key, string $tab_key): string; - /** - * Render the settings field. - * - * This should return the HTML for the field. Always escape output using esc_html(), esc_attr(), or wp_kses_post() as appropriate. - * - * @param array $option_value The option value(s) for this field. - * @param string $setting_key The setting key associated with this field. - * @param string $tab_key The tab key that this field belongs to. - * - * @return string Rendered HTML for the field. - */ - public function render_field(array $option_value, string $setting_key, string $tab_key): string; - - /** - * Get the unique field ID. - * - * Must be unique within the tab/page to avoid conflicts. - * - * @return string The field ID. - */ - public function get_id(): string; + /** + * Get the unique field ID. + * + * Must be unique within the tab/page to avoid conflicts. + * + * @return string The field ID. + */ + public function get_id(): string; - /** - * Determine if the field should render for a specific tab. - * - * @param string $tab_key The tab key to check. - * - * @return bool True if the field should render for this tab. - */ - public function should_render_for_tab(string $tab_key): bool; + /** + * Determine if the field should render for a specific tab. + * + * @param string $tab_key The tab key to check. + * + * @return bool True if the field should render for this tab. + */ + public function should_render_for_tab(string $tab_key): bool; - /** - * Add the field to WordPress Settings API. - * - * Implementing classes should call add_settings_field() internally with proper sanitization callback. - * - * @param string $section The settings section ID. - * @param string $page The settings page ID. - * @param array $args Additional field arguments. - */ - public function add_settings_field(string $section, string $page, array $args): void; + /** + * Add the field to WordPress Settings API. + * + * Implementing classes should call add_settings_field() internally with proper sanitization callback. + * + * @param string $section The settings section ID. + * @param string $page The settings page ID. + * @param array $args Additional field arguments. + */ + public function add_settings_field(string $section, string $page, array $args): void; - /** - * Sanitize the field value. - * - * Must ensure that all output saved to the database is safe. - * For text: use sanitize_text_field(), for HTML: wp_kses_post(), etc. - * - * @param mixed $value The raw value from user input. - * - * @return mixed The sanitized value to store in the database. - */ - public function sanitize_field(mixed $value): mixed; + /** + * Sanitize the field value. + * + * Must ensure that all output saved to the database is safe. + * For text: use sanitize_text_field(), for HTML: wp_kses_post(), etc. + * + * @param mixed $value The raw value from user input. + * + * @return mixed The sanitized value to store in the database. + */ + public function sanitize_field(mixed $value): mixed; } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php index 37a8f61a..cbd3c601 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php @@ -4,8 +4,6 @@ namespace WPGraphQL\Logging\Admin\Settings\Fields\Tab; -use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; - /** * Interface for settings tabs. * @@ -13,38 +11,38 @@ * Each tab must provide its metadata and fields for registration in the WordPress Settings API. * * @package WPGraphQL\Logging + * * @since 0.0.1 */ interface SettingsTabInterface { + /** + * Get the settings fields for this tab. + * + * The returned array should be keyed by field ID and contain instances + * implementing SettingsFieldInterface. These fields will be rendered + * and registered automatically in the admin settings page. + * + * @return array Fields keyed by their unique ID. + */ + public function get_fields(): array; - /** - * Get the settings fields for this tab. - * - * The returned array should be keyed by field ID and contain instances - * implementing SettingsFieldInterface. These fields will be rendered - * and registered automatically in the admin settings page. - * - * @return array Fields keyed by their unique ID. - */ - public function get_fields(): array; - - /** - * Get the unique name/slug for this tab. - * - * Must be unique within the plugin to avoid conflicts between tabs. - * This name is used in URLs, queries, and as the array key for field storage. - * - * @return string The tab name/slug. - */ - public static function get_name(): string; + /** + * Get the unique name/slug for this tab. + * + * Must be unique within the plugin to avoid conflicts between tabs. + * This name is used in URLs, queries, and as the array key for field storage. + * + * @return string The tab name/slug. + */ + public static function get_name(): string; - /** - * Get the human-readable label for this tab. - * - * The label is displayed in the admin UI as the tab title. - * Should be internationalized using esc_html__(). - * - * @return string The tab label. - */ - public static function get_label(): string; + /** + * Get the human-readable label for this tab. + * + * The label is displayed in the admin UI as the tab title. + * Should be internationalized using esc_html__(). + * + * @return string The tab label. + */ + public static function get_label(): string; } diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php index f6e16d76..7b925330 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php @@ -10,7 +10,7 @@ // Include the WP_List_Table class if not already loaded. if ( ! class_exists( 'WP_List_Table' ) ) { - require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; // @phpstan-ignore-line + require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } /** diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 5f9f53c3..285a07bf 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -363,7 +363,7 @@ public static function get_schema(): string { * Creates the logging table in the database. */ public static function create_table(): void { - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; // @phpstan-ignore-line + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( self::get_schema() ); } @@ -373,7 +373,7 @@ public static function create_table(): void { public static function drop_table(): void { global $wpdb; $table_name = self::get_table_name(); - $wpdb->query( "DROP TABLE IF EXISTS {$table_name}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange } /** From ffd97dc8b3177765e8b7d74459f0a1e97bf57ad5 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 22 Oct 2025 18:50:10 +0100 Subject: [PATCH 03/59] Fix for codeception to correctly workout percentage of code coverage. --- .../wpgraphql-logging/bin/run-codeception.sh | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/plugins/wpgraphql-logging/bin/run-codeception.sh b/plugins/wpgraphql-logging/bin/run-codeception.sh index b28eb7fe..107350de 100755 --- a/plugins/wpgraphql-logging/bin/run-codeception.sh +++ b/plugins/wpgraphql-logging/bin/run-codeception.sh @@ -35,15 +35,12 @@ run_tests() { fi if [[ -n "$COVERAGE" ]]; then - if [[ -n "$COVERAGE_OUTPUT" ]]; then - local coverage="--coverage --coverage-xml $COVERAGE_OUTPUT" - else - local coverage="--coverage --coverage-xml $suites-coverage.xml" - fi + # Generate coverage in default output locations (XML + HTML) + local coverage="--coverage --coverage-xml --coverage-html" fi # If maintenance mode is active, de-activate it - if $(wp maintenance-mode is-active --allow-root); then + if wp maintenance-mode is-active --allow-root >/dev/null 2>&1; then echo "Deactivating maintenance mode" wp maintenance-mode deactivate --allow-root fi @@ -69,15 +66,20 @@ run_tests() { # Check code coverage if coverage was requested if [[ -n "$COVERAGE" ]]; then - if [[ -n "$COVERAGE_OUTPUT" ]]; then - coverage_percent=$(grep -oP '(\d+\.\d+)%' "tests/_output/coverage/index.html" | head -1 | tr -d '%') - else - coverage_percent=$(grep -oP 'line-rate="(\d+\.\d+)"' "tests/_output/coverage.xml" | head -1 | grep -oP '\d+\.\d+') - # Convert to percent - if [[ -n "$coverage_percent" ]]; then - coverage_percent=$(awk "BEGIN { printf \"%.2f\", $coverage_percent * 100 }") + # Prefer XML summary for robustness; fallback to HTML if present + if [[ -f "tests/_output/coverage.xml" ]]; then + # Extract total statements and covered statements from the summary metrics line + total_statements=$(grep -Eo 'statements="[0-9]+"' "tests/_output/coverage.xml" | tail -1 | grep -Eo '[0-9]+') + total_covered=$(grep -Eo 'coveredstatements="[0-9]+"' "tests/_output/coverage.xml" | tail -1 | grep -Eo '[0-9]+') + if [[ -n "$total_statements" && -n "$total_covered" && "$total_statements" -gt 0 ]]; then + coverage_percent=$(awk "BEGIN { printf \"%.2f\", ($total_covered / $total_statements) * 100 }") fi fi + + if [[ -z "$coverage_percent" && -f "tests/_output/coverage/index.html" ]]; then + # macOS/BSD grep lacks -P; use -E and strip the percent sign + coverage_percent=$(grep -Eo '([0-9]+(\.[0-9]+)?)%' "tests/_output/coverage/index.html" | head -1 | tr -d '%') + fi if [[ -z "$coverage_percent" ]]; then echo "Warning: Could not determine code coverage percentage." exit 1 @@ -122,7 +124,7 @@ cleanup_after() { if [[ "$USING_XDEBUG" == '1' ]]; then echo "Disabling XDebug 3" rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - else98 + else echo "Disabling pcov/clobber" docker-php-ext-disable pcov sed -i '/pcov.enabled=1/d' /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini From b53a0c4eceae3d28bc9879fc02a13753f30622f3 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 22 Oct 2025 19:18:26 +0100 Subject: [PATCH 04/59] Update plugin docs and metadata for beta release. Updated PHP Doc Blocks. Expanded and clarified the readme.txt with detailed features, requirements, installation, and FAQ for the WPGraphQL Logging plugin beta release. Updated contributor list, license, and tested PHP/WP versions. Added and improved PHPDoc blocks in templates and event class for consistency. Renamed TESTING.md to docs/how-to/run_tests.md for better documentation structure. Minor description update in main plugin file. --- plugins/wpgraphql-logging/CHANGELOG.md | 4 - .../{TESTING.md => docs/how-to/run_tests.md} | 0 plugins/wpgraphql-logging/readme.txt | 128 +++++++++++++++--- .../Templates/wpgraphql-logger-filters.php | 8 ++ .../View/Templates/wpgraphql-logger-list.php | 2 +- .../View/Templates/wpgraphql-logger-view.php | 4 + .../wpgraphql-logging/src/Events/Events.php | 4 + .../wpgraphql-logging/wpgraphql-logging.php | 4 +- 8 files changed, 130 insertions(+), 24 deletions(-) rename plugins/wpgraphql-logging/{TESTING.md => docs/how-to/run_tests.md} (100%) diff --git a/plugins/wpgraphql-logging/CHANGELOG.md b/plugins/wpgraphql-logging/CHANGELOG.md index a8412997..e29799ee 100644 --- a/plugins/wpgraphql-logging/CHANGELOG.md +++ b/plugins/wpgraphql-logging/CHANGELOG.md @@ -43,7 +43,3 @@ ### Patch Changes - [#403](https://github.com/wpengine/hwptoolkit/pull/403) [`821908b`](https://github.com/wpengine/hwptoolkit/commit/821908b7a7b8743a44cdbdbd98eedfff7faac34a) Thanks [@colinmurphy](https://github.com/colinmurphy)! - chore: Added admin view, filters and CSV downloads. - -## 0.0.1-beta - -- Proof of concept. A plugin for logging data for WPGraphQL. diff --git a/plugins/wpgraphql-logging/TESTING.md b/plugins/wpgraphql-logging/docs/how-to/run_tests.md similarity index 100% rename from plugins/wpgraphql-logging/TESTING.md rename to plugins/wpgraphql-logging/docs/how-to/run_tests.md diff --git a/plugins/wpgraphql-logging/readme.txt b/plugins/wpgraphql-logging/readme.txt index 45d1971d..bcd51aac 100644 --- a/plugins/wpgraphql-logging/readme.txt +++ b/plugins/wpgraphql-logging/readme.txt @@ -1,35 +1,127 @@ === WPGraphQL Logging === -Contributors: wpengine +Contributors: colin-murphy, joefusco, thdespou, wpengine Tags: GraphQL, Headless, WPGraphQL, React, Rest, Logging, Performance, Debugging, Monitoring Requires at least: 6.5 -Tested up to: 6.8,2 -Requires PHP: 8.1 +Tested up to: 6.8.2 +Requires PHP: 8.1.2 Requires WPGraphQL: 2.3.0 Stable tag: 0.1.0 -License: GPL-2.0 +License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html +A WPGraphQL logging plugin that provides visibility into the GraphQL request lifecycle, giving developers the observability needed to quickly identify and resolve bottlenecks in their headless WordPress application. + == Description == -A WPGraphQL logging plugin that provides visibility into request lifecycle to help quickly identify and resolve bottlenecks in your headless WordPress application. +**WPGraphQL Logging** is a comprehensive logging solution for WPGraphQL that tracks and records GraphQL query execution, providing developers with detailed insights into query performance, errors, and request lifecycle events. -= Features = +**Note:** This plugin is currently in BETA. While it is functional and ready for testing, some features may be subject to change based on community feedback. -- Query event lifecycle logging: - - Pre Request (`do_graphql_request`): logs query, variables, operation name - - Before Execution (`graphql_before_execute`): logs request params snapshot - - After Execution (`graphql_execute`): logs response, schema, request - - Before Response Returned (`graphql_return_response`): inspects response errors and upgrades level to Error when present += Key Features = -- Pub/sub system: - - Subscribe with priorities, publish events, and apply transforms to mutate payloads - - WordPress bridges: `wpgraphql_logging_event_{event}` (action) and `wpgraphql_logging_filter_{event}` (filter) +**GraphQL Request Lifecycle Logging** +* Pre Request (`do_graphql_request`): Captures query text, variables, and operation name +* Before Execution (`graphql_before_execute`): Records request parameter snapshot +* After Execution (`graphql_execute`): Logs response data, schema context, and request details +* Before Response (`graphql_return_response`): Inspects response for errors and automatically escalates log level when errors are present -- Monolog-based storage and context: - - Default handler writes to a WordPress table (`{$wpdb->prefix}wpgraphql_logging`) - - Processors include memory usage, web request, process ID, and GraphQL request details +**Advanced Admin Interface** +* View and filter all logged GraphQL requests in the WordPress admin +* Search and sort by date, query name, log level, or custom criteria +* Export logs to CSV for offline analysis +* Configurable data retention policies with automatic cleanup + +**Flexible Event System** +* Built-in pub/sub architecture for subscribing to logging events +* Priority-based event handling +* Transform and mutate log payloads before storage +* WordPress action/filter bridges: `wpgraphql_logging_event_{event}` and `wpgraphql_logging_filter_{event}` + +**Powerful Storage & Context** +* Built on Monolog for reliable, industry-standard logging +* Default database handler stores logs in WordPress table (`{$wpdb->prefix}wpgraphql_logging`) +* Extensible processor system includes: + * Memory usage tracking + * Web request context + * Process ID tracking + * GraphQL-specific request metadata + +**Customization Options** +* Add custom log processors via filters +* Implement custom storage handlers +* Define custom logging rules and conditions +* Extend the admin interface with custom fields and tabs + += Use Cases = + +* **Performance Monitoring**: Identify slow queries and execution bottlenecks +* **Error Tracking**: Monitor and debug GraphQL errors in production +* **Development & Testing**: Track query behavior during development +* **Compliance & Auditing**: Maintain records of API access and usage +* **Analytics**: Analyze query patterns and usage trends + += Requirements = + +* WordPress 6.5 or higher +* PHP 8.1.2 or higher +* WPGraphQL 2.3.0 or higher + += Documentation = + +For detailed documentation, guides, and examples, visit the [GitHub repository](https://github.com/wpengine/hwptoolkit/tree/main/plugins/wpgraphql-logging). + +== Installation == + +1. Upload the plugin files to `/wp-content/plugins/wpgraphql-logging/`, or install the plugin through the WordPress plugins screen directly +2. Activate the plugin through the 'Plugins' screen in WordPress +3. Navigate to Settings > WPGraphQL Logging in the WordPress admin to configure settings +4. View logged queries can be found in GraphQL Logs -== Upgrade Notice == == Frequently Asked Questions == + += Is this plugin production-ready? = + +This plugin is currently in BETA. It is functional and ready for testing in staging environments. We recommend thorough testing before deploying to production. + += Does this plugin affect GraphQL query performance? = + +The plugin is designed to have minimal performance impact. Logging operations are performed asynchronously where possible, and you can configure data retention policies to manage database size. + += Can I export my logs? = + +Yes! The admin interface includes CSV export functionality for all filtered log entries. + += Can I customize what gets logged? = + +Yes, the plugin provides extensive hooks and filters to customize logging behavior, add custom processors, and implement custom storage solutions. + += How do I delete old logs? = + +The plugin includes configurable data retention settings. You can set automatic cleanup rules to delete logs older than a specified number of days. + == Screenshots == + +1. View all GraphQL query logs with filtering and search +2. Detailed log entry view showing query, variables, and response +3. Configuration settings for data retention and logging behavior +4. Export logs to CSV for analysis + == Changelog == + += 0.1.0 - 2025-01-22 = +* Initial BETA release +* Core logging functionality for WPGraphQL request lifecycle +* Admin interface for viewing and filtering logs +* CSV export functionality +* Configurable data retention and cleanup +* Extensible event system with pub/sub architecture +* Monolog-based storage with custom processors + +== Upgrade Notice == + += 0.1.0 = +Initial BETA release. + +== Support == + +For support, feature requests, or bug reports, please visit our [GitHub issues page](https://github.com/wpengine/hwptoolkit/issues). diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php index 527f659f..a279d378 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php @@ -2,6 +2,14 @@ declare(strict_types=1); +/** + * Log filters template. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 + */ + if ( ! defined( 'ABSPATH' ) ) { exit; } diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php index fa26bef5..eeba4f0b 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php @@ -5,7 +5,7 @@ /** * Logs list view template using WP_List_Table. * - * @package WPGraphQL\Logger\Admin\View\List\Templates + * @package WPGraphQL\Logging * * @var \WPGraphQL\Logging\Admin\View\List\ListTable $list_table List table instance. * diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php index cd105433..a8539a1e 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php @@ -5,7 +5,11 @@ /** * Log detail view template. * + * @package WPGraphQL\Logging + * * @var \WPGraphQL\Logging\Logger\Database\DatabaseEntity $log + * + * @since 0.0.1 */ ?>
diff --git a/plugins/wpgraphql-logging/src/Events/Events.php b/plugins/wpgraphql-logging/src/Events/Events.php index 231e4903..99a084a3 100644 --- a/plugins/wpgraphql-logging/src/Events/Events.php +++ b/plugins/wpgraphql-logging/src/Events/Events.php @@ -6,6 +6,10 @@ /** * List of available events that users can subscribe to with the EventManager. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 */ final class Events { /** diff --git a/plugins/wpgraphql-logging/wpgraphql-logging.php b/plugins/wpgraphql-logging/wpgraphql-logging.php index 29a526c7..b1fa7826 100644 --- a/plugins/wpgraphql-logging/wpgraphql-logging.php +++ b/plugins/wpgraphql-logging/wpgraphql-logging.php @@ -3,7 +3,7 @@ * Plugin Name: WPGraphQL Logging * Plugin URI: https://github.com/wpengine/hwptoolkit * GitHub Plugin URI: https://github.com/wpengine/hwptoolkit - * Description: A WPGraphQL logging plugin that provides visibility into request lifecycle to help quickly identify and resolve bottlenecks in your headless WordPress application. + * Description: A WPGraphQL logging plugin that provides visibility into the request lifecycle, giving developers the observability needed to quickly identify and resolve bottlenecks in their headless WordPress application. * Author: WPEngine Headless OSS Team * Author URI: https://github.com/wpengine * Update URI: https://github.com/wpengine/hwptoolkit @@ -23,6 +23,8 @@ * * @author WPEngine Headless OSS Team * + * @since 0.0.1 + * * @license GPL-2 */ From e1916525ce8fb9c09e2b017c12396ae6678d5b5a Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 22 Oct 2025 19:22:41 +0100 Subject: [PATCH 05/59] QA fix. --- .../src/Admin/View/Templates/wpgraphql-logger-filters.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php index a279d378..8058064f 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php @@ -1,7 +1,5 @@ Date: Wed, 22 Oct 2025 20:14:03 +0100 Subject: [PATCH 06/59] Rename admin view templates to WPGraphQLLogger* Renamed admin view template files to use the WPGraphQLLogger* naming convention for consistency. Updated all references in code, documentation, and psalm.xml to match the new filenames. --- plugins/wpgraphql-logging/docs/reference/admin.md | 7 ++++++- plugins/wpgraphql-logging/psalm.xml | 4 ++-- .../wpgraphql-logging/src/Admin/View/List/ListTable.php | 2 +- ...aphql-logger-filters.php => WPGraphQLLoggerFilters.php} | 1 - .../{wpgraphql-logger-list.php => WPGraphQLLoggerList.php} | 0 .../{wpgraphql-logger-view.php => WPGraphQLLoggerView.php} | 0 plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php | 4 ++-- plugins/wpgraphql-logging/wpgraphql-logging.php | 2 +- 8 files changed, 12 insertions(+), 8 deletions(-) rename plugins/wpgraphql-logging/src/Admin/View/Templates/{wpgraphql-logger-filters.php => WPGraphQLLoggerFilters.php} (99%) rename plugins/wpgraphql-logging/src/Admin/View/Templates/{wpgraphql-logger-list.php => WPGraphQLLoggerList.php} (100%) rename plugins/wpgraphql-logging/src/Admin/View/Templates/{wpgraphql-logger-view.php => WPGraphQLLoggerView.php} (100%) diff --git a/plugins/wpgraphql-logging/docs/reference/admin.md b/plugins/wpgraphql-logging/docs/reference/admin.md index 35cf246b..7f16e8b1 100644 --- a/plugins/wpgraphql-logging/docs/reference/admin.md +++ b/plugins/wpgraphql-logging/docs/reference/admin.md @@ -415,6 +415,11 @@ add_filter( 'wpgraphql_logging_csv_content', function( $content, $log_id, $log ) --- ### Class: `Settings\Templates\admin.php` and View Templates -Source: `src/Admin/Settings/Templates/admin.php`, `src/Admin/View/Templates/*.php` +Source: `src/Admin/Settings/Templates/admin.php`, `src/Admin/View/Templates/WPGraphQLLogger*.php` These templates are referenced by the template path filters above and do not define hooks themselves. + +**Template Files:** +- `WPGraphQLLoggerFilters.php` - Filter controls template +- `WPGraphQLLoggerList.php` - Logs list table template +- `WPGraphQLLoggerView.php` - Single log detail view template diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml index eca593fe..e291140b 100644 --- a/plugins/wpgraphql-logging/psalm.xml +++ b/plugins/wpgraphql-logging/psalm.xml @@ -38,13 +38,13 @@ - + - + diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php index 7b925330..b3138299 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php @@ -498,7 +498,7 @@ protected function display_tablenav( $which ): void { protected function render_custom_filters(): void { $template = apply_filters( 'wpgraphql_logging_filters_template', - __DIR__ . '/../Templates/wpgraphql-logger-filters.php' + __DIR__ . '/../Templates/WPGraphQLLoggerFilters.php' ); if ( ! file_exists( $template ) ) { diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerFilters.php similarity index 99% rename from plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php rename to plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerFilters.php index 8058064f..2561e3c3 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerFilters.php @@ -1,5 +1,4 @@ Date: Wed, 22 Oct 2025 20:43:54 +0100 Subject: [PATCH 07/59] Refactored error_log to only log when WP_DEBUG is defined. --- .../wpgraphql-logging/src/Events/EventManager.php | 14 ++++++++++---- .../Logger/Handlers/WordPressDatabaseHandler.php | 5 ++++- .../wpgraphql-logging/src/Logger/LoggingHelper.php | 6 +++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Events/EventManager.php b/plugins/wpgraphql-logging/src/Events/EventManager.php index 42d0a11b..a07d4a3b 100644 --- a/plugins/wpgraphql-logging/src/Events/EventManager.php +++ b/plugins/wpgraphql-logging/src/Events/EventManager.php @@ -173,8 +173,11 @@ private static function invoke_listener(callable $listener, array $payload): voi try { $listener( $payload ); } catch ( \Throwable $e ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - error_log( 'WPGraphQL Logging EventManager listener error: ' . $e->getMessage() ); + do_action( 'wpgraphql_logging_event_error_listener', $e, $listener, $payload ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- This is a development notice. + error_log( 'WPGraphQL Logging EventManager listener error: ' . $e->getMessage() ); + } } } @@ -193,8 +196,11 @@ private static function invoke_transform(callable $transform, array $payload): a return $result; } } catch ( \Throwable $e ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - error_log( 'WPGraphQL Logging EventManager transform error: ' . $e->getMessage() ); + do_action( 'wpgraphql_logging_event_error_transform', $e, $transform, $payload ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- This is a development notice. + error_log( 'WPGraphQL Logging EventManager transform error: ' . $e->getMessage() ); + } } return $payload; diff --git a/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php b/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php index 74a3b9e0..23b9a3a1 100644 --- a/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php +++ b/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php @@ -38,7 +38,10 @@ protected function write( LogRecord $record ): void { $entity->save(); } catch ( Throwable $e ) { - error_log( 'Error logging to WordPress database: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + do_action( 'wpgraphql_logging_write_database_error', $e, $record ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( 'Error logging to WordPress database: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } } } diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php index 2db28255..3fbc50e1 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -112,6 +112,10 @@ protected function is_logging_enabled( array $config, ?string $query_string = nu * @param \Throwable $exception */ protected function process_application_error( string $event, \Throwable $exception ): void { - error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore + + do_action( 'wpgraphql_logging_process_application_error', $event, $exception ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore + } } } From 99a6c7c4dc338545beb992ec97fbb7743f7e297e Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 22 Oct 2025 20:45:59 +0100 Subject: [PATCH 08/59] Make config cache duration filterable Wrap CACHE_DURATION in apply_filters to allow customization of the configuration cache duration via the 'wpgraphql_logging_config_cache_duration' filter. --- .../src/Admin/Settings/ConfigurationHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php index 75a76e3d..bec0d5a1 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php @@ -185,7 +185,7 @@ public static function init_cache_hooks(): void { protected function load_config(): void { $option_key = $this->get_option_key(); - $cache_duration = self::CACHE_DURATION; + $cache_duration = (int) apply_filters( 'wpgraphql_logging_config_cache_duration', self::CACHE_DURATION ); // Try to get from wp_cache first (in-memory cache). $cached_config = wp_cache_get( $option_key, self::CACHE_GROUP ); From cebd24d0edecc812e362ae1a51288cf5952954c9 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 22 Oct 2025 20:53:39 +0100 Subject: [PATCH 09/59] Add whitelist validation for ORDER BY in find_logs Introduces a filter 'wpgraphql_logging_allowed_orderby_columns' to whitelist allowed columns for ORDER BY in find_logs() queries, enhancing security against SQL injection. Documentation updated with usage details and examples; code now validates both column and direction, falling back to defaults if invalid values are provided. --- .../docs/reference/logging.md | 21 +++++++++++++++++++ .../src/Logger/Database/DatabaseEntity.php | 19 ++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/plugins/wpgraphql-logging/docs/reference/logging.md b/plugins/wpgraphql-logging/docs/reference/logging.md index b589073b..2dbc5c80 100644 --- a/plugins/wpgraphql-logging/docs/reference/logging.md +++ b/plugins/wpgraphql-logging/docs/reference/logging.md @@ -216,6 +216,27 @@ add_filter( 'wpgraphql_logging_database_name', function( string $name ) { }); ``` +#### Filter: `wpgraphql_logging_allowed_orderby_columns` +Filters the allowed columns for ORDER BY in `find_logs()` queries. + +**Security:** This filter adds whitelist validation to prevent SQL injection in ORDER BY clauses. Only columns in this array can be used for sorting. + +Parameters: +- `$allowed_columns` (array) Default allowed columns: `['id', 'datetime', 'level', 'level_name', 'channel', 'message']` + +Returns: array + +Example: +```php +// Add custom column to allowed ORDER BY list +add_filter( 'wpgraphql_logging_allowed_orderby_columns', function( array $columns ) { + $columns[] = 'custom_field'; + return $columns; +}); +``` + +**Note:** If an invalid column is requested, the query will fallback to ordering by `id` (default). + --- diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 285a07bf..7c2cfef9 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -278,7 +278,7 @@ public function get_query(): ?string { * @param int $limit The maximum number of log entries to return. * @param int $offset The offset for pagination. * @param array $where_clauses Optional. Additional WHERE conditions. - * @param string $orderby The column to order by. + * @param string $orderby The column to order by. Must be one of the allowed columns. * @param string $order The order direction (ASC or DESC). * * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> An array of DatabaseEntity instances, or an empty array if none found. @@ -286,8 +286,21 @@ public function get_query(): ?string { public static function find_logs(int $limit, int $offset, array $where_clauses = [], string $orderby = 'id', string $order = 'DESC'): array { global $wpdb; $table_name = self::get_table_name(); - $order = sanitize_text_field( strtoupper( $order ) ); - $orderby = sanitize_text_field( $orderby ); + + // Whitelist validation for ORDER BY column. + $allowed_orderby_columns = [ 'id', 'datetime', 'level', 'level_name', 'channel', 'message' ]; + $allowed_orderby_columns = apply_filters( 'wpgraphql_logging_allowed_orderby_columns', $allowed_orderby_columns ); + + // Fallback to default if the orderby column is not allowed. + if ( ! in_array( $orderby, $allowed_orderby_columns, true ) ) { + $orderby = 'id'; + } + + // Whitelist validation for ORDER direction. + $order = strtoupper( $order ); + if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) { + $order = 'DESC'; // Fallback to default. + } $where = ''; foreach ( $where_clauses as $clause ) { From da046e8a16b88a548955b0bbaf62badb16da2b6d Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 23 Oct 2025 11:15:20 +0100 Subject: [PATCH 10/59] Minor fixes. Fixed settings filter name. --- plugins/wpgraphql-logging/readme.txt | 2 +- .../src/Admin/Settings/ConfigurationHelper.php | 2 +- .../src/Logger/Processors/DataSanitizationProcessor.php | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/wpgraphql-logging/readme.txt b/plugins/wpgraphql-logging/readme.txt index bcd51aac..1fc3b5b6 100644 --- a/plugins/wpgraphql-logging/readme.txt +++ b/plugins/wpgraphql-logging/readme.txt @@ -1,5 +1,5 @@ === WPGraphQL Logging === -Contributors: colin-murphy, joefusco, thdespou, wpengine +Contributors: ahuseyn, colin-murphy, joefusco, thdespou, wpengine Tags: GraphQL, Headless, WPGraphQL, React, Rest, Logging, Performance, Debugging, Monitoring Requires at least: 6.5 Tested up to: 6.8.2 diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php index bec0d5a1..bb8cf7c8 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php @@ -139,7 +139,7 @@ public function clear_cache(): void { * Get the option key for the settings. */ public function get_option_key(): string { - return (string) apply_filters( 'wpgraphql_logging_settings_group_option_key', WPGRAPHQL_LOGGING_SETTINGS_KEY ); + return (string) apply_filters( 'wpgraphql_logging_settings_key', WPGRAPHQL_LOGGING_SETTINGS_KEY ); } /** diff --git a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php index 4e3226aa..b941054a 100644 --- a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php +++ b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php @@ -168,6 +168,7 @@ protected function apply_sanitization_rule(array &$current, string $key, string break; case 'truncate': if ( is_string( $current[ $key ] ) ) { + // Truncate to 50 characters. $current[ $key ] = substr( $current[ $key ], 0, 47 ) . '...'; } break; From 3823a783456aaa16a1897a7761909e5af341374c Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 24 Oct 2025 09:26:10 +0100 Subject: [PATCH 11/59] Add buffer handler to reduce database write operations for performance. Introduces Monolog's BufferHandler for database logging and adds the 'wpgraphql_logging_default_buffer_limit' filter to customize buffer size. Updates tests to flush buffer before asserting log counts and documents the new filter. Also adds an index for 'level_name' in the database entity. --- .../docs/reference/logging.md | 21 +++++++++++++++++++ .../src/Logger/Database/DatabaseEntity.php | 1 + .../src/Logger/LoggerService.php | 15 ++++++++++++- .../wpunit/Events/QueryActionLoggerTest.php | 10 +++++++++ .../wpunit/Events/QueryFilterLoggerTest.php | 10 +++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/docs/reference/logging.md b/plugins/wpgraphql-logging/docs/reference/logging.md index 2dbc5c80..7de943bb 100644 --- a/plugins/wpgraphql-logging/docs/reference/logging.md +++ b/plugins/wpgraphql-logging/docs/reference/logging.md @@ -39,6 +39,27 @@ add_filter( 'wpgraphql_logging_default_processors', function( array $processors }); ``` +#### Filter: `wpgraphql_logging_default_buffer_limit` +Filters the default buffer limit for the BufferHandler. + +Parameters: +- `$buffer_limit` (int) Current buffer limit (default: 50) + +Returns: int + +Example: +```php + +add_filter( 'wpgraphql_logging_default_buffer_limit', function( int $buffer_limit ) { + // Increase buffer limit for high-traffic sites + return 100; +}); + + +``` + + + #### Filter: `wpgraphql_logging_default_handlers` Filters the default handler list. diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 7c2cfef9..3f2963ba 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -366,6 +366,7 @@ public static function get_schema(): string { datetime DATETIME NOT NULL, PRIMARY KEY (id), INDEX channel_index (channel), + INDEX level_name_index (level_name) INDEX level_index (level), INDEX datetime_index (datetime) ) {$charset_collate}; diff --git a/plugins/wpgraphql-logging/src/Logger/LoggerService.php b/plugins/wpgraphql-logging/src/Logger/LoggerService.php index 0c5111ed..27de8150 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggerService.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggerService.php @@ -4,6 +4,7 @@ namespace WPGraphQL\Logging\Logger; +use Monolog\Handler\BufferHandler; use Monolog\Handler\HandlerInterface; use Monolog\Logger; use Monolog\Processor\MemoryPeakUsageProcessor; @@ -212,6 +213,15 @@ public function log( $level, string $message, array $context = [] ): void { $this->monolog->log( $level, $message, array_merge( $this->default_context, $context ) ); } + /** + * Gets the Monolog logger instance. + * + * @return \Monolog\Logger The Monolog logger instance. + */ + public function get_monolog(): Logger { + return $this->monolog; + } + /** * Returns an array of default processors. * @@ -241,8 +251,11 @@ public static function get_default_processors(): array { * @return array<\Monolog\Handler\AbstractProcessingHandler> */ public static function get_default_handlers(): array { + + $buffer_limit = apply_filters( 'wpgraphql_logging_default_buffer_limit', 50 ); + $database_handler = new BufferHandler( new WordPressDatabaseHandler(), $buffer_limit ); $default_handlers = [ - new WordPressDatabaseHandler(), + $database_handler, ]; // Filter for users to add their own handlers. diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php index c38dcdfa..52f732f7 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging\Tests\Events; use WPGraphQL\Logging\Plugin; +use Monolog\Handler\BufferHandler; use lucatume\WPBrowser\TestCase\WPTestCase; use WPGraphQL\Logging\Events\QueryActionLogger; use WPGraphQL\Logging\Events\Events; @@ -53,6 +54,15 @@ public function get_log_count(): int { } public function assert_log_count(int $expected_count): void { + + // Flush the buffer handler to ensure the log count is accurate. + $handlers = $this->logger->get_monolog()->getHandlers(); + foreach ($handlers as $handler) { + if ($handler instanceof BufferHandler) { + $handler->flush(); + } + } + $actual_count = $this->get_log_count(); $this->assertEquals($expected_count, $actual_count, "Expected log count to be {$expected_count}, but got {$actual_count}."); } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php index af117a7f..5b31a28a 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php @@ -6,6 +6,7 @@ use WPGraphQL\Logging\Plugin; +use Monolog\Handler\BufferHandler; use lucatume\WPBrowser\TestCase\WPTestCase; use WPGraphQL\Logging\Events\QueryFilterLogger; use WPGraphQL\Logging\Events\Events; @@ -55,6 +56,15 @@ public function get_log_count(): int { } public function assert_log_count(int $expected_count): void { + + // Flush the buffer handler to ensure the log count is accurate. + $handlers = $this->logger->get_monolog()->getHandlers(); + foreach ($handlers as $handler) { + if ($handler instanceof BufferHandler) { + $handler->flush(); + } + } + $actual_count = $this->get_log_count(); $this->assertEquals($expected_count, $actual_count, "Expected log count to be {$expected_count}, but got {$actual_count}."); } From e921f1862a9e91cbabcefedd13a2d0b6b2610b78 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 24 Oct 2025 10:51:36 +0100 Subject: [PATCH 12/59] Fixed typo. --- .../wpgraphql-logging/src/Logger/Database/DatabaseEntity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 3f2963ba..8c9b1728 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -366,7 +366,7 @@ public static function get_schema(): string { datetime DATETIME NOT NULL, PRIMARY KEY (id), INDEX channel_index (channel), - INDEX level_name_index (level_name) + INDEX level_name_index (level_name), INDEX level_index (level), INDEX datetime_index (datetime) ) {$charset_collate}; From e9c1dcbdc9c9e44b00c4a17bf033a9614170bb8c Mon Sep 17 00:00:00 2001 From: ahuseyn Date: Mon, 27 Oct 2025 17:55:19 +0100 Subject: [PATCH 13/59] extend recommended rules --- .../src/Logger/Processors/DataSanitizationProcessor.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php index b941054a..b246f5e3 100644 --- a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php +++ b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php @@ -67,6 +67,9 @@ protected function get_recommended_rules(): array { 'request.app_context.viewer.allcaps' => 'remove', 'request.app_context.viewer.cap_key' => 'remove', 'request.app_context.viewer.caps' => 'remove', + 'variables.username' => 'anonymize', + 'variables.password' => 'anonymize', + 'variables.email' => 'anonymize', ]; return apply_filters( 'wpgraphql_logging_data_sanitization_recommended_rules', $rules ); From 8c617e49ac9ba2a7c26823a180ffa45e3170226f Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 09:22:20 +0000 Subject: [PATCH 14/59] Fix formatting issue. --- .../src/Logger/Processors/DataSanitizationProcessor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php index b246f5e3..f90a4b57 100644 --- a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php +++ b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php @@ -67,9 +67,9 @@ protected function get_recommended_rules(): array { 'request.app_context.viewer.allcaps' => 'remove', 'request.app_context.viewer.cap_key' => 'remove', 'request.app_context.viewer.caps' => 'remove', - 'variables.username' => 'anonymize', - 'variables.password' => 'anonymize', - 'variables.email' => 'anonymize', + 'variables.username' => 'anonymize', + 'variables.password' => 'anonymize', + 'variables.email' => 'anonymize', ]; return apply_filters( 'wpgraphql_logging_data_sanitization_recommended_rules', $rules ); From d3822e61a7126552ee252037164b30b11c4bd667 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 10:12:09 +0000 Subject: [PATCH 15/59] Fixed issuef for passing variable by reference when the value is null for DatabaseSanitization --- .../src/Logger/Processors/DataSanitizationProcessor.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php index f90a4b57..ff0b340c 100644 --- a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php +++ b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php @@ -145,7 +145,8 @@ protected function &navigate_to_parent(array &$data, array $keys): ?array { $current = &$data; foreach ( $keys as $segment ) { if ( ! is_array( $current ) || ! isset( $current[ $segment ] ) ) { - return null; + $null = null; + return $null; } $current = &$current[ $segment ]; } From 3b760f9fd3dd08870b0ec189736db0f5f8e633a8 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 11:30:40 +0000 Subject: [PATCH 16/59] Add log entity interface and WordPress DB entity Introduces LogEntityInterface defining the contract for log entities and implements it in WordPressDatabaseEntity, which provides methods for creating, saving, and managing log entries in a WordPress database. This lays the foundation for structured logging within the WPGraphQL Logging plugin. --- .../src/Logger/Api/LogEntityInterface.php | 99 +++++++ .../Database/WordPressDatabaseEntity.php | 268 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php create mode 100644 plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php diff --git a/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php new file mode 100644 index 00000000..eb585e67 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php @@ -0,0 +1,99 @@ + The context of the log entry. + */ + public function get_context(): array; + + /** + * Gets the extra data of the log entry. + * + * @return array The extra data of the log entry. + */ + public function get_extra(): array; + + /** + * Gets the datetime of the log entry. + * + * @return string The datetime of the log entry in MySQL format. + */ + public function get_datetime(): string; + + /** + * Gets the schema for the log entry. + * + * @return string The schema for the log entry. + */ + public function get_schema(): string; + + /** + * Creates a new log entry. + * + * @param string $channel The channel for the log entry. + * @param int $level The logging level. + * @param string $level_name The name of the logging level. + * @param string $message The log message. + * @param array $context Additional context for the log entry. + * @param array $extra Extra data for the log entry. + * + * @return self The log entry. + */ + public function create(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []): self; + + /** + * Saves the log entry + * + * @return int|null The ID of the newly created log entry, or null on failure. + */ + public function save(): ?int; +} diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php new file mode 100644 index 00000000..e614ac82 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php @@ -0,0 +1,268 @@ + + */ + protected array $context = []; + + /** + * Extra data for the log entry. + * + * @var array + */ + protected array $extra = []; + + /** + * The datetime of the log entry. + * + * @var string + */ + protected string $datetime = ''; + + /** + * The constructor is protected to encourage creation via static methods. + */ + public function __construct() { + // Set a default datetime for new, unsaved entries. + $this->datetime = current_time( 'mysql', 1 ); + } + + /** + * Gets the ID of the log entry. + */ + public function get_id(): int { + return (int) $this->id; + } + + /** + * Gets the channel of the log entry. + */ + public function get_channel(): string { + return $this->channel; + } + + /** + * Gets the logging level of the log entry. + */ + public function get_level(): int { + return $this->level; + } + + /** + * Gets the name of the logging level of the log entry. + */ + public function get_level_name(): string { + return $this->level_name; + } + + /** + * Gets the message of the log entry. + * + * @return string The message of the log entry. + */ + public function get_message(): string { + return $this->message; + } + + /** + * Gets the context of the log entry. + * + * @return array The context of the log entry. + */ + public function get_context(): array { + return $this->context; + } + + /** + * Gets the extra data of the log entry. + * + * @return array The extra data of the log entry. + */ + public function get_extra(): array { + return $this->extra; + } + + /** + * Gets the datetime of the log entry. + * + * @return string The datetime of the log entry in MySQL format. + */ + public function get_datetime(): string { + return $this->datetime; + } + + /** + * Gets the schema for the log entry. + * + * @return string The schema for the log entry. + */ + public function get_schema(): string { + global $wpdb; + $table_name = $this->get_table_name(); + $charset_collate = $wpdb->get_charset_collate(); + + // **IMPORTANT**: This schema format with PRIMARY KEY on its own line is the + // correct and stable way to work with dbDelta. + return " + CREATE TABLE {$table_name} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + channel VARCHAR(191) NOT NULL, + level SMALLINT UNSIGNED NOT NULL, + level_name VARCHAR(50) NOT NULL, + message LONGTEXT NOT NULL, + context JSON NULL, + extra JSON NULL, + datetime DATETIME NOT NULL, + PRIMARY KEY (id), + INDEX channel_index (channel), + INDEX level_name_index (level_name), + INDEX level_index (level), + INDEX datetime_index (datetime) + ) {$charset_collate}; + "; + } + + /** + * Gets the name of the table for the log entry. + * + * @return string The name of the table for the log entry. + */ + public function get_table_name(): string { + global $wpdb; + return apply_filters( 'wpgraphql_logging_database_name', $wpdb->prefix . 'wpgraphql_logging' ); + } + + /** + * Creates a new, unsaved log entry instance. + * + * @param string $channel The channel for the log entry. + * @param int $level The logging level. + * @param string $level_name The name of the logging level. + * @param string $message The log message. + * @param array $context Additional context for the log entry. + * @param array $extra Extra data for the log entry. + */ + public function create(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []): self { + $entity = new self(); + $entity->channel = $this->sanitize_text_field( $channel ); + $entity->level = $level; + $entity->level_name = $this->sanitize_text_field( $level_name ); + $entity->message = $this->sanitize_text_field( $message ); + $entity->context = $this->sanitize_array_field( $context ); + $entity->extra = $this->sanitize_array_field( $extra ); + + return $entity; + } + + /** + * Saves a new logging entity to the database. This is an insert-only operation. + * + * @return int|null The ID of the newly created log entry, or 0 on failure. + */ + public function save(): ?int { + global $wpdb; + $table_name = self::get_table_name(); + + $data = [ + 'channel' => $this->get_channel(), + 'level' => $this->get_level(), + 'level_name' => $this->get_level_name(), + 'message' => $this->get_message(), + 'context' => wp_json_encode( $this->get_context() ), + 'extra' => wp_json_encode( $this->get_extra() ), + 'datetime' => $this->get_datetime(), + ]; + + $formats = [ '%s', '%d', '%s', '%s', '%s', '%s', '%s' ]; + + $result = $wpdb->insert( $table_name, $data, $formats ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + + if ( $result ) { + $this->id = (int) $wpdb->insert_id; + return $this->get_id(); + } + + return null; + } + + /** + * Sanitizes a text field. + * + * @param string $value The value to sanitize. + */ + protected function sanitize_text_field(string $value): string { + return sanitize_text_field( $value ); + } + + /** + * Sanitizes an array field recursively. + * + * @param array $data The array to sanitize. + * + * @return array The sanitized array. + */ + protected function sanitize_array_field(array $data): array { + foreach ( $data as &$value ) { + if ( is_string( $value ) ) { + $value = $this->sanitize_text_field( $value ); + continue; + } + + if ( is_array( $value ) ) { + $value = $this->sanitize_array_field( $value ); + } + } + return $data; + } +} From 96078ddb4beb8037a4c5be8464d0b9aaec4f6a85 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 16:39:55 +0000 Subject: [PATCH 17/59] Refactor logging interfaces and add database log service Refactored LogEntityInterface to require a constructor and static from_array method, and removed the create method. Added LogServiceInterface for CRUD operations on log entities. Updated WordPressDatabaseEntity to implement the new interface, including a new constructor, from_array, and static get_table_name. Added WordPressDatabaseLogService to provide database-backed CRUD operations for log entities. --- .../src/Logger/Api/LogEntityInterface.php | 42 ++-- .../src/Logger/Api/LogServiceInterface.php | 82 +++++++ .../Database/WordPressDatabaseEntity.php | 112 +++++---- .../Database/WordPressDatabaseLogService.php | 221 ++++++++++++++++++ 4 files changed, 390 insertions(+), 67 deletions(-) create mode 100644 plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php create mode 100644 plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php diff --git a/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php index eb585e67..f00f2428 100644 --- a/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php +++ b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php @@ -11,8 +11,28 @@ * * @since 0.0.1 */ - interface LogEntityInterface { + /** + * The constructor. + * + * @param string $channel The channel for the log entry. + * @param int $level The logging level. + * @param string $level_name The name of the logging level. + * @param string $message The log message. + * @param array $context Additional context for the log entry. + * @param array $extra Extra data for the log entry. + */ + public function __construct(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []); + + /** + * Creates a new log entry instance from an array. + * + * @param array $data The array to create the log entry from. + * + * @return \WPGraphQL\Logging\Logger\Api\LogEntityInterface The created log entry instance. + */ + public static function from_array(array $data): self; + /** * Gets the ID of the log entry. * @@ -77,23 +97,9 @@ public function get_datetime(): string; public function get_schema(): string; /** - * Creates a new log entry. - * - * @param string $channel The channel for the log entry. - * @param int $level The logging level. - * @param string $level_name The name of the logging level. - * @param string $message The log message. - * @param array $context Additional context for the log entry. - * @param array $extra Extra data for the log entry. - * - * @return self The log entry. - */ - public function create(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []): self; - - /** - * Saves the log entry + * Saves the log entry to the database. * - * @return int|null The ID of the newly created log entry, or null on failure. + * @return int The ID of the saved log entry, or 0 on failure. */ - public function save(): ?int; + public function save(): int; } diff --git a/plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php b/plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php new file mode 100644 index 00000000..ccdbd1f0 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php @@ -0,0 +1,82 @@ + $context Additional context for the log entry. + * @param array $extra Extra data for the log entry. + * + * @return \WPGraphQL\Logging\Logger\Api\LogEntityInterface|null The created log entity, or null on failure. + */ + public function create_log_entity(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []): ?LogEntityInterface; + + /** + * Finds a log entity by ID. + * + * @param int $id The ID of the log entity. + * + * @return \WPGraphQL\Logging\Logger\Api\LogEntityInterface|null The found log entity, or null on failure. + */ + public function find_entity_by_id(int $id): ?LogEntityInterface; + + /** + * Finds log entities by a where clause. + * + * @param array $args The arguments for the where clause. + * + * @return array<\WPGraphQL\Logging\Logger\Api\LogEntityInterface> The found log entities. + */ + public function find_entities_by_where(array $args = []): array; + + /** + * Deletes a log entity by ID. + * + * @param int $id The ID of the log entity. + * + * @return bool True if the log entity was deleted, false otherwise. + */ + public function delete_entity_by_id(int $id): bool; + + /** + * Deletes log entities older than a specific date. + * + * @param \DateTime $date The date to delete log entities older than. + * + * @return bool True if the log entities were deleted, false otherwise. + */ + public function delete_entities_older_than(DateTime $date): bool; + + /** + * Deletes all log entities. + * + * @return bool True if the log entities were deleted, false otherwise. + */ + public function delete_all_entities(): bool; + + /** + * Counts the number of log entities by a where clause. + * + * @param array $args The arguments for the where clause. + * + * @return int The number of log entities. + */ + public function count_entities_by_where(array $args = []): int; +} diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php index e614ac82..84472c00 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php @@ -71,13 +71,48 @@ class WordPressDatabaseEntity implements LogEntityInterface { protected string $datetime = ''; /** - * The constructor is protected to encourage creation via static methods. + * Creates a new, unsaved log entry instance. + * + * @param string $channel The channel for the log entry. + * @param int $level The logging level. + * @param string $level_name The name of the logging level. + * @param string $message The log message. + * @param array $context Additional context for the log entry. + * @param array $extra Extra data for the log entry. */ - public function __construct() { + public function __construct(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []) { + $this->channel = $this->sanitize_text_field( $channel ); + $this->level = $level; + $this->level_name = $this->sanitize_text_field( $level_name ); + $this->message = $this->sanitize_text_field( $message ); + $this->context = $this->sanitize_array_field( $context ); + $this->extra = $this->sanitize_array_field( $extra ); + // Set a default datetime for new, unsaved entries. $this->datetime = current_time( 'mysql', 1 ); } + /** + * Creates a new log entry instance from an array. + * + * @param array $data The array to create the log entry from. + * + * @return \WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity The created log entry instance. + */ + public static function from_array(array $data): self { + $entity = new self( + (string) $data['channel'], + (int) $data['level'], + (string) $data['level_name'], + (string) $data['message'], + ( isset( $data['context'] ) && '' !== $data['context'] ) ? json_decode( $data['context'], true ) : [], + ( isset( $data['extra'] ) && '' !== $data['extra'] ) ? json_decode( $data['extra'], true ) : [], + ); + $entity->id = (int) $data['id']; + $entity->datetime = (string) $data['datetime']; + return $entity; + } + /** * Gets the ID of the log entry. */ @@ -149,11 +184,9 @@ public function get_datetime(): string { */ public function get_schema(): string { global $wpdb; - $table_name = $this->get_table_name(); + $table_name = self::get_table_name(); $charset_collate = $wpdb->get_charset_collate(); - // **IMPORTANT**: This schema format with PRIMARY KEY on its own line is the - // correct and stable way to work with dbDelta. return " CREATE TABLE {$table_name} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -174,54 +207,24 @@ public function get_schema(): string { } /** - * Gets the name of the table for the log entry. + * Saves the log entry to the database. * - * @return string The name of the table for the log entry. + * @return int The ID of the saved log entry, or 0 on failure. */ - public function get_table_name(): string { + public function save(): int { global $wpdb; - return apply_filters( 'wpgraphql_logging_database_name', $wpdb->prefix . 'wpgraphql_logging' ); - } + $table_name = $this->get_table_name(); - /** - * Creates a new, unsaved log entry instance. - * - * @param string $channel The channel for the log entry. - * @param int $level The logging level. - * @param string $level_name The name of the logging level. - * @param string $message The log message. - * @param array $context Additional context for the log entry. - * @param array $extra Extra data for the log entry. - */ - public function create(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []): self { - $entity = new self(); - $entity->channel = $this->sanitize_text_field( $channel ); - $entity->level = $level; - $entity->level_name = $this->sanitize_text_field( $level_name ); - $entity->message = $this->sanitize_text_field( $message ); - $entity->context = $this->sanitize_array_field( $context ); - $entity->extra = $this->sanitize_array_field( $extra ); - - return $entity; - } - - /** - * Saves a new logging entity to the database. This is an insert-only operation. - * - * @return int|null The ID of the newly created log entry, or 0 on failure. - */ - public function save(): ?int { - global $wpdb; - $table_name = self::get_table_name(); + // Note: Data sanitization is handled in the constructor.. $data = [ - 'channel' => $this->get_channel(), - 'level' => $this->get_level(), - 'level_name' => $this->get_level_name(), - 'message' => $this->get_message(), - 'context' => wp_json_encode( $this->get_context() ), - 'extra' => wp_json_encode( $this->get_extra() ), - 'datetime' => $this->get_datetime(), + 'channel' => $this->channel, + 'level' => $this->level, + 'level_name' => $this->level_name, + 'message' => $this->message, + 'context' => wp_json_encode( $this->context ), + 'extra' => wp_json_encode( $this->extra ), + 'datetime' => $this->datetime, ]; $formats = [ '%s', '%d', '%s', '%s', '%s', '%s', '%s' ]; @@ -230,10 +233,21 @@ public function save(): ?int { if ( $result ) { $this->id = (int) $wpdb->insert_id; - return $this->get_id(); + return $this->id; } - return null; + return 0; + } + + /** + * Gets the name of the table for the log entry. + * + * @return string The name of the table for the log entry. + */ + public static function get_table_name(): string { + global $wpdb; + // @TODO - Check for multisite + return apply_filters( 'wpgraphql_logging_database_name', $wpdb->prefix . 'wpgraphql_logging' ); } /** diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php new file mode 100644 index 00000000..886a0f49 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php @@ -0,0 +1,221 @@ + + */ + protected array $where_values = []; + + /** + * Creates a new log entity. + * + * @param string $channel The channel for the log entry. + * @param int $level The logging level. + * @param string $level_name The name of the logging level. + * @param string $message The log message. + * @param array $context Additional context for the log entry. + * @param array $extra Extra data for the log entry. + * + * @return \WPGraphQL\Logging\Logger\Api\LogEntityInterface|null The created log entity, or null on failure. + */ + public function create_log_entity(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []): ?LogEntityInterface { + $entity = new WordPressDatabaseEntity( $channel, $level, $level_name, $message, $context, $extra ); + $entity->save(); + return $entity->get_id() > 0 ? $entity : null; + } + + /** + * Finds a log entity by ID. + * + * @param int $id The ID of the log entity. + * + * @return \WPGraphQL\Logging\Logger\Api\LogEntityInterface|null The found log entity, or null on failure. + */ + public function find_entity_by_id(int $id): ?LogEntityInterface { + global $wpdb; + $table_name = $this->get_table_name(); + + $query = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $row = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + + if ( ! $row ) { + return null; + } + + return WordPressDatabaseEntity::from_array( $row ); + } + + /** + * Finds log entities by a where clause. + * + * @param array $args The arguments for the where clause. + * + * @return array<\WPGraphQL\Logging\Logger\Api\LogEntityInterface> The found log entities. + */ + public function find_entities_by_where(array $args = []): array { + global $wpdb; + + $this->where_values = []; + $sql = 'SELECT * FROM %i'; + $this->where_values[] = sanitize_text_field( $this->get_table_name() ); + $sql = $this->prepare_sql( $sql, $args ); + + // @TODO + // Add the order by and limit to the SQL query. + // $sql .= " ORDER BY $orderby $order LIMIT %d, %d"; + // $values[] = $offset; + // $values[] = $limit; + // $query = $wpdb->prepare( $sql, $values ); + // @TODO - Fix this. + $results = $wpdb->get_results( $wpdb->prepare( $sql, $this->where_values ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + if ( empty( $results ) || ! is_array( $results ) ) { + return []; + } + + return array_map( + static function (array $row) { + return WordPressDatabaseEntity::from_array( $row ); + }, + $results + ); + } + + /** + * Deletes a log entity by ID. + * + * @param int $id The ID of the log entity. + * + * @return bool True if the log entity was deleted, false otherwise. + */ + public function delete_entity_by_id(int $id): bool { + global $wpdb; + $table_name = $this->get_table_name(); + + if ( $id <= 0 ) { + return false; + } + + $result = $wpdb->delete( $table_name, [ 'id' => $id ], [ '%d' ] ); + return false !== $result; + } + + /** + * Deletes log entities older than a specific date. + * + * @param \DateTime $date The date to delete log entities older than. + * + * @return bool True if the log entities were deleted, false otherwise. + */ + public function delete_entities_older_than(DateTime $date): bool { + global $wpdb; + $table_name = $this->get_table_name(); + + $result = $wpdb->query( $wpdb->prepare( + 'DELETE FROM %i WHERE datetime < %s', + $table_name, + $date->format( 'Y-m-d H:i:s' ) + ) ); + return false !== $result; + } + + /** + * Deletes all log entities. + * + * @return bool True if the log entities were deleted, false otherwise. + */ + public function delete_all_entities(): bool { + global $wpdb; + $table_name = $this->get_table_name(); + $result = $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', $table_name ) ); + return false !== $result; + } + + /** + * Counts the number of log entities by a where clause. + * + * @param array $args The arguments for the where clause. + * + * @return int The number of log entities. + */ + public function count_entities_by_where(array $args = []): int { + global $wpdb; + $this->where_values = []; + $sql = 'SELECT COUNT(*) FROM %i'; + $this->where_values = [ $this->get_table_name() ]; + $sql = $this->prepare_sql( $sql, $args ); + // @TODO - Fix this. + return (int) $wpdb->get_var( $wpdb->prepare( $sql, $this->where_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Gets the table name. + */ + protected function get_table_name(): string { + return WordPressDatabaseEntity::get_table_name(); + } + + /** + * Prepares the SQL query. + * + * @param string $sql The SQL query template. + * @param array $where_conditions The where conditions. + * + * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh + * + * @return string The prepared SQL query. + */ + protected function prepare_sql(string $sql, array $where_conditions): string { + $where_clauses = []; + $safe_operators = $this->get_safe_operators(); + foreach ( $where_conditions as $column => $condition ) { + if ( ! is_array( $condition ) || ! isset( $condition['value'] ) || ! isset( $condition['operator'] ) ) { + continue; + } + + $value = $condition['value'] ?? ''; + $operator = $condition['operator'] ?? ''; + if ( ! in_array( $operator, $safe_operators, true ) ) { + continue; + } + + $where_clauses[] = "%i $operator %s"; + $this->where_values[] = sanitize_text_field( (string) $column ); + $this->where_values[] = sanitize_text_field( (string) $value ); + } + + if ( ! empty( $where_clauses ) ) { + $sql .= ' WHERE ' . implode( ' AND ', $where_clauses ); + } + + return $sql; + } + + /** + * The safe operators. + * + * @return array The safe operators. + */ + protected function get_safe_operators(): array { + return [ '=', '!=', '>', '<', '>=', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ]; + } +} From e96004ffdfdf94295c4dd13db38481f78574bdba Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 16:46:00 +0000 Subject: [PATCH 18/59] Fixed codeception test coverage. --- plugins/wpgraphql-logging/bin/run-codeception.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wpgraphql-logging/bin/run-codeception.sh b/plugins/wpgraphql-logging/bin/run-codeception.sh index 107350de..6bbcc0e7 100755 --- a/plugins/wpgraphql-logging/bin/run-codeception.sh +++ b/plugins/wpgraphql-logging/bin/run-codeception.sh @@ -69,8 +69,8 @@ run_tests() { # Prefer XML summary for robustness; fallback to HTML if present if [[ -f "tests/_output/coverage.xml" ]]; then # Extract total statements and covered statements from the summary metrics line - total_statements=$(grep -Eo 'statements="[0-9]+"' "tests/_output/coverage.xml" | tail -1 | grep -Eo '[0-9]+') - total_covered=$(grep -Eo 'coveredstatements="[0-9]+"' "tests/_output/coverage.xml" | tail -1 | grep -Eo '[0-9]+') + total_statements=$(grep -Eo 'statements="[0-9]+"' "tests/_output/coverage.xml" | head -1 | grep -Eo '[0-9]+') + total_covered=$(grep -Eo 'coveredstatements="[0-9]+"' "tests/_output/coverage.xml" | head -1 | grep -Eo '[0-9]+') if [[ -n "$total_statements" && -n "$total_covered" && "$total_statements" -gt 0 ]]; then coverage_percent=$(awk "BEGIN { printf \"%.2f\", ($total_covered / $total_statements) * 100 }") fi From ea512f15b1f68890df0e080ea88da56b16ee58ad Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 17:14:31 +0000 Subject: [PATCH 19/59] Added LogStoreService. Refactored Download Service. Replaces direct repository usage with LogStoreService for log retrieval in DownloadLogService, improving extensibility. Adds get_query() to LogEntityInterface and implements it in WordPressDatabaseEntity to extract GraphQL queries from log context. Updates related tests to use the new service and interface. --- .../View/Download/DownloadLogService.php | 18 +- .../src/Logger/Api/LogEntityInterface.php | 7 + .../Database/WordPressDatabaseEntity.php | 36 ++++ .../src/Logger/Store/LogStoreService.php | 50 ++++++ .../View/Download/DownloadLogServiceTest.php | 159 +----------------- 5 files changed, 110 insertions(+), 160 deletions(-) create mode 100644 plugins/wpgraphql-logging/src/Logger/Store/LogStoreService.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php index 2caa30ea..795a08b6 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php @@ -5,8 +5,8 @@ namespace WPGraphQL\Logging\Admin\View\Download; use League\Csv\Writer; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; -use WPGraphQL\Logging\Logger\Database\LogsRepository; +use WPGraphQL\Logging\Logger\Api\LogEntityInterface; +use WPGraphQL\Logging\Logger\Store\LogStoreService; /** * Service for handling log downloads. @@ -30,8 +30,10 @@ public function generate_csv( int $log_id ): void { wp_die( esc_html__( 'Invalid log ID.', 'wpgraphql-logging' ) ); } - $repository = new LogsRepository(); - $log = $repository->get_log( $log_id ); + + $log_service = LogStoreService::get_log_service(); + $log = $log_service->find_entity_by_id( $log_id ); + if ( is_null( $log ) ) { wp_die( esc_html__( 'Log not found.', 'wpgraphql-logging' ) ); } @@ -61,11 +63,11 @@ public function generate_csv( int $log_id ): void { /** * Get default CSV headers. * - * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $log The log entry. + * @param \WPGraphQL\Logging\Logger\Api\LogEntityInterface $log The log entry. * * @return array The default CSV headers. */ - public function get_headers(DatabaseEntity $log): array { + public function get_headers(LogEntityInterface $log): array { $headers = [ 'ID', 'Date', @@ -83,11 +85,11 @@ public function get_headers(DatabaseEntity $log): array { /** * Get CSV content for a log entry. * - * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $log The log entry. + * @param \WPGraphQL\Logging\Logger\Api\LogEntityInterface $log The log entry. * * @return array The CSV content for the log entry. */ - public function get_content(DatabaseEntity $log): array { + public function get_content(LogEntityInterface $log): array { $content = [ $log->get_id(), $log->get_datetime(), diff --git a/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php index f00f2428..047e62b9 100644 --- a/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php +++ b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php @@ -89,6 +89,13 @@ public function get_extra(): array; */ public function get_datetime(): string; + /** + * Gets the query of the log entry. + * + * @return string|null The query of the log entry. + */ + public function get_query(): ?string; + /** * Gets the schema for the log entry. * diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php index 84472c00..2533bc50 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php @@ -206,6 +206,42 @@ public function get_schema(): string { "; } + /** + * Extracts and returns the GraphQL query from the context, if available. + * + * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh, Generic.Metrics.CyclomaticComplexity.TooHigh + * + * @return string|null The GraphQL query string, or null if not available. + */ + public function get_query(): ?string { + + $context = $this->get_context(); + if ( empty( $context ) ) { + return null; + } + + $query = $context['query'] ?? null; + if ( is_string( $query ) ) { + return $query; + } + + $request = $context['request'] ?? null; + if ( empty( $request ) || ! is_array( $request ) ) { + return $query; + } + + $params = $request['params'] ?? null; + if ( empty( $params ) || ! is_array( $params ) ) { + return $query; + } + + if ( isset( $params['query'] ) && is_string( $params['query'] ) ) { + return $params['query']; + } + + return $query; + } + /** * Saves the log entry to the database. * diff --git a/plugins/wpgraphql-logging/src/Logger/Store/LogStoreService.php b/plugins/wpgraphql-logging/src/Logger/Store/LogStoreService.php new file mode 100644 index 00000000..649902b8 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Store/LogStoreService.php @@ -0,0 +1,50 @@ + 'wpgraphql_logging', @@ -56,7 +55,7 @@ class DownloadLogServiceTest extends WPTestCase { public function setUp(): void { parent::setUp(); $this->service = new DownloadLogService(); - $this->repository = new LogsRepository(); + $this->repository = new WordPressDatabaseLogService(); } public function set_as_admin(): void { @@ -96,7 +95,7 @@ public function test_generate_csv_requires_valid_log_id_in_database(): void { public function test_generate_csv_returns_valid_csv(): void { $this->set_as_admin(); // Mock a database entity instead of creating a real one - $entity = \Mockery::mock(DatabaseEntity::class); + $entity = \Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_id')->andReturn(123); $entity->shouldReceive('get_datetime')->andReturn('2023-01-01 12:00:00'); $entity->shouldReceive('get_level')->andReturn($this->fixture['level']); @@ -108,8 +107,8 @@ public function test_generate_csv_returns_valid_csv(): void { $entity->shouldReceive('get_extra')->andReturn($this->fixture['extra']); // Mock the repository to return our mocked entity - $this->repository = \Mockery::mock(LogsRepository::class); - $this->repository->shouldReceive('find_by_id')->with(123)->andReturn($entity); + $this->repository = \Mockery::mock(WordPressDatabaseLogService::class); + $this->repository->shouldReceive('find_entity_by_id')->with(123)->andReturn($entity); // Inject the mocked repository into the service $this->service = new DownloadLogService($this->repository); @@ -141,150 +140,6 @@ public function test_generate_csv_returns_valid_csv(): void { $this->assertEquals($this->fixture['channel'], $content[5]); $this->assertEquals(wp_json_encode($this->fixture['context']), $content[7]); $this->assertEquals(wp_json_encode($this->fixture['extra']), $content[8]); - - - // Capture output - // ob_start(); - // $this->service->generate_csv($log_id); - // $output = ob_get_clean(); - - // // // Parse CSV output - // $lines = explode("\n", trim($output)); - // $headers = str_getcsv($lines[0]); - // $content = str_getcsv($lines[1]); - - // // Verify headers - // $expected_headers = [ - // 'ID', - // 'Date', - // 'Level', - // 'Level Name', - // 'Message', - // 'Channel', - // 'Query', - // 'Context', - // 'Extra', - // ]; - // $this->assertEquals($expected_headers, $headers); - - // // Verify content - // $this->assertEquals($log_id, $content[0]); - // $this->assertEquals('2023-01-01 12:00:00', $content[1]); - // $this->assertEquals('200', $content[2]); - // $this->assertEquals('INFO', $content[3]); - // $this->assertEquals('Test log message', $content[4]); - // $this->assertEquals('wpgraphql', $content[5]); - // $this->assertEquals('query { posts { nodes { id title } } }', $content[6]); - // $this->assertEquals('{"user_id":1}', $content[7]); - // $this->assertEquals('{"test":"data"}', $content[8]); - // } - - // public function test_generate_csv_with_nonexistent_log(): void { - // // Set admin user - // $admin_user = $this->factory->user->create(['role' => 'administrator']); - // wp_set_current_user($admin_user); - // set_current_screen('dashboard'); - - // $this->expectOutputString(''); - - // ob_start(); - // $this->service->generate_csv(999999); - // $output = ob_get_clean(); - - // $this->assertEmpty($output); } - // public function test_csv_filename_filter(): void { - // // Set admin user - // $admin_user = $this->factory->user->create(['role' => 'administrator']); - // wp_set_current_user($admin_user); - // set_current_screen('dashboard'); - - // // Create test log entry - // global $wpdb; - // $wpdb->insert( - // $wpdb->prefix . 'wpgraphql_logs', - // [ - // 'level' => 200, - // 'level_name' => 'INFO', - // 'message' => 'Test message', - // 'channel' => 'wpgraphql', - // 'datetime' => '2023-01-01 12:00:00', - // 'context' => '{}', - // 'extra' => '{}', - // 'query' => 'test query' - // ] - // ); - // $log_id = $wpdb->insert_id; - - // // Add filter for filename - // add_filter('wpgraphql_logging_csv_filename', function($filename) { - // return 'custom_log_export.csv'; - // }); - - // // Check headers contain custom filename - // ob_start(); - // $this->service->generate_csv($log_id); - // $output = ob_get_contents(); - // ob_end_clean(); - - // $headers = headers_list(); - // $content_disposition_found = false; - // foreach ($headers as $header) { - // if (strpos($header, 'Content-Disposition') !== false && strpos($header, 'custom_log_export.csv') !== false) { - // $content_disposition_found = true; - // break; - // } - // } - - // $this->assertTrue($content_disposition_found); - // } - - // public function test_csv_headers_filter(): void { - // // Set admin user - // $admin_user = $this->factory->user->create(['role' => 'administrator']); - // wp_set_current_user($admin_user); - // set_current_screen('dashboard'); - - // // Create test log entry - // global $wpdb; - // $wpdb->insert( - // $wpdb->prefix . 'wpgraphql_logs', - // [ - // 'level' => 200, - // 'level_name' => 'INFO', - // 'message' => 'Test message', - // 'channel' => 'wpgraphql', - // 'datetime' => '2023-01-01 12:00:00', - // 'context' => '{}', - // 'extra' => '{}', - // 'query' => 'test query' - // ] - // ); - // $log_id = $wpdb->insert_id; - - // // Add filter for headers - // add_filter('wpgraphql_logging_csv_headers', function($headers) { - // return array_merge($headers, ['Custom Field']); - // }); - - // // Add filter for content - // add_filter('wpgraphql_logging_csv_content', function($content) { - // return array_merge($content, ['Custom Value']); - // }); - - // ob_start(); - // $this->service->generate_csv($log_id); - // $output = ob_get_contents(); - // ob_end_clean(); - - // // Parse CSV output - // $lines = explode("\n", trim($output)); - // $headers = str_getcsv($lines[0]); - // $content = str_getcsv($lines[1]); - - // // Verify custom header and content - // $this->assertContains('Custom Field', $headers); - // $this->assertContains('Custom Value', $content); - // } } From b29d376d1016bd9a979ef4651b77f844519fea87 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 17:22:50 +0000 Subject: [PATCH 20/59] Refactor DataDeletionScheduler to use LogServiceInterface Replaces direct dependency on LogsRepository with LogServiceInterface in DataDeletionScheduler. Updates instantiation to use LogStoreService and modifies log deletion to use the new service method for improved abstraction and flexibility. --- .../src/Logger/Scheduler/DataDeletionScheduler.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php b/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php index 3ed11d42..d9590437 100644 --- a/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php +++ b/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php @@ -6,7 +6,8 @@ use WPGraphQL\Logging\Admin\Settings\ConfigurationHelper; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\DataManagementTab; -use WPGraphQL\Logging\Logger\Database\LogsRepository; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; +use WPGraphQL\Logging\Logger\Store\LogStoreService; /** * Data Deletion Scheduler class. @@ -40,9 +41,11 @@ class DataDeletionScheduler { protected static ?DataDeletionScheduler $instance = null; /** - * Private constructor to prevent direct instantiation. + * Constructor. + * + * @param \WPGraphQL\Logging\Logger\Api\LogServiceInterface $log_service The log service. */ - protected function __construct(readonly LogsRepository $repository) { + protected function __construct(protected LogServiceInterface $log_service) { $config_helper = ConfigurationHelper::get_instance(); $this->config = $config_helper->get_data_management_config(); } @@ -53,7 +56,8 @@ protected function __construct(readonly LogsRepository $repository) { public static function init(): self { if ( null === self::$instance ) { - self::$instance = new self( new LogsRepository() ); + $log_service = LogStoreService::get_log_service(); + self::$instance = new self( $log_service ); self::$instance->setup(); } @@ -131,6 +135,6 @@ protected function setup(): void { protected function delete_old_logs(int $retention_days): void { $date_time = new \DateTime(); $date_time->modify( "-{$retention_days} days" ); - $this->repository->delete_log_older_than( $date_time ); + $this->log_service->delete_entities_older_than( $date_time ); } } From a92546002e12394abf72425f4ca02584f7bb6e71 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 17:32:11 +0000 Subject: [PATCH 21/59] Refactor ViewLogsPage to use LogServiceInterface Replaces direct usage of LogsRepository with LogServiceInterface in ViewLogsPage for improved abstraction and flexibility. Updates method to retrieve logs via the log service and adds a protected getter for the log service instance. Removes unused LogsRepository import from the related test file. --- .../src/Admin/ViewLogsPage.php | 17 ++++++++++++++--- .../wpunit/Admin/View/ViewLogsPageTest.php | 1 - 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index c61ae46c..611eb399 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -6,7 +6,9 @@ use WPGraphQL\Logging\Admin\View\Download\DownloadLogService; use WPGraphQL\Logging\Admin\View\List\ListTable; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; use WPGraphQL\Logging\Logger\Database\LogsRepository; +use WPGraphQL\Logging\Logger\Store\LogStoreService; /** * The view logs page class for WPGraphQL Logging. @@ -196,7 +198,7 @@ public function process_filters_redirect(): void { * @return string The constructed redirect URL. */ public function get_redirect_url(): string { - $redirect_url = menu_page_url( self::ADMIN_PAGE_SLUG, false ); + $redirect_url = menu_page_url( self::ADMIN_PAGE_SLUG, false ); $possible_filters = [ 'start_date', @@ -276,8 +278,8 @@ protected function render_view_page(): void { return; } - $repository = new LogsRepository(); - $log = $repository->get_log( $log_id ); + $log_service = $this->get_log_service(); + $log = $log_service->find_entity_by_id( $log_id ); if ( is_null( $log ) ) { echo '

' . esc_html__( 'Log not found.', 'wpgraphql-logging' ) . '

'; @@ -291,4 +293,13 @@ protected function render_view_page(): void { require_once $log_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } + + /** + * Retrieves the log service instance. + * + * @return \WPGraphQL\Logging\Logger\Api\LogServiceInterface The log service instance. + */ + protected function get_log_service(): LogServiceInterface { + return LogStoreService::get_log_service(); + } } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php index 12968345..c4d9911a 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php @@ -6,7 +6,6 @@ use WPGraphQL\Logging\Admin\ViewLogsPage; -use WPGraphQL\Logging\Logger\Database\LogsRepository; use Codeception\TestCase\WPTestCase; use Brain\Monkey; From 9b5ee54e57db2b1cf2d67c77d6bdbdc7ce223514 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 17:53:04 +0000 Subject: [PATCH 22/59] Added missing test for getting log service. --- plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php | 2 +- .../tests/wpunit/Admin/View/ViewLogsPageTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 611eb399..04b56dc5 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -299,7 +299,7 @@ protected function render_view_page(): void { * * @return \WPGraphQL\Logging\Logger\Api\LogServiceInterface The log service instance. */ - protected function get_log_service(): LogServiceInterface { + public function get_log_service(): LogServiceInterface { return LogStoreService::get_log_service(); } } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php index c4d9911a..ee6767fa 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php @@ -8,6 +8,7 @@ use WPGraphQL\Logging\Admin\ViewLogsPage; use Codeception\TestCase\WPTestCase; use Brain\Monkey; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; /** * Test for the ViewLogsPage @@ -213,4 +214,11 @@ public function test_get_redirect_url_constructs_correct_url(): void { $url ); } + + public function test_get_log_service_returns_log_service_instance(): void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + $log_service = $instance->get_log_service(); + $this->assertInstanceOf(LogServiceInterface::class, $log_service); + } } From 47bb318130e9530ab995d5b97fffcadaae665d66 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 28 Oct 2025 18:00:43 +0000 Subject: [PATCH 23/59] Revert test as typo with changing signature and not needed. --- plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php | 2 +- .../tests/wpunit/Admin/View/ViewLogsPageTest.php | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 04b56dc5..611eb399 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -299,7 +299,7 @@ protected function render_view_page(): void { * * @return \WPGraphQL\Logging\Logger\Api\LogServiceInterface The log service instance. */ - public function get_log_service(): LogServiceInterface { + protected function get_log_service(): LogServiceInterface { return LogStoreService::get_log_service(); } } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php index ee6767fa..df2cb3f8 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php @@ -214,11 +214,4 @@ public function test_get_redirect_url_constructs_correct_url(): void { $url ); } - - public function test_get_log_service_returns_log_service_instance(): void { - $this->set_as_admin(); - $instance = ViewLogsPage::init(); - $log_service = $instance->get_log_service(); - $this->assertInstanceOf(LogServiceInterface::class, $log_service); - } } From 0305fcab8556d8e1a301dc29d63d304a93d74fd9 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 12:17:23 +0000 Subject: [PATCH 24/59] Refactored DownloadLogService to inject service. --- .../src/Admin/View/Download/DownloadLogService.php | 14 ++++++++++---- .../wpgraphql-logging/src/Admin/ViewLogsPage.php | 3 +-- .../Admin/View/Download/DownloadLogServiceTest.php | 12 ++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php index 795a08b6..898812b8 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php @@ -6,7 +6,7 @@ use League\Csv\Writer; use WPGraphQL\Logging\Logger\Api\LogEntityInterface; -use WPGraphQL\Logging\Logger\Store\LogStoreService; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; /** * Service for handling log downloads. @@ -16,6 +16,14 @@ * @since 0.0.1 */ class DownloadLogService { + /** + * Constructor. + * + * @param \WPGraphQL\Logging\Logger\Api\LogServiceInterface $log_service The log service. + */ + public function __construct(protected readonly LogServiceInterface $log_service) { + } + /** * Generates and serves a CSV file for a single log entry. * @@ -30,9 +38,7 @@ public function generate_csv( int $log_id ): void { wp_die( esc_html__( 'Invalid log ID.', 'wpgraphql-logging' ) ); } - - $log_service = LogStoreService::get_log_service(); - $log = $log_service->find_entity_by_id( $log_id ); + $log = $this->log_service->find_entity_by_id( $log_id ); if ( is_null( $log ) ) { wp_die( esc_html__( 'Log not found.', 'wpgraphql-logging' ) ); diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 611eb399..b2f8399b 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -245,7 +245,6 @@ protected function get_post_value(string $key): ?string { * Renders the list page for log entries. */ protected function render_list_page(): void { - // Variable required for list template. $list_table = new ListTable( new LogsRepository() ); // @phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable $list_template = apply_filters( 'wpgraphql_logging_list_template', @@ -263,7 +262,7 @@ protected function process_log_download(): void { } $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended - $downloader = new DownloadLogService(); + $downloader = new DownloadLogService( $this->get_log_service() ); $downloader->generate_csv( $log_id ); } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php index a2d6f65a..3a958b66 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php @@ -21,7 +21,7 @@ class DownloadLogServiceTest extends WPTestCase { private DownloadLogService $service; - private WordPressDatabaseLogService $repository; + private WordPressDatabaseLogService $log_service; protected array $fixture = [ 'channel' => 'wpgraphql_logging', @@ -54,8 +54,8 @@ class DownloadLogServiceTest extends WPTestCase { public function setUp(): void { parent::setUp(); - $this->service = new DownloadLogService(); - $this->repository = new WordPressDatabaseLogService(); + $this->log_service = new WordPressDatabaseLogService(); + $this->service = new DownloadLogService($this->log_service); } public function set_as_admin(): void { @@ -107,11 +107,11 @@ public function test_generate_csv_returns_valid_csv(): void { $entity->shouldReceive('get_extra')->andReturn($this->fixture['extra']); // Mock the repository to return our mocked entity - $this->repository = \Mockery::mock(WordPressDatabaseLogService::class); - $this->repository->shouldReceive('find_entity_by_id')->with(123)->andReturn($entity); + $this->log_service = \Mockery::mock(WordPressDatabaseLogService::class); + $this->log_service->shouldReceive('find_entity_by_id')->with(123)->andReturn($entity); // Inject the mocked repository into the service - $this->service = new DownloadLogService($this->repository); + $this->service = new DownloadLogService($this->log_service); $log_id = 123; From f311a4f13d61eccdaa658c168e967e3936987d4d Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 14:35:07 +0000 Subject: [PATCH 25/59] Refactored ListTable to use LogServiceInterface. Refactored argument processing for Database Service. --- .../src/Admin/View/List/ListTable.php | 74 ++++++++------- .../View/Templates/WPGraphQLLoggerView.php | 2 +- .../src/Admin/ViewLogsPage.php | 3 +- .../Database/WordPressDatabaseLogService.php | 37 +++++--- .../wpunit/Admin/View/List/ListTableTest.php | 93 +++++++++++-------- 5 files changed, 122 insertions(+), 87 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php index b3138299..a1d68afb 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php @@ -4,8 +4,9 @@ namespace WPGraphQL\Logging\Admin\View\List; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; -use WPGraphQL\Logging\Logger\Database\LogsRepository; +use DateTime; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity; use WP_List_Table; // Include the WP_List_Table class if not already loaded. @@ -33,11 +34,11 @@ class ListTable extends WP_List_Table { /** * Constructor. * - * @param \WPGraphQL\Logging\Logger\Database\LogsRepository $repository The logs repository. + * @param \WPGraphQL\Logging\Logger\Api\LogServiceInterface $log_service The log service. * @param array $args Optional. An array of arguments. */ public function __construct( - public readonly LogsRepository $repository, + public readonly LogServiceInterface $log_service, $args = [] ) { $args = wp_parse_args( @@ -75,7 +76,7 @@ public function prepare_items(): void { $current_page = $this->get_pagenum(); /** @psalm-suppress InvalidArgument */ $where = $this->process_where( $_REQUEST ); - $total_items = $this->repository->get_log_count( $where ); + $total_items = $this->log_service->count_entities_by_where( $where ); $this->set_pagination_args( [ @@ -97,8 +98,7 @@ public function prepare_items(): void { $args['order'] = sanitize_text_field( wp_unslash( (string) $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } $args['where'] = $where; - - $this->items = $this->repository->get_logs( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); + $this->items = $this->log_service->find_entities_by_where( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); } /** @@ -148,15 +148,15 @@ public function process_bulk_action(): void { // Remove redundant empty check since array_map always returns array. foreach ( $ids as $id ) { if ( $id > 0 ) { // Only process valid IDs. - $this->repository->delete( $id ); + $this->log_service->delete_entity_by_id( $id ); } } $deleted_count = count( array_filter( $ids, static fn( $id ) => $id > 0 ) ); } if ( 'delete_all' === $action ) { - $count_before_delete = $this->repository->get_log_count( [] ); - $this->repository->delete_all(); + $count_before_delete = $this->log_service->count_entities_by_where( [] ); + $this->log_service->delete_all_entities(); $deleted_count = $count_before_delete; } @@ -224,15 +224,15 @@ public function get_columns(): array { /** * Get the default column value for a log entry. * - * @param mixed|\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. - * @param string $column_name The column name. + * @param mixed|\WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity $item The log entry item. + * @param string $column_name The column name. * * @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded * * @return mixed The default column value or null. */ public function column_default( $item, $column_name ): mixed { - if ( ! $item instanceof DatabaseEntity ) { + if ( ! $item instanceof WordPressDatabaseEntity ) { return null; } @@ -277,12 +277,12 @@ public function column_default( $item, $column_name ): mixed { /** * Renders the checkbox column for a log entry. * - * @param mixed|\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * @param mixed|\WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity $item The log entry item. * * @return string The rendered checkbox column or null. */ public function column_cb( $item ): string { - if ( ! $item instanceof DatabaseEntity ) { + if ( ! $item instanceof WordPressDatabaseEntity ) { return ''; } return sprintf( @@ -294,11 +294,11 @@ public function column_cb( $item ): string { /** * Renders the ID column for a log entry. * - * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * @param \WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity $item The log entry item. * * @return string The rendered ID column or null. */ - public function column_id( DatabaseEntity $item ): string { + public function column_id( WordPressDatabaseEntity $item ): string { $url = \WPGraphQL\Logging\Admin\ViewLogsPage::ADMIN_PAGE_SLUG; $actions = [ 'view' => sprintf( @@ -327,11 +327,11 @@ public function column_id( DatabaseEntity $item ): string { /** * Renders the query column for a log entry. * - * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * @param \WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity $item The log entry item. * * @return string|null The rendered query column or null. */ - public function column_query( DatabaseEntity $item ): ?string { + public function column_query( WordPressDatabaseEntity $item ): ?string { $extra = $item->get_extra(); return ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; } @@ -341,7 +341,7 @@ public function column_query( DatabaseEntity $item ): ?string { * * @return string The query */ - public function get_query(DatabaseEntity $item): string { + public function get_query(WordPressDatabaseEntity $item): string { $query = $item->get_query(); if ( ! is_string( $query ) || '' === $query ) { return ''; @@ -354,7 +354,7 @@ public function get_query(DatabaseEntity $item): string { * * @return string The event */ - public function get_event(DatabaseEntity $item): string { + public function get_event(WordPressDatabaseEntity $item): string { $extra = $item->get_extra(); return ! empty( $extra['wpgraphql_event'] ) ? esc_html( $extra['wpgraphql_event'] ) : $item->get_message(); @@ -363,11 +363,11 @@ public function get_event(DatabaseEntity $item): string { /** * Gets the event from extra. * - * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * @param \WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity $item The log entry item. * * @return int The event */ - public function get_process_id(DatabaseEntity $item): int { + public function get_process_id(WordPressDatabaseEntity $item): int { $extra = $item->get_extra(); return ! empty( $extra['process_id'] ) ? (int) $extra['process_id'] : 0; } @@ -377,7 +377,7 @@ public function get_process_id(DatabaseEntity $item): int { * * @return string The event */ - public function get_memory_usage(DatabaseEntity $item): string { + public function get_memory_usage(WordPressDatabaseEntity $item): string { $extra = $item->get_extra(); return ! empty( $extra['memory_peak_usage'] ) ? esc_html( $extra['memory_peak_usage'] ) : ''; } @@ -387,7 +387,7 @@ public function get_memory_usage(DatabaseEntity $item): string { * * @return string The event */ - public function get_request_headers(DatabaseEntity $item): string { + public function get_request_headers(WordPressDatabaseEntity $item): string { $extra = $item->get_extra(); $request_headers = $extra['request_headers'] ?? []; if ( empty( $request_headers ) || ! is_array( $request_headers ) ) { @@ -420,7 +420,7 @@ protected function format_code(string $code): string { * * @param array $request The request data. * - * @return array The where clauses. + * @return array The where clauses. */ protected function process_where(array $request): array { $where_clauses = []; @@ -431,19 +431,31 @@ protected function process_where(array $request): array { if ( ! empty( $request['level_filter'] ) ) { $level = sanitize_text_field( wp_unslash( (string) $request['level_filter'] ) ); - $where_clauses[] = "level_name = '" . $level . "'"; + $where_clauses[] = [ + 'column' => 'level_name', + 'operator' => '=', + 'value' => $level, + ]; } if ( ! empty( $request['start_date'] ) ) { $start_date = sanitize_text_field( $request['start_date'] ); - $date = new \DateTime( $start_date ); - $where_clauses[] = "datetime >= '" . $date->format( 'Y-m-d H:i:s' ) . "'"; + $date = new DateTime( $start_date ); + $where_clauses[] = [ + 'column' => 'datetime', + 'operator' => '>=', + 'value' => $date->format( 'Y-m-d H:i:s' ), + ]; } if ( ! empty( $request['end_date'] ) ) { $end_date = sanitize_text_field( $request['end_date'] ); - $date = new \DateTime( $end_date ); - $where_clauses[] = "datetime <= '" . $date->format( 'Y-m-d H:i:s' ) . "'"; + $date = new DateTime( $end_date ); + $where_clauses[] = [ + 'column' => 'datetime', + 'operator' => '<=', + 'value' => $date->format( 'Y-m-d H:i:s' ), + ]; } // Allow developers to modify the where clauses. diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerView.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerView.php index a8539a1e..244a2481 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerView.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerView.php @@ -7,7 +7,7 @@ * * @package WPGraphQL\Logging * - * @var \WPGraphQL\Logging\Logger\Database\DatabaseEntity $log + * @var \WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity $log * * @since 0.0.1 */ diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index b2f8399b..3189abfb 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -7,7 +7,6 @@ use WPGraphQL\Logging\Admin\View\Download\DownloadLogService; use WPGraphQL\Logging\Admin\View\List\ListTable; use WPGraphQL\Logging\Logger\Api\LogServiceInterface; -use WPGraphQL\Logging\Logger\Database\LogsRepository; use WPGraphQL\Logging\Logger\Store\LogStoreService; /** @@ -245,7 +244,7 @@ protected function get_post_value(string $key): ?string { * Renders the list page for log entries. */ protected function render_list_page(): void { - $list_table = new ListTable( new LogsRepository() ); // @phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable + $list_table = new ListTable( $this->get_log_service() ); // @phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable $list_template = apply_filters( 'wpgraphql_logging_list_template', __DIR__ . '/View/Templates/WPGraphQLLoggerList.php' diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php index 886a0f49..33bcd838 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php @@ -77,15 +77,19 @@ public function find_entities_by_where(array $args = []): array { $this->where_values = []; $sql = 'SELECT * FROM %i'; $this->where_values[] = sanitize_text_field( $this->get_table_name() ); - $sql = $this->prepare_sql( $sql, $args ); - - // @TODO - // Add the order by and limit to the SQL query. - // $sql .= " ORDER BY $orderby $order LIMIT %d, %d"; - // $values[] = $offset; - // $values[] = $limit; - // $query = $wpdb->prepare( $sql, $values ); - // @TODO - Fix this. + if ( isset( $args['where'] ) && is_array( $args['where'] ) ) { + $sql = $this->prepare_sql( $sql, $args['where'] ); + } + + $orderby = $args['orderby'] ?? 'id'; + $order = $args['order'] ?? 'DESC'; + $limit = $args['number'] ?? 100; + $offset = $args['offset'] ?? 0; + + $sql .= " ORDER BY $orderby $order LIMIT %d, %d"; + $this->where_values[] = (string) $offset; + $this->where_values[] = (string) $limit; + $results = $wpdb->get_results( $wpdb->prepare( $sql, $this->where_values ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( empty( $results ) || ! is_array( $results ) ) { @@ -177,10 +181,11 @@ protected function get_table_name(): string { /** * Prepares the SQL query. * - * @param string $sql The SQL query template. - * @param array $where_conditions The where conditions. + * @param string $sql The SQL query template. + * @param array $where_conditions The where conditions. * * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh + * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh * * @return string The prepared SQL query. */ @@ -188,12 +193,16 @@ protected function prepare_sql(string $sql, array $where_conditions): string { $where_clauses = []; $safe_operators = $this->get_safe_operators(); foreach ( $where_conditions as $column => $condition ) { - if ( ! is_array( $condition ) || ! isset( $condition['value'] ) || ! isset( $condition['operator'] ) ) { + if ( ! is_array( $condition ) || ! isset( $condition['column'] ) || ! isset( $condition['value'] ) || ! isset( $condition['operator'] ) ) { continue; } - $value = $condition['value'] ?? ''; - $operator = $condition['operator'] ?? ''; + $column = $condition['column']; + if ( '' === $column ) { + continue; + } + $value = $condition['value']; + $operator = $condition['operator']; if ( ! in_array( $operator, $safe_operators, true ) ) { continue; } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php index 78471014..6a665d0b 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php @@ -6,8 +6,8 @@ use WPGraphQL\Logging\Admin\View\List\ListTable; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; -use WPGraphQL\Logging\Logger\Database\LogsRepository; +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity; +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseLogService; use Codeception\TestCase\WPTestCase; use Mockery; @@ -21,13 +21,13 @@ class ListTableTest extends WPTestCase { private ListTable $list_table; - private LogsRepository $repository; + private WordPressDatabaseLogService $log_service; public function setUp(): void { parent::setUp(); - $this->repository = Mockery::mock(LogsRepository::class); - $this->list_table = new ListTable($this->repository); + $this->log_service = Mockery::mock(WordPressDatabaseLogService::class); + $this->list_table = new ListTable($this->log_service); } public function tearDown(): void { @@ -42,7 +42,7 @@ public function test_constructor_sets_default_args(): void { 'ajax' => true, ]; - $list_table = new ListTable($this->repository, $args); + $list_table = new ListTable($this->log_service, $args); $this->assertInstanceOf(ListTable::class, $list_table); } @@ -91,7 +91,7 @@ public function test_get_sortable_columns_returns_expected_columns(): void { } public function test_column_cb_returns_checkbox_for_valid_item(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_id')->andReturn(123); $result = $this->list_table->column_cb($entity); @@ -107,7 +107,7 @@ public function test_column_cb_returns_empty_string_for_invalid_item(): void { } public function test_column_id_returns_formatted_id_with_actions(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_id')->andReturn(456); $result = $this->list_table->column_id($entity); @@ -124,7 +124,7 @@ public function test_column_default_returns_null_for_invalid_item(): void { } public function test_column_default_returns_date_for_date_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_datetime')->andReturn('2023-01-01 12:00:00'); $result = $this->list_table->column_default($entity, 'date'); @@ -133,7 +133,7 @@ public function test_column_default_returns_date_for_date_column(): void { } public function test_column_default_returns_level_for_level_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_level')->andReturn(200); $result = $this->list_table->column_default($entity, 'level'); @@ -142,7 +142,7 @@ public function test_column_default_returns_level_for_level_column(): void { } public function test_get_query_returns_formatted_query(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_query')->andReturn('{ user { id name } }'); $result = $this->list_table->get_query($entity); @@ -152,7 +152,7 @@ public function test_get_query_returns_formatted_query(): void { } public function test_get_query_returns_empty_string_for_empty_query(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_query')->andReturn(''); $result = $this->list_table->get_query($entity); @@ -161,7 +161,7 @@ public function test_get_query_returns_empty_string_for_empty_query(): void { } public function test_get_event_returns_event_from_extra(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['wpgraphql_event' => 'query_executed']); $result = $this->list_table->get_event($entity); @@ -170,7 +170,7 @@ public function test_get_event_returns_event_from_extra(): void { } public function test_get_event_returns_message_when_no_event_in_extra(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn([]); $entity->shouldReceive('get_message')->andReturn('Default message'); @@ -180,7 +180,7 @@ public function test_get_event_returns_message_when_no_event_in_extra(): void { } public function test_get_process_id_returns_process_id_from_extra(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['process_id' => '12345']); $result = $this->list_table->get_process_id($entity); @@ -189,7 +189,7 @@ public function test_get_process_id_returns_process_id_from_extra(): void { } public function test_get_process_id_returns_zero_when_not_in_extra(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn([]); $result = $this->list_table->get_process_id($entity); @@ -198,7 +198,7 @@ public function test_get_process_id_returns_zero_when_not_in_extra(): void { } public function test_get_memory_usage_returns_memory_from_extra(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['memory_peak_usage' => '2MB']); $result = $this->list_table->get_memory_usage($entity); @@ -208,7 +208,7 @@ public function test_get_memory_usage_returns_memory_from_extra(): void { public function test_get_request_headers_returns_formatted_headers(): void { $headers = ['Content-Type' => 'application/json', 'Authorization' => 'Bearer token']; - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['request_headers' => $headers]); $result = $this->list_table->get_request_headers($entity); @@ -219,7 +219,7 @@ public function test_get_request_headers_returns_formatted_headers(): void { } public function test_get_request_headers_returns_empty_string_for_empty_headers(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn([]); $result = $this->list_table->get_request_headers($entity); @@ -268,7 +268,13 @@ public function test_process_where_handles_level_filter(): void { $request = ['level_filter' => 'ERROR']; $result = $method->invoke($this->list_table, $request); - $this->assertContains("level_name = 'ERROR'", $result); + $this->assertSame([ + [ + 'column' => 'level_name', + 'operator' => '=', + 'value' => 'ERROR', + ], + ], $result); } public function test_process_where_handles_date_filters(): void { @@ -277,19 +283,28 @@ public function test_process_where_handles_date_filters(): void { $method->setAccessible(true); $request = [ - 'start_date' => '2023-01-01', - 'end_date' => '2023-12-31' + 'start_date' => '2025-01-01', + 'end_date' => '2025-12-31' ]; $result = $method->invoke($this->list_table, $request); - $this->assertCount(2, $result); - $this->assertStringContainsString("datetime >= '2023-01-01", $result[0]); - $this->assertStringContainsString("datetime <= '2023-12-31", $result[1]); + $this->assertSame([ + [ + 'column' => 'datetime', + 'operator' => '>=', + 'value' => '2025-01-01 00:00:00', + ], + [ + 'column' => 'datetime', + 'operator' => '<=', + 'value' => '2025-12-31 00:00:00', + ], + ], $result); } public function test_prepare_items_sets_pagination_args(): void { - $this->repository->shouldReceive('get_log_count')->andReturn(50); - $this->repository->shouldReceive('get_logs')->andReturn([]); + $this->log_service->shouldReceive('count_entities_by_where')->andReturn(50); + $this->log_service->shouldReceive('find_entities_by_where')->andReturn([]); $_REQUEST = []; @@ -300,8 +315,8 @@ public function test_prepare_items_sets_pagination_args(): void { } public function test_prepare_items_handles_orderby_and_order_params(): void { - $this->repository->shouldReceive('get_log_count')->andReturn(10); - $this->repository->shouldReceive('get_logs')->andReturn([]); + $this->log_service->shouldReceive('count_entities_by_where')->andReturn(10); + $this->log_service->shouldReceive('find_entities_by_where')->andReturn([]); $_REQUEST = [ 'orderby' => 'date', @@ -315,7 +330,7 @@ public function test_prepare_items_handles_orderby_and_order_params(): void { } public function test_column_query_returns_query_from_extra(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['wpgraphql_query' => '{ user { id name } }']); $result = $this->list_table->column_query($entity); @@ -324,7 +339,7 @@ public function test_column_query_returns_query_from_extra(): void { } public function test_column_default_returns_channel_for_channel_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_channel')->andReturn('wpgraphql'); $result = $this->list_table->column_default($entity, 'channel'); @@ -333,7 +348,7 @@ public function test_column_default_returns_channel_for_channel_column(): void { } public function test_column_default_returns_level_name_for_level_name_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_level_name')->andReturn('ERROR'); $result = $this->list_table->column_default($entity, 'level_name'); @@ -342,7 +357,7 @@ public function test_column_default_returns_level_name_for_level_name_column(): } public function test_column_default_returns_message_for_message_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_message')->andReturn('Test log message'); $result = $this->list_table->column_default($entity, 'message'); @@ -351,7 +366,7 @@ public function test_column_default_returns_message_for_message_column(): void { } public function test_column_default_returns_event_for_event_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['wpgraphql_event' => 'query_executed']); $result = $this->list_table->column_default($entity, 'event'); @@ -360,7 +375,7 @@ public function test_column_default_returns_event_for_event_column(): void { } public function test_column_default_returns_process_id_for_process_id_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['process_id' => '98765']); $result = $this->list_table->column_default($entity, 'process_id'); @@ -369,7 +384,7 @@ public function test_column_default_returns_process_id_for_process_id_column(): } public function test_column_default_returns_memory_usage_for_memory_usage_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['memory_peak_usage' => '5MB']); $result = $this->list_table->column_default($entity, 'memory_usage'); @@ -378,7 +393,7 @@ public function test_column_default_returns_memory_usage_for_memory_usage_column } public function test_column_default_returns_query_for_wpgraphql_query_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_query')->andReturn('{ posts { id title } }'); $result = $this->list_table->column_default($entity, 'wpgraphql_query'); @@ -389,7 +404,7 @@ public function test_column_default_returns_query_for_wpgraphql_query_column(): public function test_column_default_returns_headers_for_request_headers_column(): void { $headers = ['User-Agent' => 'Test Agent', 'Accept' => 'application/json']; - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $entity->shouldReceive('get_extra')->andReturn(['request_headers' => $headers]); $result = $this->list_table->column_default($entity, 'request_headers'); @@ -400,7 +415,7 @@ public function test_column_default_returns_headers_for_request_headers_column() } public function test_column_default_returns_empty_string_for_unknown_column(): void { - $entity = Mockery::mock(DatabaseEntity::class); + $entity = Mockery::mock(WordPressDatabaseEntity::class); $result = $this->list_table->column_default($entity, 'unknown_column'); From 77fb41ee50095a8b02431612c53f45bccdf4fedf Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 14:45:18 +0000 Subject: [PATCH 26/59] Refactored WordPressDatabaseHandler to use Log Store Service Replaces direct usage of DatabaseEntity with LogStoreService in WordPressDatabaseHandler. This change centralizes log entity creation and storage, improving maintainability and consistency. --- .../src/Logger/Handlers/WordPressDatabaseHandler.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php b/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php index 23b9a3a1..13624780 100644 --- a/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php +++ b/plugins/wpgraphql-logging/src/Logger/Handlers/WordPressDatabaseHandler.php @@ -7,7 +7,7 @@ use Monolog\Handler\AbstractProcessingHandler; use Monolog\LogRecord; use Throwable; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; +use WPGraphQL\Logging\Logger\Store\LogStoreService; /** * WordPress Database Handler for Monolog @@ -27,7 +27,8 @@ class WordPressDatabaseHandler extends AbstractProcessingHandler { */ protected function write( LogRecord $record ): void { try { - $entity = DatabaseEntity::create( + $log_service = LogStoreService::get_log_service(); + $log_service->create_log_entity( // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid $record->channel, $record->level->value, $this->get_record_name( $record ), @@ -35,8 +36,6 @@ protected function write( LogRecord $record ): void { $record->context ?? [], $record->extra ?? [] ); - - $entity->save(); } catch ( Throwable $e ) { do_action( 'wpgraphql_logging_write_database_error', $e, $record ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { From 060dc8bd3f08cfdcfd6c046988dc6247616530b9 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 15:00:07 +0000 Subject: [PATCH 27/59] Updated API Interfaces and updated the logic for activate/de-activate the plugin to use interface methods instead so developers can replace the service via the log store service. --- .../src/Logger/Api/LogEntityInterface.php | 12 ++-- .../src/Logger/Api/LogServiceInterface.php | 10 ++++ .../Database/WordPressDatabaseEntity.php | 58 +++++++++---------- .../Database/WordPressDatabaseLogService.php | 21 +++++++ plugins/wpgraphql-logging/src/Plugin.php | 22 ++++--- 5 files changed, 81 insertions(+), 42 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php index 047e62b9..5d4b6242 100644 --- a/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php +++ b/plugins/wpgraphql-logging/src/Logger/Api/LogEntityInterface.php @@ -97,16 +97,16 @@ public function get_datetime(): string; public function get_query(): ?string; /** - * Gets the schema for the log entry. + * Saves the log entry to the database. * - * @return string The schema for the log entry. + * @return int The ID of the saved log entry, or 0 on failure. */ - public function get_schema(): string; + public function save(): int; /** - * Saves the log entry to the database. + * Gets the schema for the log entry. * - * @return int The ID of the saved log entry, or 0 on failure. + * @return string The schema for the log entry. */ - public function save(): int; + public static function get_schema(): string; } diff --git a/plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php b/plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php index ccdbd1f0..cd636a84 100644 --- a/plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php +++ b/plugins/wpgraphql-logging/src/Logger/Api/LogServiceInterface.php @@ -14,6 +14,16 @@ * @since 0.0.1 */ interface LogServiceInterface { + /** + * Activates the log service. + */ + public function activate(): void; + + /** + * Deactivates the log service. + */ + public function deactivate(): void; + /** * Creates a new log entity. * diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php index 2533bc50..a9f655ec 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseEntity.php @@ -177,35 +177,6 @@ public function get_datetime(): string { return $this->datetime; } - /** - * Gets the schema for the log entry. - * - * @return string The schema for the log entry. - */ - public function get_schema(): string { - global $wpdb; - $table_name = self::get_table_name(); - $charset_collate = $wpdb->get_charset_collate(); - - return " - CREATE TABLE {$table_name} ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - channel VARCHAR(191) NOT NULL, - level SMALLINT UNSIGNED NOT NULL, - level_name VARCHAR(50) NOT NULL, - message LONGTEXT NOT NULL, - context JSON NULL, - extra JSON NULL, - datetime DATETIME NOT NULL, - PRIMARY KEY (id), - INDEX channel_index (channel), - INDEX level_name_index (level_name), - INDEX level_index (level), - INDEX datetime_index (datetime) - ) {$charset_collate}; - "; - } - /** * Extracts and returns the GraphQL query from the context, if available. * @@ -275,6 +246,35 @@ public function save(): int { return 0; } + /** + * Gets the schema for the log entry. + * + * @return string The schema for the log entry. + */ + public static function get_schema(): string { + global $wpdb; + $table_name = self::get_table_name(); + $charset_collate = $wpdb->get_charset_collate(); + + return " + CREATE TABLE {$table_name} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + channel VARCHAR(191) NOT NULL, + level SMALLINT UNSIGNED NOT NULL, + level_name VARCHAR(50) NOT NULL, + message LONGTEXT NOT NULL, + context JSON NULL, + extra JSON NULL, + datetime DATETIME NOT NULL, + PRIMARY KEY (id), + INDEX channel_index (channel), + INDEX level_name_index (level_name), + INDEX level_index (level), + INDEX datetime_index (datetime) + ) {$charset_collate}; + "; + } + /** * Gets the name of the table for the log entry. * diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php index 33bcd838..dce6cf43 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php @@ -171,6 +171,27 @@ public function count_entities_by_where(array $args = []): int { return (int) $wpdb->get_var( $wpdb->prepare( $sql, $this->where_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } + /** + * Activates the log service. + */ + public function activate(): void { + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( WordPressDatabaseEntity::get_schema() ); + } + + /** + * Deactivates the log service. + */ + public function deactivate(): void { + if ( ! defined( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN' ) ) { + return; + } + + global $wpdb; + $table_name = $this->get_table_name(); + $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table_name ) ); + } + /** * Gets the table name. */ diff --git a/plugins/wpgraphql-logging/src/Plugin.php b/plugins/wpgraphql-logging/src/Plugin.php index 732d83ff..499afb01 100644 --- a/plugins/wpgraphql-logging/src/Plugin.php +++ b/plugins/wpgraphql-logging/src/Plugin.php @@ -9,8 +9,9 @@ use WPGraphQL\Logging\Admin\ViewLogsPage; use WPGraphQL\Logging\Events\EventManager; use WPGraphQL\Logging\Events\QueryEventLifecycle; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; use WPGraphQL\Logging\Logger\Scheduler\DataDeletionScheduler; +use WPGraphQL\Logging\Logger\Store\LogStoreService; /** * Plugin class for WPGraphQL Logging. @@ -98,11 +99,21 @@ public static function transform( string $event_name, callable $transform, int $ EventManager::subscribe_to_transform( $event_name, $transform, $priority ); } + /** + * Gets the log service instance. + * + * @return \WPGraphQL\Logging\Logger\Api\LogServiceInterface The log service instance. + */ + public static function get_log_service(): LogServiceInterface { + return LogStoreService::get_log_service(); + } + /** * Activation callback for the plugin. */ public static function activate(): void { - DatabaseEntity::create_table(); + $log_service = self::get_log_service(); + $log_service->activate(); } /** @@ -113,11 +124,8 @@ public static function activate(): void { public static function deactivate(): void { DataDeletionScheduler::clear_scheduled_deletion(); - - if ( ! defined( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN' ) ) { - return; - } - DatabaseEntity::drop_table(); + $log_service = self::get_log_service(); + $log_service->deactivate(); } /** From dd5ae29a9dbeb9b8cef359dabc4d97dcd87a91a1 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 15:28:00 +0000 Subject: [PATCH 28/59] Removed DatabaseEntity and LogsRepository from tests. --- .../tests/wpunit/Core/ActivationTest.php | 14 ++++--- .../tests/wpunit/Core/PluginTest.php | 1 - .../wpunit/Events/QueryActionLoggerTest.php | 38 +++++++++---------- .../wpunit/Events/QueryFilterLoggerTest.php | 21 +++++----- .../Handlers/WordPressDatabaseHandlerTest.php | 20 +++++++--- .../tests/wpunit/Logger/LoggerServiceTest.php | 1 - 6 files changed, 52 insertions(+), 43 deletions(-) diff --git a/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php b/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php index a241ce24..5d714a39 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php @@ -6,8 +6,7 @@ use lucatume\WPBrowser\TestCase\WPTestCase; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; - +use WPGraphQL\Logging\Plugin; /** * Test for the activation callback @@ -18,15 +17,20 @@ class ActivationTest extends WPTestCase { protected function setUp(): void { + // Sets the uninstall constant to true to ensure the log service is deactivated. + if ( ! defined( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN' ) ) { + define( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN', true ); + } parent::setUp(); if ( ! function_exists( 'wpgraphql_logging_activation_callback' ) ) { require_once dirname( __DIR__ ) . '/activation.php'; } - $this->drop_table(); + $this->deactivate(); } - public function drop_table(): void { - DatabaseEntity::drop_table(); + public function deactivate(): void { + $log_service = Plugin::get_log_service(); + $log_service->deactivate(); } public function test_activation_callback_function_exists(): void { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php b/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php index cdbbc264..7020bbc8 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php @@ -7,7 +7,6 @@ use WPGraphQL\Logging\Plugin; use lucatume\WPBrowser\TestCase\WPTestCase; use ReflectionClass; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use WPGraphQL\Logging\Events\EventManager; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php index 52f732f7..f4f35315 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php @@ -10,9 +10,7 @@ use WPGraphQL\Logging\Events\QueryActionLogger; use WPGraphQL\Logging\Events\Events; use GraphQL\Executor\ExecutionResult; -use WPGraphQL\Logging\Logger\Database\LogsRepository; use WPGraphQL\Logging\Logger\LoggerService; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use Monolog\Level; use WPGraphQL; @@ -20,6 +18,9 @@ use GraphQL\Type\SchemaConfig; use WPGraphQL\WPSchema; use WPGraphQL\Request; +use WPGraphQL\Logging\Logger\Store\LogStoreService; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity; /** * Tests for the QueryActionLogger class. @@ -30,19 +31,19 @@ */ class QueryActionLoggerTest extends WPTestCase { - protected LogsRepository $repository; - protected LoggerService $logger; + protected LogServiceInterface $log_service; + public function setUp(): void { parent::setUp(); - $this->repository = new LogsRepository(); + $this->log_service = LogStoreService::get_log_service(); $this->logger = LoggerService::get_instance(); } public function tearDown(): void { parent::tearDown(); - $this->repository->delete_all(); + $this->log_service->delete_all_entities(); } public function create_instance(array $config) : QueryActionLogger { @@ -50,7 +51,7 @@ public function create_instance(array $config) : QueryActionLogger { } public function get_log_count(): int { - return $this->repository->get_log_count([]); + return $this->log_service->count_entities_by_where([]); } public function assert_log_count(int $expected_count): void { @@ -148,12 +149,9 @@ public function test_pre_request_add_context(): void { $this->assert_log_count(1); // Check that the additional context is present in the log - $logs = $this->repository->get_logs([]); + $logs = $this->log_service->find_entities_by_where([]); $this->assertCount(1, $logs); $log = $logs[0]; - $this->assertInstanceOf(DatabaseEntity::class, $log); - - $this->assertEquals(Level::Debug->value, $log->get_level()); $this->assertArrayHasKey('custom_key', $log->get_context()); $this->assertEquals('custom_value', $log->get_context()['custom_key']); @@ -293,10 +291,10 @@ public function test_graphql_before_execute_add_context(): void { $this->assert_log_count(1); // Check that the additional context is present in the log - $logs = $this->repository->get_logs([]); + $logs = $this->log_service->find_entities_by_where([]); $this->assertCount(1, $logs); $log = $logs[0]; - $this->assertInstanceOf(DatabaseEntity::class, $log); + $this->assertInstanceOf(WordPressDatabaseEntity::class, $log); $this->assertEquals(Level::Error->value, $log->get_level()); @@ -382,10 +380,10 @@ public function test_log_before_response_returned_log_data_with_errors(): void { // Check for error level and context - $logs = $this->repository->get_logs([]); + $logs = $this->log_service->find_entities_by_where([]); $this->assertCount(1, $logs); $log = $logs[0]; - $this->assertInstanceOf(DatabaseEntity::class, $log); + $this->assertInstanceOf(WordPressDatabaseEntity::class, $log); $this->assertEquals(Level::Error->value, $log->get_level()); @@ -421,10 +419,10 @@ public function test_log_before_response_returned_log_data_with_errors_array(): // Check for error level and context - $logs = $this->repository->get_logs([]); + $logs = $this->log_service->find_entities_by_where([]); $this->assertCount(1, $logs); $log = $logs[0]; - $this->assertInstanceOf(DatabaseEntity::class, $log); + $this->assertInstanceOf(WordPressDatabaseEntity::class, $log); $this->assertEquals(Level::Error->value, $log->get_level()); @@ -461,16 +459,14 @@ public function test_log_before_response_returned_log_data_with_empty_errors_arr // Check for error level and context - $logs = $this->repository->get_logs([]); + $logs = $this->log_service->find_entities_by_where([]); $this->assertCount(1, $logs); $log = $logs[0]; - $this->assertInstanceOf(DatabaseEntity::class, $log); + $this->assertInstanceOf(WordPressDatabaseEntity::class, $log); $this->assertNotEquals(Level::Error->value, $log->get_level()); $this->assertArrayNotHasKey('errors', $log->get_context()); } - - } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php index 5b31a28a..9b4ad643 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php @@ -11,9 +11,7 @@ use WPGraphQL\Logging\Events\QueryFilterLogger; use WPGraphQL\Logging\Events\Events; use GraphQL\Executor\ExecutionResult; -use WPGraphQL\Logging\Logger\Database\LogsRepository; use WPGraphQL\Logging\Logger\LoggerService; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use Monolog\Level; use WPGraphQL; @@ -21,6 +19,9 @@ use GraphQL\Type\SchemaConfig; use WPGraphQL\WPSchema; use WPGraphQL\Request; +use WPGraphQL\Logging\Logger\Store\LogStoreService; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity; /** * Tests for the QueryFilterLogger class. @@ -32,19 +33,19 @@ class QueryFilterLoggerTest extends WPTestCase { - protected LogsRepository $repository; + protected LogServiceInterface $log_service; protected LoggerService $logger; public function setUp(): void { parent::setUp(); - $this->repository = new LogsRepository(); + $this->log_service = LogStoreService::get_log_service(); $this->logger = LoggerService::get_instance(); } public function tearDown(): void { parent::tearDown(); - $this->repository->delete_all(); + $this->log_service->delete_all_entities(); } public function create_instance(array $config) : QueryFilterLogger { @@ -52,7 +53,7 @@ public function create_instance(array $config) : QueryFilterLogger { } public function get_log_count(): int { - return $this->repository->get_log_count([]); + return $this->log_service->count_entities_by_where([]); } public function assert_log_count(int $expected_count): void { @@ -128,7 +129,7 @@ public function test_graphql_request_data_log_event_with_context_data_from_subsc $this->assert_log_count(1); // Check for the new meta_data field in the log entry. - $logs = $this->repository->get_logs([], 10, 0); + $logs = $this->log_service->find_entities_by_where([]); $this->assertNotEmpty($logs); $log_entry = $logs[0]; $this->assertArrayHasKey('meta_data', $log_entry->get_context()); @@ -217,10 +218,10 @@ public function test_graphql_request_results_log_data_with_errors_array(): void // Check for error level and context - $logs = $this->repository->get_logs([]); + $logs = $this->log_service->find_entities_by_where([]); $this->assertCount(1, $logs); $log = $logs[0]; - $this->assertInstanceOf(DatabaseEntity::class, $log); + $this->assertInstanceOf(WordPressDatabaseEntity::class, $log); $this->assertEquals(Level::Error->value, $log->get_level()); $this->assertArrayHasKey('errors', $log->get_context()); @@ -263,7 +264,7 @@ public function test_graphql_request_results_log_event_add_context_with_subscrib // Check for the new meta_data field in the log entry. - $logs = $this->repository->get_logs([], 10, 0); + $logs = $this->log_service->find_entities_by_where([]); $this->assertNotEmpty($logs); $log_entry = $logs[0]; $this->assertArrayHasKey('meta_data', $log_entry->get_context()); diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Handlers/WordPressDatabaseHandlerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Handlers/WordPressDatabaseHandlerTest.php index 568681c3..55e9d88d 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Handlers/WordPressDatabaseHandlerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Handlers/WordPressDatabaseHandlerTest.php @@ -8,8 +8,9 @@ use Monolog\Level; use Monolog\LogRecord; use DateTimeImmutable; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity; use WPGraphQL\Logging\Logger\Handlers\WordPressDatabaseHandler; +use WPGraphQL\Logging\Logger\Store\LogStoreService; /** * Class WordPressDatabaseHandlerTest @@ -31,7 +32,8 @@ class WordPressDatabaseHandlerTest extends WPTestCase public function setUp(): void { parent::setUp(); - DatabaseEntity::create_table(); + $log_service = LogStoreService::get_log_service(); + $log_service->activate(); // Setup test record data. $this->log_data = [ @@ -75,7 +77,11 @@ public function setUp(): void public function tearDown(): void { - DatabaseEntity::drop_table(); + $log_service = LogStoreService::get_log_service(); + if ( ! defined( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN' ) ) { + define( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN', true ); + } + $log_service->deactivate(); parent::tearDown(); } @@ -89,7 +95,7 @@ public function test_write_method_saves_log_to_database(): void $log_data = $this->log_data; // 4. Verify the data was saved correctly in the database. - $table_name = DatabaseEntity::get_table_name(); + $table_name = WordPressDatabaseEntity::get_table_name(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $saved_row = $wpdb->get_row("SELECT * FROM {$table_name} ORDER BY id DESC LIMIT 1", ARRAY_A); @@ -107,7 +113,11 @@ public function test_write_method_saves_log_to_database(): void public function test_write_method_handles_exceptions_gracefully(): void { // Drop the table to force the save operation to fail. - DatabaseEntity::drop_table(); + $log_service = LogStoreService::get_log_service(); + if ( ! defined( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN' ) ) { + define( 'WP_GRAPHQL_LOGGING_UNINSTALL_PLUGIN', true ); + } + $log_service->deactivate(); global $wpdb; $wpdb->flush(); diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php index a4079113..355d7d00 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php @@ -9,7 +9,6 @@ use Monolog\Processor\ProcessorInterface; use ReflectionClass; use WPGraphQL\Logging\Logger\LoggerService; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use Monolog\LogRecord; From c58952b7c3f503694c3a0e63daa943b9697970c0 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 17:57:55 +0000 Subject: [PATCH 29/59] Refactor database logging to use WordPressDatabaseEntity Removed DatabaseEntity and LogsRepository classes and their tests. Updated tests to use the new WordPressDatabaseEntity class, including renaming and refactoring the test file. Updated documentation to reflect the new API structure. This change consolidates database logging logic under WordPressDatabaseEntity for improved maintainability. --- plugins/wpgraphql-logging/composer.lock | 109 +++-- plugins/wpgraphql-logging/docs/index.md | 1 + .../src/Logger/Database/DatabaseEntity.php | 422 ------------------ .../src/Logger/Database/LogsRepository.php | 138 ------ .../Logger/Database/LogsRepositoryTest.php | 183 -------- ...st.php => WordPressDatabaseEntityTest.php} | 156 ++++--- .../WordPressDatabaseLogServiceTest.php | 187 ++++++++ .../Scheduler/DataDeletionSchedulerTest.php | 54 +-- 8 files changed, 342 insertions(+), 908 deletions(-) delete mode 100644 plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php delete mode 100644 plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php delete mode 100644 plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php rename plugins/wpgraphql-logging/tests/wpunit/Logger/Database/{DatabaseEntityTest.php => WordPressDatabaseEntityTest.php} (58%) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Logger/Database/WordPressDatabaseLogServiceTest.php diff --git a/plugins/wpgraphql-logging/composer.lock b/plugins/wpgraphql-logging/composer.lock index 41f74ea1..d5942d8d 100644 --- a/plugins/wpgraphql-logging/composer.lock +++ b/plugins/wpgraphql-logging/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "league/csv", - "version": "9.27.0", + "version": "9.27.1", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "cb491b1ba3c42ff2bcd0113814f4256b42bae845" + "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/cb491b1ba3c42ff2bcd0113814f4256b42bae845", - "reference": "cb491b1ba3c42ff2bcd0113814f4256b42bae845", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/26de738b8fccf785397d05ee2fc07b6cd8749797", + "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797", "shasum": "" }, "require": { @@ -95,7 +95,7 @@ "type": "github" } ], - "time": "2025-10-16T08:22:09+00:00" + "time": "2025-10-25T08:35:20+00:00" }, { "name": "monolog/monolog", @@ -707,21 +707,21 @@ }, { "name": "codeception/lib-innerbrowser", - "version": "4.0.6", + "version": "4.0.7", "source": { "type": "git", "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "74476dd019ec7900b26b7dca91a42fdcb04e549f" + "reference": "cf2ddaae5e07eb3cceb504d93bd95f4a2d892dab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/74476dd019ec7900b26b7dca91a42fdcb04e549f", - "reference": "74476dd019ec7900b26b7dca91a42fdcb04e549f", + "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/cf2ddaae5e07eb3cceb504d93bd95f4a2d892dab", + "reference": "cf2ddaae5e07eb3cceb504d93bd95f4a2d892dab", "shasum": "" }, "require": { "codeception/codeception": "^5.0.8", - "codeception/lib-web": "^1.0.1", + "codeception/lib-web": "^1.0.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-mbstring": "*", @@ -760,9 +760,9 @@ ], "support": { "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/4.0.6" + "source": "https://github.com/Codeception/lib-innerbrowser/tree/4.0.7" }, - "time": "2025-02-14T07:02:48+00:00" + "time": "2025-10-23T05:52:19+00:00" }, { "name": "codeception/lib-web", @@ -4011,16 +4011,16 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.8.2", + "version": "v6.8.3", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8" + "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", - "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/abeb5a8b58fda7ac21f15ee596f302f2959a7114", + "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114", "shasum": "" }, "conflict": { @@ -4056,9 +4056,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.2" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.3" }, - "time": "2025-07-16T06:41:00+00:00" + "time": "2025-09-30T20:58:47+00:00" }, { "name": "php-stubs/wp-cli-stubs", @@ -4176,12 +4176,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "de29923c98ce1d7d35df862c51d5dc0062c95401" + "reference": "71a1e55c148ec0d248f599faea63044f81a1952c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/de29923c98ce1d7d35df862c51d5dc0062c95401", - "reference": "de29923c98ce1d7d35df862c51d5dc0062c95401", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/71a1e55c148ec0d248f599faea63044f81a1952c", + "reference": "71a1e55c148ec0d248f599faea63044f81a1952c", "shasum": "" }, "require": { @@ -4262,7 +4262,7 @@ "type": "thanks_dev" } ], - "time": "2025-10-20T21:11:54+00:00" + "time": "2025-10-29T00:11:25+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", @@ -4417,16 +4417,16 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb" + "reference": "8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/882b8c947ada27eb002870fe77fee9ce0a454cdb", - "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e", + "reference": "8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e", "shasum": "" }, "require": { @@ -4495,7 +4495,7 @@ "type": "thanks_dev" } ], - "time": "2025-09-05T06:54:52+00:00" + "time": "2025-10-28T17:00:02+00:00" }, { "name": "phpcsstandards/phpcsutils", @@ -5683,16 +5683,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.13", + "version": "v0.12.14", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e" + "reference": "95c29b3756a23855a30566b745d218bee690bef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e", - "reference": "d86c2f750e72017a5cdb1b9f1cef468a5cbacd1e", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", + "reference": "95c29b3756a23855a30566b745d218bee690bef2", "shasum": "" }, "require": { @@ -5713,7 +5713,6 @@ "suggest": { "composer/class-map-generator": "Improved tab completion performance with better class discovery.", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ @@ -5757,9 +5756,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.13" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" }, - "time": "2025-10-20T22:48:29+00:00" + "time": "2025-10-27T17:15:31+00:00" }, { "name": "ralouphie/getallheaders", @@ -7420,16 +7419,16 @@ }, { "name": "symfony/console", - "version": "v6.4.26", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f" + "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f", - "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f", + "url": "https://api.github.com/repos/symfony/console/zipball/13d3176cf8ad8ced24202844e9f95af11e2959fc", + "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc", "shasum": "" }, "require": { @@ -7494,7 +7493,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.26" + "source": "https://github.com/symfony/console/tree/v6.4.27" }, "funding": [ { @@ -7514,7 +7513,7 @@ "type": "tidelift" } ], - "time": "2025-09-26T12:13:46+00:00" + "time": "2025-10-06T10:25:16+00:00" }, { "name": "symfony/css-selector", @@ -7955,16 +7954,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.24", + "version": "v6.4.27", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "73089124388c8510efb8d2d1689285d285937b08" + "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", - "reference": "73089124388c8510efb8d2d1689285d285937b08", + "url": "https://api.github.com/repos/symfony/finder/zipball/a1b6aa435d2fba50793b994a839c32b6064f063b", + "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b", "shasum": "" }, "require": { @@ -7999,7 +7998,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.24" + "source": "https://github.com/symfony/finder/tree/v6.4.27" }, "funding": [ { @@ -8019,7 +8018,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T12:02:45+00:00" + "time": "2025-10-15T18:32:00+00:00" }, { "name": "symfony/polyfill-ctype", @@ -11313,12 +11312,12 @@ "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "04f9932abfa2ed71dd16e88964f6ac6cd64c37dd" + "reference": "901157f28a507e6dd1f2b26de80b27eea57339de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/04f9932abfa2ed71dd16e88964f6ac6cd64c37dd", - "reference": "04f9932abfa2ed71dd16e88964f6ac6cd64c37dd", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/901157f28a507e6dd1f2b26de80b27eea57339de", + "reference": "901157f28a507e6dd1f2b26de80b27eea57339de", "shasum": "" }, "require": { @@ -11332,11 +11331,11 @@ "require-dev": { "justinrainbow/json-schema": "^6.3", "roave/security-advisories": "dev-latest", - "wp-cli/db-command": "^1.3 || ^2", - "wp-cli/entity-command": "^1.2 || ^2", - "wp-cli/extension-command": "^1.1 || ^2", - "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/db-command": "^2", + "wp-cli/entity-command": "^2", + "wp-cli/extension-command": "^2", + "wp-cli/package-command": "^2", + "wp-cli/wp-cli-tests": "^5" }, "suggest": { "ext-readline": "Include for a better --prompt implementation", @@ -11377,7 +11376,7 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2025-10-01T15:55:36+00:00" + "time": "2025-10-23T14:44:57+00:00" }, { "name": "wp-cli/wp-cli-bundle", diff --git a/plugins/wpgraphql-logging/docs/index.md b/plugins/wpgraphql-logging/docs/index.md index 9ac08e22..8f45da35 100644 --- a/plugins/wpgraphql-logging/docs/index.md +++ b/plugins/wpgraphql-logging/docs/index.md @@ -24,6 +24,7 @@ wpgraphql-logging/ │ ├── Settings/ # Admin settings functionality for displaying and saving data. │ ├── Events/ # Event logging, pub/sub event manager for extending the logging. │ ├── Logger/ # Logger service, Monolog handlers & processors +│ ├── Api/ # Api interfaces for fetching and writing log data │ ├── Database/ # Database entity and helper │ ├── Handlers/ # Monolog WordPress database handler for logging data │ ├── Processors/ # Monolog processors for data sanitization and request headers diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php deleted file mode 100644 index 8c9b1728..00000000 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ /dev/null @@ -1,422 +0,0 @@ - - */ - protected array $context = []; - - /** - * Extra data for the log entry. - * - * @var array - */ - protected array $extra = []; - - /** - * The datetime of the log entry. - * - * @var string - */ - protected string $datetime = ''; - - /** - * The constructor is protected to encourage creation via static methods. - */ - protected function __construct() { - // Set a default datetime for new, unsaved entries. - $this->datetime = current_time( 'mysql', 1 ); - } - - /** - * Creates a new, unsaved log entry instance. - * - * @param string $channel The channel for the log entry. - * @param int $level The logging level. - * @param string $level_name The name of the logging level. - * @param string $message The log message. - * @param array $context Additional context for the log entry. - * @param array $extra Extra data for the log entry. - */ - public static function create(string $channel, int $level, string $level_name, string $message, array $context = [], array $extra = []): self { - $entity = new self(); - $entity->channel = self::sanitize_text_field( $channel ); - $entity->level = $level; - $entity->level_name = self::sanitize_text_field( $level_name ); - $entity->message = self::sanitize_text_field( $message ); - $entity->context = self::sanitize_array_field( $context ); - $entity->extra = self::sanitize_array_field( $extra ); - - return $entity; - } - - /** - * Finds a single log entry by its ID and returns it as an object. - * - * @param int $id The ID of the log entry to find. - * - * @return self|null Returns an instance of DatabaseEntity if found, or null if not found. - */ - public static function find_by_id(int $id): ?self { - global $wpdb; - $table_name = self::get_table_name(); - - $query = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $row = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared - - if ( ! $row ) { - return null; - } - - return self::create_from_db_row( $row ); - } - - /** - * Helper to populate an instance from a database row. - * - * @param array $row The database row to populate from. - * - * @return self The populated instance. - */ - public static function create_from_db_row(array $row): self { - $log = new self(); - $log->id = (int) $row['id']; - $log->channel = $row['channel']; - $log->level = (int) $row['level']; - $log->level_name = $row['level_name']; - $log->message = $row['message']; - $log->context = ( isset( $row['context'] ) && '' !== $row['context'] ) ? json_decode( $row['context'], true ) : []; - $log->extra = ( isset( $row['extra'] ) && '' !== $row['extra'] ) ? json_decode( $row['extra'], true ) : []; - $log->datetime = $row['datetime']; - return $log; - } - - /** - * Saves a new logging entity to the database. This is an insert-only operation. - * - * @return int The ID of the newly created log entry, or 0 on failure. - */ - public function save(): int { - global $wpdb; - $table_name = self::get_table_name(); - - $data = [ - 'channel' => $this->channel, - 'level' => $this->level, - 'level_name' => $this->level_name, - 'message' => $this->message, - 'context' => wp_json_encode( $this->context ), - 'extra' => wp_json_encode( $this->extra ), - 'datetime' => $this->datetime, - ]; - - $formats = [ '%s', '%d', '%s', '%s', '%s', '%s', '%s' ]; - - $result = $wpdb->insert( $table_name, $data, $formats ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - - if ( $result ) { - $this->id = (int) $wpdb->insert_id; - return $this->id; - } - - return 0; - } - - /** - * Gets the ID of the log entry. - */ - public function get_id(): int { - return (int) $this->id; - } - - /** - * Gets the channel of the log entry. - */ - public function get_channel(): string { - return $this->channel; - } - - /** - * Gets the logging level of the log entry. - */ - public function get_level(): int { - return $this->level; - } - - /** - * Gets the name of the logging level of the log entry. - */ - public function get_level_name(): string { - return $this->level_name; - } - - /** - * Gets the message of the log entry. - * - * @return string The message of the log entry. - */ - public function get_message(): string { - return $this->message; - } - - /** - * Gets the context of the log entry. - * - * @return array The context of the log entry. - */ - public function get_context(): array { - return $this->context; - } - - /** - * Gets the extra data of the log entry. - * - * @return array The extra data of the log entry. - */ - public function get_extra(): array { - return $this->extra; - } - - /** - * Gets the datetime of the log entry. - * - * @return string The datetime of the log entry in MySQL format. - */ - public function get_datetime(): string { - return $this->datetime; - } - - /** - * Extracts and returns the GraphQL query from the context, if available. - * - * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh, Generic.Metrics.CyclomaticComplexity.TooHigh - * - * @return string|null The GraphQL query string, or null if not available. - */ - public function get_query(): ?string { - - $context = $this->get_context(); - if ( empty( $context ) ) { - return null; - } - - $query = $context['query'] ?? null; - if ( is_string( $query ) ) { - return $query; - } - - $request = $context['request'] ?? null; - if ( empty( $request ) || ! is_array( $request ) ) { - return $query; - } - - $params = $request['params'] ?? null; - if ( empty( $params ) || ! is_array( $params ) ) { - return $query; - } - - if ( isset( $params['query'] ) && is_string( $params['query'] ) ) { - return $params['query']; - } - - return $query; - } - - /** - * Finds multiple log entries and returns them as an array. - * - * @param int $limit The maximum number of log entries to return. - * @param int $offset The offset for pagination. - * @param array $where_clauses Optional. Additional WHERE conditions. - * @param string $orderby The column to order by. Must be one of the allowed columns. - * @param string $order The order direction (ASC or DESC). - * - * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> An array of DatabaseEntity instances, or an empty array if none found. - */ - public static function find_logs(int $limit, int $offset, array $where_clauses = [], string $orderby = 'id', string $order = 'DESC'): array { - global $wpdb; - $table_name = self::get_table_name(); - - // Whitelist validation for ORDER BY column. - $allowed_orderby_columns = [ 'id', 'datetime', 'level', 'level_name', 'channel', 'message' ]; - $allowed_orderby_columns = apply_filters( 'wpgraphql_logging_allowed_orderby_columns', $allowed_orderby_columns ); - - // Fallback to default if the orderby column is not allowed. - if ( ! in_array( $orderby, $allowed_orderby_columns, true ) ) { - $orderby = 'id'; - } - - // Whitelist validation for ORDER direction. - $order = strtoupper( $order ); - if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) { - $order = 'DESC'; // Fallback to default. - } - - $where = ''; - foreach ( $where_clauses as $clause ) { - if ( '' !== $where ) { - $where .= ' AND '; - } - $where .= (string) $clause; - } - if ( '' !== $where ) { - $where = 'WHERE ' . $where; - } - - /** @psalm-suppress PossiblyInvalidCast */ - $query = $wpdb->prepare( - "SELECT * FROM {$table_name} {$where} ORDER BY {$orderby} {$order} LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $offset, - $limit - ); - - // We do not want to cache as this is a paginated query. - $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared - - if ( empty( $results ) || ! is_array( $results ) ) { - return []; - } - - return array_map( - static function (array $row) { - return DatabaseEntity::create_from_db_row( $row ); - }, - $results - ); - } - - /** - * Gets the name of the logging table. - */ - public static function get_table_name(): string { - global $wpdb; - $name = apply_filters( 'wpgraphql_logging_database_name', $wpdb->prefix . 'wpgraphql_logging' ); - return self::sanitize_text_field( $name ); - } - - /** - * Gets the database schema for the logging table. - */ - public static function get_schema(): string { - global $wpdb; - $table_name = self::get_table_name(); - $charset_collate = $wpdb->get_charset_collate(); - - // **IMPORTANT**: This schema format with PRIMARY KEY on its own line is the - // correct and stable way to work with dbDelta. - return " - CREATE TABLE {$table_name} ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - channel VARCHAR(191) NOT NULL, - level SMALLINT UNSIGNED NOT NULL, - level_name VARCHAR(50) NOT NULL, - message LONGTEXT NOT NULL, - context JSON NULL, - extra JSON NULL, - datetime DATETIME NOT NULL, - PRIMARY KEY (id), - INDEX channel_index (channel), - INDEX level_name_index (level_name), - INDEX level_index (level), - INDEX datetime_index (datetime) - ) {$charset_collate}; - "; - } - - /** - * Creates the logging table in the database. - */ - public static function create_table(): void { - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - dbDelta( self::get_schema() ); - } - - /** - * Drops the logging table from the database. - */ - public static function drop_table(): void { - global $wpdb; - $table_name = self::get_table_name(); - $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange - } - - /** - * Sanitizes a text field. - * - * @param string $value The value to sanitize. - */ - protected static function sanitize_text_field(string $value): string { - return sanitize_text_field( $value ); - } - - /** - * Sanitizes an array field recursively. - * - * @param array $data The array to sanitize. - * - * @return array The sanitized array. - */ - protected static function sanitize_array_field(array $data): array { - foreach ( $data as &$value ) { - if ( is_string( $value ) ) { - $value = self::sanitize_text_field( $value ); - continue; - } - - if ( is_array( $value ) ) { - $value = self::sanitize_array_field( $value ); - } - } - return $data; - } -} diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php deleted file mode 100644 index 66db4731..00000000 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ /dev/null @@ -1,138 +0,0 @@ - $args - * - * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> - */ - public function get_logs(array $args = []): array { - $defaults = [ - 'number' => 100, - 'offset' => 0, - 'orderby' => 'id', - 'order' => 'DESC', - 'where' => [], - ]; - $args = wp_parse_args( $args, $defaults ); - - $orderby = $args['orderby']; - if ( ! is_string( $orderby ) || '' === $orderby ) { - $orderby = $defaults['orderby']; - } - $order = $args['order']; - if ( ! is_string( $order ) || '' === $order ) { - $order = $defaults['order']; - } - $where = $args['where']; - if ( ! is_array( $where ) ) { - $where = $defaults['where']; - } - - $limit = absint( $args['number'] ); - $offset = absint( $args['offset'] ); - - - return DatabaseEntity::find_logs( $limit, $offset, $where, $orderby, $order ); - } - - /** - * Get the total number of log entries. - * - * @param array $where_clauses Array of where clauses to filter the count. - * - * @phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching - * - * @return int The total number of log entries. - */ - public function get_log_count(array $where_clauses): int { - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - - if ( empty( $where_clauses ) ) { - return (int) $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - 'SELECT COUNT(*) FROM %i', - $table_name - ) ); - } - - $where = ''; - foreach ( $where_clauses as $clause ) { - if ( '' !== $where ) { - $where .= ' AND '; - } - $where .= (string) $clause; - } - - return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name} WHERE {$where}" ); // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } - - /** - * Get a single log entry by ID. - * - * @param int $id The log entry ID. - * - * @return ?\WPGraphQL\Logging\Logger\Database\DatabaseEntity The log entry or null if not found. - */ - public function get_log( int $id ): ?DatabaseEntity { - return DatabaseEntity::find_by_id( $id ); - } - - /** - * Delete a single log entry by ID. - * - * @param int $id - */ - public function delete(int $id): bool { - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - - if ( $id <= 0 ) { - return false; - } - - $result = $wpdb->delete( $table_name, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - return false !== $result; - } - - /** - * Delete a logs entry older than a specific date. - * - * @param \DateTime $date The date to delete logs older than. - */ - public function delete_log_older_than(DateTime $date): bool { - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - - $result = $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - 'DELETE FROM %i WHERE datetime < %s', - $table_name, - $date->format( 'Y-m-d H:i:s' ) - ) ); - return false !== $result; - } - - /** - * Delete all log entries. - */ - public function delete_all(): void { - global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery - } -} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php deleted file mode 100644 index bd1fa9c1..00000000 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php +++ /dev/null @@ -1,183 +0,0 @@ -logs_repository = new LogsRepository(); - - // Create the database table for testing - DatabaseEntity::create_table(); - } - - protected function tearDown(): void - { - // Clean up the database table - $this->logs_repository->delete_all(); - parent::tearDown(); - } - - public function insert_mock_data(): void - { - $log_data = [ - 'channel' => 'wpgraphql_logging', - 'level' => 200, - 'level_name' => 'INFO', - 'message' => 'WPGraphQL Outgoing Response', - 'context' => [ - 'site_url' => 'http://test.local', - 'wp_version' => '6.8.2', - 'wp_debug_mode' => true, - 'plugin_version'=> '0.0.1' - ], - 'extra' => [ - 'ip' => '127.0.0.1', - 'url' => '/index.php?graphql', - 'server' => 'test.local', - 'referrer' => 'http://test.local/wp-admin/admin.php?page=graphiql-ide', - 'process_id' => 5819, - 'http_method' => 'POST', - 'memory_usage' => '14 MB', - 'wpgraphql_query' => 'query GetPost($uri: ID!) { post(id: $uri, idType: URI) { title content } }', - 'memory_peak_usage' => '14 MB', - 'wpgraphql_variables' => [ - 'uri' => 'hello-world' - ], - 'wpgraphql_operation_name' => 'GetPost' - ] - ]; - - $log_entry = DatabaseEntity::create(...$log_data); - $log_entry->save(); - $log_data['level'] = 400; - $log_data['level_name'] = 'ERROR'; - $log_data['message'] = 'WPGraphQL Error'; - sleep(1); // Ensure different timestamps - $log_entry = DatabaseEntity::create(...$log_data); - $log_entry->save(); - } - - public function test_get_logs_with_default_args(): void - { - $this->insert_mock_data(); - $logs = $this->logs_repository->get_logs(); - $this->assertIsArray($logs); - $this->assertCount(2, $logs); - $this->assertInstanceOf(DatabaseEntity::class, $logs[0]); - $this->assertInstanceOf(DatabaseEntity::class, $logs[1]); - } - - public function test_get_logs_with_custom_args(): void - { - $this->insert_mock_data(); - $args = [ - 'number' => 50, - 'offset' => 0, - 'orderby' => 'datetime', - 'order' => 'DESC' - ]; - $logs = $this->logs_repository->get_logs($args); - $this->assertIsArray($logs); - $this->assertCount(2, $logs); - - // Should get the last inserted log first - $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); - - /** - * Test default orderby - */ - $args['orderby'] = ''; - $logs = $this->logs_repository->get_logs($args); - $this->assertIsArray($logs); - - // Should be last as default is DESC - $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); - - /** - * Test where is string should not work - */ - $args['where'] = 'level = 200'; - $logs = $this->logs_repository->get_logs($args); - $this->assertIsArray($logs); - - /** - * Test invalid order - */ - $args['order'] = ''; - $logs = $this->logs_repository->get_logs($args); - $this->assertIsArray($logs); - $this->assertCount(2, $logs); - - // Should be last one as where clause is ignored - $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); - - // Check log count - $this->assertEquals(2, $this->logs_repository->get_log_count([])); - $this->assertEquals(1, $this->logs_repository->get_log_count(["level = 400", "channel = 'wpgraphql_logging'"])); - } - - public function test_delete_logs(): void - { - $this->insert_mock_data(); - $logs = $this->logs_repository->get_logs(); - $this->assertCount(2, $logs); - - // Delete one log - $result = $this->logs_repository->delete($logs[0]->get_id()); - $this->assertTrue($result); - - // Delete invalid logs - $result = $this->logs_repository->delete(0); - $this->assertFalse($result); - - // Check remaining logs - $logs = $this->logs_repository->get_logs(); - $this->assertCount(1, $logs); - - // Delete all logs - $this->logs_repository->delete_all(); - $logs = $this->logs_repository->get_logs(); - $this->assertCount(0, $logs); - } - - public function test_delete_log_older_than(): void - { - $this->insert_mock_data(); - $logs = $this->logs_repository->get_logs(); - $this->assertCount(2, $logs); - $date = $logs[0]->get_datetime(); - $dateTime = new \DateTime($date); - - $result = $this->logs_repository->delete_log_older_than($dateTime); - $this->assertTrue($result); - $logs = $this->logs_repository->get_logs(); - $this->assertCount(1, $logs); - - // Delete last log - $dateTime->modify('+1 second'); - $result = $this->logs_repository->delete_log_older_than($dateTime); - $this->assertTrue($result); - $logs = $this->logs_repository->get_logs(); - $this->assertCount(0, $logs); - } -} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/WordPressDatabaseEntityTest.php similarity index 58% rename from plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Database/WordPressDatabaseEntityTest.php index c87023f2..f505b93e 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/WordPressDatabaseEntityTest.php @@ -5,18 +5,17 @@ namespace WPGraphQL\Logging\Tests\Logger\Database; use lucatume\WPBrowser\TestCase\WPTestCase; -use DateTimeImmutable; use ReflectionClass; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity; use Mockery; /** - * Test for the DatabaseEntity + * Test for the WordPressDatabaseEntity * * @package WPGraphQL\Logging * @since 0.0.1 */ -class DatabaseEntityTest extends WPTestCase +class WordPressDatabaseEntityTest extends WPTestCase { public function setUp(): void @@ -30,16 +29,44 @@ public function tearDown(): void { $this->drop_table(); parent::tearDown(); + Mockery::close(); } private function drop_table(): void { - DatabaseEntity::drop_table(); + global $wpdb; + $table_name = WordPressDatabaseEntity::get_table_name(); + $wpdb->query("DROP TABLE IF EXISTS {$table_name}"); } private function create_table(): void { - DatabaseEntity::create_table(); + global $wpdb; + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + $schema = WordPressDatabaseEntity::get_schema(); + dbDelta($schema); + + if ($wpdb->last_error) { + $this->fail("dbDelta failed: " . $wpdb->last_error); + } + } + + /** + * Helper to recursively sanitize an array using sanitize_text_field on string values. + * + * @param array $data The array to sanitize. + * @return array The sanitized array. + */ + private function sanitize_array(array $data): array + { + foreach ($data as &$value) { + if (is_string($value)) { + $value = sanitize_text_field($value); + } elseif (is_array($value)) { + $value = $this->sanitize_array($value); + } + } + return $data; } public function test_save_method_inserts_log_into_database(): void @@ -75,30 +102,43 @@ public function test_save_method_inserts_log_into_database(): void ]; // Create and save the entity - $entity = DatabaseEntity::create(...$log_data); + $entity = new WordPressDatabaseEntity( + $log_data['channel'], + $log_data['level'], + $log_data['level_name'], + $log_data['message'], + $log_data['context'], + $log_data['extra'] + ); $insert_id = $entity->save(); $this->assertIsInt( $insert_id ); $this->assertGreaterThan(0, $insert_id, 'The save method should return a positive insert ID.'); - $entity = DatabaseEntity::find_by_id($insert_id); - $this->assertInstanceOf(DatabaseEntity::class, $entity, 'The find method should return an instance of DatabaseEntity.'); - $this->assertEquals($log_data['channel'], $entity->get_channel(), 'The channel should match the saved data.'); - $this->assertEquals($log_data['level'], $entity->get_level(), 'The level should match the saved data.'); - $this->assertEquals($log_data['level_name'], $entity->get_level_name(), 'The level name should match the saved data.'); - $this->assertEquals($log_data['message'], $entity->get_message(), 'The message should match the saved data.'); - $this->assertEquals($log_data['context'], $entity->get_context(), 'The context should match the saved data.'); - $this->assertEquals($log_data['extra'], $entity->get_extra(), 'The extra data should match the saved data.'); - - $this->assertNotEmpty($entity->get_datetime(), 'The datetime should not be empty.'); - $this->assertIsInt($entity->get_id(), 'The ID should be an integer.'); - $this->assertGreaterThan(0, $entity->get_id(), 'The ID should be greater than 0.'); - } + // Retrieve the log directly from the database + $table_name = WordPressDatabaseEntity::get_table_name(); + $retrieved_log = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $insert_id), ARRAY_A); - public function test_find_returns_null_for_nonexistent_id(): void - { - $entity = DatabaseEntity::find_by_id(999999); - $this->assertNull($entity, 'find_by_id() should return null for a non-existent ID.'); + $this->assertIsArray($retrieved_log, 'The log should be retrieved from the database.'); + + $entity_from_db = WordPressDatabaseEntity::from_array($retrieved_log); + + $this->assertInstanceOf(WordPressDatabaseEntity::class, $entity_from_db, 'from_array should return an instance of WordPressDatabaseEntity.'); + $this->assertEquals($log_data['channel'], $entity_from_db->get_channel(), 'The channel should match the saved data.'); + $this->assertEquals($log_data['level'], $entity_from_db->get_level(), 'The level should match the saved data.'); + $this->assertEquals($log_data['level_name'], $entity_from_db->get_level_name(), 'The level name should match the saved data.'); + $this->assertEquals($log_data['message'], $entity_from_db->get_message(), 'The message should match the saved data.'); + + // For context and extra, we need to compare them after sanitization is applied on the original data. + $sanitized_context = $this->sanitize_array($log_data['context']); + $sanitized_extra = $this->sanitize_array($log_data['extra']); + + $this->assertEquals($sanitized_context, $entity_from_db->get_context(), 'The context should match the saved data.'); + $this->assertEquals($sanitized_extra, $entity_from_db->get_extra(), 'The extra data should match the saved data.'); + + $this->assertNotEmpty($entity_from_db->get_datetime(), 'The datetime should not be empty.'); + $this->assertIsInt($entity_from_db->get_id(), 'The ID should be an integer.'); + $this->assertGreaterThan(0, $entity_from_db->get_id(), 'The ID should be greater than 0.'); } /** @@ -107,7 +147,7 @@ public function test_find_returns_null_for_nonexistent_id(): void */ public function test_sanitize_array_field_method(): void { - $reflection = new ReflectionClass(DatabaseEntity::class); + $reflection = new ReflectionClass(WordPressDatabaseEntity::class); $method = $reflection->getMethod('sanitize_array_field'); $method->setAccessible(true); @@ -131,12 +171,13 @@ public function test_sanitize_array_field_method(): void ], ]; - $clean_array = $method->invoke($this->entity_instance, $dirty_array); + $entity_instance = new WordPressDatabaseEntity('test', 100, 'DEBUG', 'test message'); + $clean_array = $method->invoke($entity_instance, $dirty_array); $this->assertEquals($expected_clean_array, $clean_array); } - /** + /** * @test * It should return 0 when the database insert operation fails. * @@ -148,7 +189,7 @@ public function test_save_returns_zero_on_database_failure(): void // 1. Define a callback that will force the DB query to fail. $force_fail_callback = function ($query) { // Check if this is the specific INSERT query we want to fail. - if (strpos($query, 'INSERT INTO `' . DatabaseEntity::get_table_name() . '`') !== false) { + if (strpos($query, 'INSERT INTO `' . WordPressDatabaseEntity::get_table_name() . '`') !== false) { // Returning a non-string value like `false` will cause the // $wpdb->insert() method to fail and return false. return false; @@ -161,7 +202,7 @@ public function test_save_returns_zero_on_database_failure(): void add_filter('query', $force_fail_callback); // 3. Create a valid entity that we will attempt to save. - $entity = DatabaseEntity::create( + $entity = new WordPressDatabaseEntity( 'failure_test', 500, 'CRITICAL', @@ -180,7 +221,7 @@ public function test_save_returns_zero_on_database_failure(): void public function test_get_query() : void { - $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity = Mockery::mock(WordPressDatabaseEntity::class)->makePartial(); $mockEntity->shouldReceive('get_context')->andReturn([ 'query' => 'query GetAllPosts { posts { nodes { title content } } }' ]); @@ -193,7 +234,7 @@ public function test_get_query() : void public function test_get_query_in_request() : void { - $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity = Mockery::mock(WordPressDatabaseEntity::class)->makePartial(); $mockEntity->shouldReceive('get_context')->andReturn([ 'request' => [ 'params' => [ @@ -210,7 +251,7 @@ public function test_get_query_in_request() : void public function test_get_invalid_query() : void { - $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity = Mockery::mock(WordPressDatabaseEntity::class)->makePartial(); $mockEntity->shouldReceive('get_context')->andReturn([ 'request' => 'query GetAllPosts { posts { nodes { title content } } }' ]); @@ -219,7 +260,7 @@ public function test_get_invalid_query() : void $mockEntity->get_query() ); - $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity = Mockery::mock(WordPressDatabaseEntity::class)->makePartial(); $mockEntity->shouldReceive('get_context')->andReturn([ 'request' => [ 'query GetAllPosts { posts { nodes { title content } } }' @@ -230,7 +271,7 @@ public function test_get_invalid_query() : void $mockEntity->get_query() ); - $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity = Mockery::mock(WordPressDatabaseEntity::class)->makePartial(); $mockEntity->shouldReceive('get_context')->andReturn([]); $this->assertNull( @@ -238,7 +279,7 @@ public function test_get_invalid_query() : void ); - $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity = Mockery::mock(WordPressDatabaseEntity::class)->makePartial(); $mockEntity->shouldReceive('get_context')->andReturn([ 'request' => [ 'params' => [ @@ -251,49 +292,4 @@ public function test_get_invalid_query() : void $mockEntity->get_query() ); } - - public function test_find_logs() : void { - - $log_data = [ - 'channel' => 'wpgraphql_logging', - 'level' => 200, - 'level_name' => 'INFO', - 'message' => 'WPGraphQL Outgoing Response', - 'context' => [ - 'site_url' => 'http://test.local', - 'wp_version' => '6.8.2', - 'wp_debug_mode' => true, - 'plugin_version'=> '0.0.1' - ], - 'extra' => [ - 'ip' => '127.0.0.1', - 'url' => '/index.php?graphql', - 'server' => 'test.local', - 'referrer' => 'http://test.local/wp-admin/admin.php?page=graphiql-ide', - 'process_id' => 5819, - 'http_method' => 'POST', - 'memory_usage' => '14 MB', - 'wpgraphql_query' => 'query GetPost($uri: ID!) { post(id: $uri, idType: URI) { title content } }', - 'memory_peak_usage' => '14 MB', - 'wpgraphql_variables' => [ - 'uri' => 'hello-world' - ], - 'wpgraphql_operation_name' => 'GetPost' - ] - ]; - - // Create and save the entity - $entity = DatabaseEntity::create(...$log_data); - $insert_id = $entity->save(); - - $where_clauses = [ - 'level' => 200, - 'id' => $insert_id - ]; - $logs = DatabaseEntity::find_logs(10, 0, $where_clauses, 'id', 'DESC'); - $this->assertIsArray($logs); - $this->assertCount(1, $logs); - $this->assertInstanceOf(DatabaseEntity::class, $logs[0]); - - } } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/WordPressDatabaseLogServiceTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/WordPressDatabaseLogServiceTest.php new file mode 100644 index 00000000..92ef409d --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/WordPressDatabaseLogServiceTest.php @@ -0,0 +1,187 @@ +log_service = new WordPressDatabaseLogService(); + $this->log_service->activate(); + } + + protected function tearDown(): void + { + $this->log_service->deactivate(); + parent::tearDown(); + } + + public function insert_mock_data(): void + { + $log_data = [ + 'channel' => 'wpgraphql_logging', + 'level' => 200, + 'level_name' => 'INFO', + 'message' => 'WPGraphQL Outgoing Response', + 'context' => [ + 'site_url' => 'http://test.local', + 'wp_version' => '6.8.2', + 'wp_debug_mode' => true, + 'plugin_version'=> '0.0.1' + ], + 'extra' => [ + 'ip' => '127.0.0.1', + 'url' => '/index.php?graphql', + 'server' => 'test.local', + 'referrer' => 'http://test.local/wp-admin/admin.php?page=graphiql-ide', + 'process_id' => 5819, + 'http_method' => 'POST', + 'memory_usage' => '14 MB', + 'wpgraphql_query' => 'query GetPost($uri: ID!) { post(id: $uri, idType: URI) { title content } }', + 'memory_peak_usage' => '14 MB', + 'wpgraphql_variables' => [ + 'uri' => 'hello-world' + ], + 'wpgraphql_operation_name' => 'GetPost' + ] + ]; + + $this->log_service->create_log_entity(...array_values($log_data)); + + $log_data['level'] = 400; + $log_data['level_name'] = 'ERROR'; + $log_data['message'] = 'WPGraphQL Error'; + sleep(1); // Ensure different timestamps + + $this->log_service->create_log_entity(...array_values($log_data)); + } + + public function test_find_entities_with_default_args(): void + { + $this->insert_mock_data(); + $logs = $this->log_service->find_entities_by_where(); + $this->assertIsArray($logs); + $this->assertCount(2, $logs); + $this->assertInstanceOf(LogEntityInterface::class, $logs[0]); + $this->assertInstanceOf(LogEntityInterface::class, $logs[1]); + } + + public function test_find_entities_with_custom_args(): void + { + $this->insert_mock_data(); + $args = [ + 'number' => 50, + 'offset' => 0, + 'orderby' => 'datetime', + 'order' => 'DESC' + ]; + $logs = $this->log_service->find_entities_by_where($args); + $this->assertIsArray($logs); + $this->assertCount(2, $logs); + + // Should get the last inserted log first + $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); + + /** + * Test default orderby + */ + $args['orderby'] = 'id'; + $logs = $this->log_service->find_entities_by_where($args); + $this->assertIsArray($logs); + + // Should be last as default is DESC + $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); + + /** + * Test where clause + */ + $args['where'] = [ + [ + 'column' => 'level', + 'operator' => '=', + 'value' => '200', + ], + ]; + + $logs = $this->log_service->find_entities_by_where($args); + $this->assertCount(1, $logs); + $this->assertEquals('WPGraphQL Outgoing Response', $logs[0]->get_message()); + + /** + * Test invalid order + */ + $args['order'] = ''; + $logs = $this->log_service->find_entities_by_where($args); + $this->assertIsArray($logs); + $this->assertCount(1, $logs); + + // Check log count + $this->assertEquals(2, $this->log_service->count_entities_by_where([])); + $this->assertEquals(2, $this->log_service->count_entities_by_where($args)); + } + + public function test_delete_logs(): void + { + $this->insert_mock_data(); + $logs = $this->log_service->find_entities_by_where(); + $this->assertCount(2, $logs); + + // Delete one log + $result = $this->log_service->delete_entity_by_id($logs[0]->get_id()); + $this->assertTrue($result); + + // Delete invalid logs + $result = $this->log_service->delete_entity_by_id(0); + $this->assertFalse($result); + + // Check remaining logs + $logs = $this->log_service->find_entities_by_where(); + $this->assertCount(1, $logs); + + // Delete all logs + $this->log_service->delete_all_entities(); + $logs = $this->log_service->find_entities_by_where(); + $this->assertCount(0, $logs); + } + + public function test_delete_log_older_than(): void + { + $this->insert_mock_data(); + $logs = $this->log_service->find_entities_by_where(['orderby' => 'datetime', 'order' => 'ASC']); + $this->assertCount(2, $logs); + + $date = $logs[0]->get_datetime(); + $dateTime = new \DateTime($date); + $dateTime->modify('+1 second'); + + $result = $this->log_service->delete_entities_older_than($dateTime); + $this->assertTrue($result); + + $logs = $this->log_service->find_entities_by_where(); + $this->assertCount(1, $logs); + + // Delete last log + $dateTime->modify('+1 second'); + $result = $this->log_service->delete_entities_older_than($dateTime); + $this->assertTrue($result); + + $logs = $this->log_service->find_entities_by_where(); + $this->assertCount(0, $logs); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php index 100df7d3..f9d8a854 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php @@ -6,24 +6,26 @@ use WPGraphQL\Logging\Admin\Settings\ConfigurationHelper; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\DataManagementTab; -use WPGraphQL\Logging\Logger\Database\LogsRepository; use WPGraphQL\Logging\Logger\Scheduler\DataDeletionScheduler; -use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use Monolog\Level; use lucatume\WPBrowser\TestCase\WPTestCase; use Mockery; - +use WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity; +use WPGraphQL\Logging\Logger\Api\LogServiceInterface; +use WPGraphQL\Logging\Logger\Store\LogStoreService; +use WPGraphQL\Logging\Plugin; class DataDeletionSchedulerTest extends WPTestCase { protected $initial_log_count = 0; - protected LogsRepository $repository; + private LogServiceInterface $log_service; protected function setUp(): void { parent::setUp(); - $this->repository = new LogsRepository(); + Plugin::activate(); + $this->log_service = LogStoreService::get_log_service(); $this->generate_logs(); $this->initial_log_count = $this->get_total_log_count(); } @@ -33,15 +35,13 @@ protected function tearDown(): void { parent::tearDown(); } - public function generate_logs() : void { global $wpdb; - $table_name = DatabaseEntity::get_table_name(); - $repository = new LogsRepository(); $now = new \DateTime(); + $table_name = WordPressDatabaseEntity::get_table_name(); for ($i = 0; $i < 10; $i++) { - $entity = DatabaseEntity::create( + $entity = $this->log_service->create_log_entity( 'wpgraphql_logging', 200, 'info', @@ -50,15 +50,12 @@ public function generate_logs() : void { [] ); - - $id = $entity->save(); - // Manually set the datetime to simulate old logs $log_date = (clone $now)->modify("-" . $i . " days"); $wpdb->update( $table_name, ['datetime' => $log_date->format('Y-m-d H:i:s')], - ['id' => $id], + ['id' => $entity->get_id()], ['%s'], ['%d'] ); @@ -66,24 +63,11 @@ public function generate_logs() : void { } public function delete_logs() : void { - $this->repository->delete_all(); + $this->log_service->delete_all_entities(); } public function get_total_log_count() : int { - return $this->repository->get_log_count([]); - } - - public function test_init_creates_singleton_instance(): void { - $reflection = new \ReflectionClass(DataDeletionScheduler::class); - $instanceProperty = $reflection->getProperty('instance'); - $instanceProperty->setAccessible(true); - $instanceProperty->setValue(null, null); - - $instance1 = DataDeletionScheduler::init(); - $instance2 = DataDeletionScheduler::init(); - $this->assertSame($instance1, $instance2); - $this->assertInstanceOf(DataDeletionScheduler::class, $instance1); - $this->assertInstanceOf(DataDeletionScheduler::class, $instance2); + return $this->log_service->count_entities_by_where([]); } @@ -145,10 +129,20 @@ public function test_data_deletion_scheduler_valid_config(): void { ] ); - $expected_count = $this->repository->get_log_count(['datetime >= NOW() - INTERVAL 3 DAY']); + // datetime >= NOW() - INTERVAL 3 DAY + $args = [ + [ + 'column' => 'datetime', + 'operator' => '>=', + 'value' => date('Y-m-d H:i:s', strtotime('-3 day')), + ], + ]; + $expected_count = $this->log_service->count_entities_by_where($args); + $this->assertEquals(4, $expected_count); + // Delete logs $scheduler->perform_deletion(); - $total_count = $this->get_total_log_count(); + $total_count = $this->log_service->count_entities_by_where([]); $this->assertLessThan($this->initial_log_count, $total_count); $this->assertGreaterThanOrEqual(0, $total_count); $this->assertEquals($expected_count, $total_count); From af2055b203d2afdaf6ad2a2bc718829506521ecd Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 18:01:39 +0000 Subject: [PATCH 30/59] Update docs. --- .../docs/how-to/admin_add_view_column.md | 4 ++-- plugins/wpgraphql-logging/docs/reference/admin.md | 6 +++--- .../wpgraphql-logging/docs/reference/logging.md | 15 ++------------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/plugins/wpgraphql-logging/docs/how-to/admin_add_view_column.md b/plugins/wpgraphql-logging/docs/how-to/admin_add_view_column.md index a9223ec9..71e02161 100644 --- a/plugins/wpgraphql-logging/docs/how-to/admin_add_view_column.md +++ b/plugins/wpgraphql-logging/docs/how-to/admin_add_view_column.md @@ -33,7 +33,7 @@ add_filter( 'wpgraphql_logging_logs_table_column_headers', function( $headers ) ### Step 2 — Provide the cell value -Each row item is a `\WPGraphQL\Logging\Logger\Database\DatabaseEntity`. Memory data is stored in `$item->get_extra()['memory_peak_usage']`. +Each row item is a `\WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity`. Memory data is stored in `$item->get_extra()['memory_peak_usage']`. ```php add_filter( 'wpgraphql_logging_logs_table_column_value', function( $value, $item, $column_name ) { @@ -41,7 +41,7 @@ add_filter( 'wpgraphql_logging_logs_table_column_value', function( $value, $item return $value; } - if ( ! $item instanceof \WPGraphQL\Logging\Logger\Database\DatabaseEntity ) { + if ( ! $item instanceof \WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity ) { return $value; } diff --git a/plugins/wpgraphql-logging/docs/reference/admin.md b/plugins/wpgraphql-logging/docs/reference/admin.md index 7f16e8b1..7b583812 100644 --- a/plugins/wpgraphql-logging/docs/reference/admin.md +++ b/plugins/wpgraphql-logging/docs/reference/admin.md @@ -304,7 +304,7 @@ Filters the rendered value for each column. Parameters: - `$value` (mixed) -- `$item` (\WPGraphQL\Logging\Logger\Database\DatabaseEntity) +- `$item` (\WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity) - `$column_name` (string) Returns: mixed @@ -380,7 +380,7 @@ Filters the CSV column headers. Parameters: - `$headers` (array) - `$log_id` (int) -- `$log` (\WPGraphQL\Logging\Logger\Database\DatabaseEntity) +- `$log` (\WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity) Returns: array @@ -397,7 +397,7 @@ Filters the CSV row values. Parameters: - `$content` (array) - `$log_id` (int) -- `$log` (\WPGraphQL\Logging\Logger\Database\DatabaseEntity) +- `$log` (\WPGraphQL\Logging\Logger\Database\WordPressDatabaseEntity) Returns: array diff --git a/plugins/wpgraphql-logging/docs/reference/logging.md b/plugins/wpgraphql-logging/docs/reference/logging.md index 7de943bb..dc197f57 100644 --- a/plugins/wpgraphql-logging/docs/reference/logging.md +++ b/plugins/wpgraphql-logging/docs/reference/logging.md @@ -9,8 +9,7 @@ The WPGraphQL Logging subsystem is built on [Monolog](https://github.com/Seldaek - [Logger\Handlers\WordPressDatabaseHandler](#class-loggerhandlerswordpressdatabasehandler) - [Logger\Processors\RequestHeadersProcessor](#class-loggerprocessorsrequestheadersprocessor) - [Logger\Processors\DataSanitizationProcessor](#class-loggerprocessorsdatasanitizationprocessor) -- [Logger\Database\DatabaseEntity](#class-loggerdatabasedatabaseentity) -- [Logger\Database\LogsRepository](#class-loggerdatabaselogsrepository) +- [Logger\Database\WordPressDatabaseEntity](#class-loggerdatabasewordpressdatabaseentity) - [Logger\Scheduler\DataDeletionScheduler](#class-loggerschedulerdatadeletionscheduler) - [Quick Start](#quick-start) - [Available Log Levels](#available-log-levels) @@ -217,7 +216,7 @@ Returns: Monolog\LogRecord --- -### Class: `Logger\Database\DatabaseEntity` +### Class: `Logger\Database\WordPressDatabaseEntity` Source: Represents a single log entry and provides persistence helpers. @@ -259,16 +258,6 @@ add_filter( 'wpgraphql_logging_allowed_orderby_columns', function( array $column **Note:** If an invalid column is requested, the query will fallback to ordering by `id` (default). ---- - -### Class: `Logger\Database\LogsRepository` -Source: - -Query and mutation helpers for log entries. - -Hooks: None. - - --- ### Class: `Logger\Scheduler\DataDeletionScheduler` From f6591442b946c3d5c26b12cf8861612f327b449c Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 18:03:19 +0000 Subject: [PATCH 31/59] Fixed DatabaseEntity name in psalm config file. --- plugins/wpgraphql-logging/psalm.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml index e291140b..e6aa32bb 100644 --- a/plugins/wpgraphql-logging/psalm.xml +++ b/plugins/wpgraphql-logging/psalm.xml @@ -32,7 +32,7 @@ - + From 4af2e086f4226eac5cdbc92b4b87bb6415f33032 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 18:54:00 +0000 Subject: [PATCH 32/59] Fixed some issues with Database Log Service. --- .../Database/WordPressDatabaseLogService.php | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php index dce6cf43..30de1487 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php @@ -21,7 +21,7 @@ class WordPressDatabaseLogService implements LogServiceInterface { /** * The values for the where clause. * - * @var array + * @var array */ protected array $where_values = []; @@ -52,10 +52,10 @@ public function create_log_entity(string $channel, int $level, string $level_nam */ public function find_entity_by_id(int $id): ?LogEntityInterface { global $wpdb; - $table_name = $this->get_table_name(); - $query = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $row = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + $table_name = $this->get_table_name(); + $query = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $row = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared if ( ! $row ) { return null; @@ -69,28 +69,53 @@ public function find_entity_by_id(int $id): ?LogEntityInterface { * * @param array $args The arguments for the where clause. * + * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh, Generic.Metrics.CyclomaticComplexity.MaxExceeded + * * @return array<\WPGraphQL\Logging\Logger\Api\LogEntityInterface> The found log entities. */ public function find_entities_by_where(array $args = []): array { global $wpdb; - $this->where_values = []; + // Reset the where values. + $this->where_values = []; + $sql = 'SELECT * FROM %i'; - $this->where_values[] = sanitize_text_field( $this->get_table_name() ); + $this->where_values[] = $this->get_table_name(); if ( isset( $args['where'] ) && is_array( $args['where'] ) ) { - $sql = $this->prepare_sql( $sql, $args['where'] ); + $sql = $this->prepare_sql( $sql, $args['where'] ?? [] ); } + $allowed_columns = $this->get_allowed_columns(); + + // Validate the orderby column. $orderby = $args['orderby'] ?? 'id'; - $order = $args['order'] ?? 'DESC'; - $limit = $args['number'] ?? 100; - $offset = $args['offset'] ?? 0; + if ( ! in_array( $orderby, $allowed_columns, true ) ) { + $orderby = 'id'; + } + + $order = $args['order'] ?? 'DESC'; + if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) { + $order = 'DESC'; + } + + $limit = $args['number'] ?? 100; + if ( ! is_numeric( $limit ) ) { + $limit = 100; + } + $limit = (int) $limit; + + $offset = $args['offset'] ?? 0; + if ( ! is_numeric( $offset ) ) { + $offset = 0; + } $sql .= " ORDER BY $orderby $order LIMIT %d, %d"; - $this->where_values[] = (string) $offset; - $this->where_values[] = (string) $limit; + $this->where_values[] = $offset; + $this->where_values[] = $limit; - $results = $wpdb->get_results( $wpdb->prepare( $sql, $this->where_values ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + // We validate the parameters above, so we can use them directly. + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( $wpdb->prepare( $sql, $this->where_values ), ARRAY_A ); if ( empty( $results ) || ! is_array( $results ) ) { return []; @@ -167,8 +192,9 @@ public function count_entities_by_where(array $args = []): int { $sql = 'SELECT COUNT(*) FROM %i'; $this->where_values = [ $this->get_table_name() ]; $sql = $this->prepare_sql( $sql, $args ); - // @TODO - Fix this. - return (int) $wpdb->get_var( $wpdb->prepare( $sql, $this->where_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + // Values are validated above, so we can use them directly. + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return (int) $wpdb->get_var( $wpdb->prepare( $sql, $this->where_values ) ); } /** @@ -211,8 +237,9 @@ protected function get_table_name(): string { * @return string The prepared SQL query. */ protected function prepare_sql(string $sql, array $where_conditions): string { - $where_clauses = []; - $safe_operators = $this->get_safe_operators(); + $where_clauses = []; + $safe_operators = $this->get_safe_operators(); + $allowed_columns = $this->get_allowed_columns(); foreach ( $where_conditions as $column => $condition ) { if ( ! is_array( $condition ) || ! isset( $condition['column'] ) || ! isset( $condition['value'] ) || ! isset( $condition['operator'] ) ) { continue; @@ -222,6 +249,9 @@ protected function prepare_sql(string $sql, array $where_conditions): string { if ( '' === $column ) { continue; } + if ( ! in_array( $column, $allowed_columns, true ) ) { + continue; + } $value = $condition['value']; $operator = $condition['operator']; if ( ! in_array( $operator, $safe_operators, true ) ) { @@ -229,8 +259,8 @@ protected function prepare_sql(string $sql, array $where_conditions): string { } $where_clauses[] = "%i $operator %s"; - $this->where_values[] = sanitize_text_field( (string) $column ); - $this->where_values[] = sanitize_text_field( (string) $value ); + $this->where_values[] = $column; + $this->where_values[] = $value; } if ( ! empty( $where_clauses ) ) { @@ -246,6 +276,15 @@ protected function prepare_sql(string $sql, array $where_conditions): string { * @return array The safe operators. */ protected function get_safe_operators(): array { - return [ '=', '!=', '>', '<', '>=', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ]; + return [ '=', '!=', '>', '<', '>=', '<=', 'LIKE', 'NOT LIKE' ]; + } + + /** + * Gets the allowed columns for the database table. + * + * @return array + */ + protected function get_allowed_columns(): array { + return [ 'id', 'datetime', 'level', 'level_name', 'channel', 'message' ]; } } From da1b11c023e94bec5c7737588366bae68f2cb9f7 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 29 Oct 2025 18:58:16 +0000 Subject: [PATCH 33/59] Removed filters. --- .../wpgraphql-logging/docs/reference/admin.md | 31 ------------------- .../src/Admin/ViewLogsPage.php | 5 +-- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/plugins/wpgraphql-logging/docs/reference/admin.md b/plugins/wpgraphql-logging/docs/reference/admin.md index 7b583812..bf48cb62 100644 --- a/plugins/wpgraphql-logging/docs/reference/admin.md +++ b/plugins/wpgraphql-logging/docs/reference/admin.md @@ -232,21 +232,6 @@ add_filter( 'wpgraphql_logging_filter_redirect_url', function( $redirect_url, $f }, 10, 2 ); ``` -#### Filter: `wpgraphql_logging_list_template` -Filters the template path for the logs list. - -Parameters: -- `$template_path` (string) - -Returns: string - -Example: -```php -add_filter( 'wpgraphql_logging_list_template', function( $template_path ) { - return plugin_dir_path( __FILE__ ) . 'templates/custom-list.php'; -}); -``` - #### Filter: `wpgraphql_logging_view_template` Filters the template path for the single log view. @@ -338,22 +323,6 @@ add_filter( 'wpgraphql_logging_logs_table_where_clauses', function( $where, $req return $where; }, 10, 2 ); ``` - -#### Filter: `wpgraphql_logging_filters_template` -Filters the template path for the filters UI. - -Parameters: -- `$template_path` (string) - -Returns: string - -Example: -```php -add_filter( 'wpgraphql_logging_filters_template', function( $template_path ) { - return plugin_dir_path( __FILE__ ) . 'templates/custom-filters.php'; -}); -``` - --- ### Class: `View\Download\DownloadLogService` diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 3189abfb..ec4851ce 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -245,10 +245,7 @@ protected function get_post_value(string $key): ?string { */ protected function render_list_page(): void { $list_table = new ListTable( $this->get_log_service() ); // @phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable - $list_template = apply_filters( - 'wpgraphql_logging_list_template', - __DIR__ . '/View/Templates/WPGraphQLLoggerList.php' - ); + $list_template = __DIR__ . '/View/Templates/WPGraphQLLoggerList.php'; require_once $list_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } From b027cf86ff013de4d3c724c33f485d92ce7a2c30 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 30 Oct 2025 10:09:05 +0000 Subject: [PATCH 34/59] Add SRI to external scripts. --- .../wpgraphql-logging/bin/run-codeception.sh | 3 +- .../src/Admin/ViewLogsPage.php | 106 +++++++++++++++++- 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/plugins/wpgraphql-logging/bin/run-codeception.sh b/plugins/wpgraphql-logging/bin/run-codeception.sh index 6bbcc0e7..8607e94a 100755 --- a/plugins/wpgraphql-logging/bin/run-codeception.sh +++ b/plugins/wpgraphql-logging/bin/run-codeception.sh @@ -77,8 +77,7 @@ run_tests() { fi if [[ -z "$coverage_percent" && -f "tests/_output/coverage/index.html" ]]; then - # macOS/BSD grep lacks -P; use -E and strip the percent sign - coverage_percent=$(grep -Eo '([0-9]+(\.[0-9]+)?)%' "tests/_output/coverage/index.html" | head -1 | tr -d '%') + coverage_percent=$(grep -Eo '[0-9]+\.[0-9]+%' "tests/_output/coverage/index.html" | head -1 | tr -d '%') fi if [[ -z "$coverage_percent" ]]; then echo "Warning: Could not determine code coverage percentage." diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index ec4851ce..a595ead3 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -65,6 +65,8 @@ public function setup(): void { /** * Registers the settings page for the view logs. + * + * @psalm-suppress HookNotFound */ public function register_settings_page(): void { @@ -91,7 +93,9 @@ public function register_settings_page(): void { add_action( 'load-' . $this->page_hook, [ $this, 'process_page_actions_before_rendering' ], 10, 0 ); // Enqueue scripts for the admin page. - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ], 9, 1 ); // Need to load before adding SRI attributes. + add_action( 'script_loader_tag', [ $this, 'add_sri_to_scripts' ], 10, 2 ); + add_action( 'style_loader_tag', [ $this, 'add_sri_to_styles' ], 10, 2 ); } /** @@ -104,18 +108,17 @@ public function enqueue_admin_scripts( string $hook_suffix ): void { return; } - // Enqueue WordPress's built-in datepicker and slider. wp_enqueue_script( 'jquery-ui-datepicker' ); wp_enqueue_script( 'jquery-ui-slider' ); - // Enqueue the timepicker addon script and styles from a CDN. wp_enqueue_script( 'jquery-ui-timepicker-addon', 'https://cdnjs.cloudflare.com/ajax/libs/jquery-ui-timepicker-addon/1.6.3/jquery-ui-timepicker-addon.min.js', [ 'jquery-ui-datepicker', 'jquery-ui-slider' ], '1.6.3', - true + true, ); + wp_enqueue_style( 'jquery-ui-timepicker-addon-style', 'https://cdnjs.cloudflare.com/ajax/libs/jquery-ui-timepicker-addon/1.6.3/jquery-ui-timepicker-addon.min.css', @@ -123,10 +126,8 @@ public function enqueue_admin_scripts( string $hook_suffix ): void { '1.6.3' ); - // Enqueue the base jQuery UI styles. wp_enqueue_style( 'jquery-ui-style', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css', [], '1.12.1' ); - // Add inline script to initialize the datetimepicker. wp_add_inline_script( 'jquery-ui-timepicker-addon', 'jQuery(document).ready(function($){ $(".wpgraphql-logging-datepicker").datetimepicker({ dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss" }); });' @@ -136,6 +137,99 @@ public function enqueue_admin_scripts( string $hook_suffix ): void { do_action( 'wpgraphql_logging_view_logs_admin_enqueue_scripts', $hook_suffix ); } + /** + * Adds integrity and crossorigin attributes to scripts. + * + * @param string $tag The script tag. + * @param string $handle The script handle. + * + * @link https://www.srihash.org/ to generate the integrity and crossorigin attributes. + */ + public function add_sri_to_scripts($tag, $handle): void { + + $scripts = [ + 'jquery-ui-timepicker-addon' => [ + 'integrity' => 'sha512-s5u/JBtkPg+Ff2WEr49/cJsod95UgLHbC000N/GglqdQuLnYhALncz8ZHiW/LxDRGduijLKzeYb7Aal9h3codZA==', + 'crossorigin' => 'anonymous', + ], + ]; + + $scripts = apply_filters( 'wpgraphql_logging_view_logs_add_sri_to_scripts', $scripts ); + + if ( ! isset( $scripts[ $handle ] ) ) { + return; + } + + $integrity = $scripts[ $handle ]['integrity']; + $crossorigin = $scripts[ $handle ]['crossorigin']; + + $tag = str_replace( + ' src=', + ' integrity="' . esc_attr( $integrity ) . '" crossorigin="' . esc_attr( $crossorigin ) . '" src=', + $tag + ); + + echo wp_kses( $tag, [ + 'script' => [ + 'src' => [], + 'integrity' => [], + 'crossorigin' => [], + 'type' => [], + 'id' => [], + ], + 'link' => [ + 'href' => [], + 'integrity' => [], + 'crossorigin' => [], + 'rel' => [], + 'type' => [], + 'id' => [], + ], + ] ); + } + + /** + * Adds integrity and crossorigin attributes to styles. + * + * @param string $tag The script tag. + * @param string $handle The script handle. + * + * @link https://www.srihash.org/ to generate the integrity and crossorigin attributes. + */ + public function add_sri_to_styles($tag, $handle): void { + + $styles = [ + 'jquery-ui-timepicker-addon-style' => [ + 'integrity' => 'sha512-LT9fy1J8pE4Cy6ijbg96UkExgOjCqcxAC7xsnv+mLJxSvftGVmmc236jlPTZXPcBRQcVOWoK1IJhb1dAjtb4lQ==', + 'crossorigin' => 'anonymous', + ], + ]; + $styles = apply_filters( 'wpgraphql_logging_view_logs_add_sri_to_styles', $styles ); + + if ( ! isset( $styles[ $handle ] ) ) { + return; + } + + $integrity = $styles[ $handle ]['integrity']; + $crossorigin = $styles[ $handle ]['crossorigin']; + + $tag = str_replace( + ' href=', + ' integrity="' . esc_attr( $integrity ) . '" crossorigin="' . esc_attr( $crossorigin ) . '" href=', + $tag + ); + + echo wp_kses( $tag, [ + 'link' => [ + 'src' => [], + 'integrity' => [], + 'crossorigin' => [], + 'type' => [], + 'id' => [], + ], + ] ); + } + /** * Renders the admin page for the logs. */ From bb690c137a24727681faf6afa47e4eea9d30ca44 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 30 Oct 2025 10:34:03 +0000 Subject: [PATCH 35/59] Added nonce to download action. --- .../src/Admin/View/List/ListTable.php | 8 +++++--- .../wpgraphql-logging/src/Admin/ViewLogsPage.php | 6 +++++- .../tests/wpunit/Admin/View/ViewLogsPageTest.php | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php index a1d68afb..62ff9103 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php @@ -299,8 +299,9 @@ public function column_cb( $item ): string { * @return string The rendered ID column or null. */ public function column_id( WordPressDatabaseEntity $item ): string { - $url = \WPGraphQL\Logging\Admin\ViewLogsPage::ADMIN_PAGE_SLUG; - $actions = [ + $url = \WPGraphQL\Logging\Admin\ViewLogsPage::ADMIN_PAGE_SLUG; + $download_nonce = wp_create_nonce( 'wpgraphql-logging-download_' . $item->get_id() ); + $actions = [ 'view' => sprintf( '%s', esc_attr( $url ), @@ -309,10 +310,11 @@ public function column_id( WordPressDatabaseEntity $item ): string { esc_html__( 'View', 'wpgraphql-logging' ) ), 'download' => sprintf( - '%s', + '%s', esc_attr( $url ), 'download', $item->get_id(), + esc_attr( $download_nonce ), esc_html__( 'Download', 'wpgraphql-logging' ) ), ]; diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index a595ead3..879bee2f 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -128,6 +128,7 @@ public function enqueue_admin_scripts( string $hook_suffix ): void { wp_enqueue_style( 'jquery-ui-style', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css', [], '1.12.1' ); + // Add inline script to initialize the datetimepicker. wp_add_inline_script( 'jquery-ui-timepicker-addon', 'jQuery(document).ready(function($){ $(".wpgraphql-logging-datepicker").datetimepicker({ dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss" }); });' @@ -351,7 +352,10 @@ protected function process_log_download(): void { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wpgraphql-logging' ) ); } - $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; + if ( $log_id > 0 ) { + check_admin_referer( 'wpgraphql-logging-download_' . $log_id ); + } $downloader = new DownloadLogService( $this->get_log_service() ); $downloader->generate_csv( $log_id ); } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php index df2cb3f8..7690f447 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php @@ -214,4 +214,20 @@ public function test_get_redirect_url_constructs_correct_url(): void { $url ); } + + public function test_process_log_download_dies_without_nonce(): void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + $_GET['action'] = 'download'; + $_GET['log'] = 'nonexistent-log-id'; + ob_start(); + $this->expectException(\WPDieException::class); + $this->expectExceptionMessage('Invalid log ID.'); + + // Use reflection to call the protected method + $reflection = new \ReflectionClass($instance); + $method = $reflection->getMethod('process_log_download'); + $method->setAccessible(true); + $method->invoke($instance); + } } From cb4d9d90dd44f435c2d86b378094ea527c3a2a52 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 30 Oct 2025 10:41:48 +0000 Subject: [PATCH 36/59] Add nonce verification for admin list --- .../src/Admin/View/List/ListTable.php | 4 ++++ .../Admin/View/Templates/WPGraphQLLoggerList.php | 1 + .../wpgraphql-logging/src/Admin/ViewLogsPage.php | 13 +++++++++++++ 3 files changed, 18 insertions(+) diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php index 62ff9103..2d1c09fc 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php +++ b/plugins/wpgraphql-logging/src/Admin/View/List/ListTable.php @@ -60,6 +60,10 @@ public function __construct( * @psalm-suppress PossiblyInvalidCast */ public function prepare_items(): void { + if ( array_key_exists( 'orderby', $_REQUEST ) || array_key_exists( 'order', $_REQUEST ) ) { + check_admin_referer( 'wpgraphql-logging-sort' ); + } + $this->process_bulk_action(); $this->_column_headers = apply_filters( diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerList.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerList.php index eeba4f0b..f4926d7a 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerList.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerList.php @@ -22,6 +22,7 @@
+ prepare_items(); $list_table->display(); diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 879bee2f..3b2acf92 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -134,6 +134,19 @@ public function enqueue_admin_scripts( string $hook_suffix ): void { 'jQuery(document).ready(function($){ $(".wpgraphql-logging-datepicker").datetimepicker({ dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss" }); });' ); + // Add nonce to sorting links. + wp_add_inline_script( + 'jquery', + 'jQuery(document).ready(function($){ + var nonce = $("#wpgraphql-logging-sort-nonce").val(); + if ( nonce ) { + $("th.sortable a").each(function(){ + this.href = this.href + "&_wpnonce=" + nonce; + }); + } + });' + ); + // Allow other plugins to enqueue their own scripts/styles. do_action( 'wpgraphql_logging_view_logs_admin_enqueue_scripts', $hook_suffix ); } From 778a93eb4e706a12e552d64ab4866e23c004375b Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 30 Oct 2025 11:59:58 +0000 Subject: [PATCH 37/59] Removed some filters. Fixed sort by issue. --- .../wpgraphql-logging/docs/reference/admin.md | 16 ---------------- .../wpgraphql-logging/docs/reference/logging.md | 15 --------------- .../src/Admin/View/List/ListTable.php | 5 +---- .../wpgraphql-logging/src/Admin/ViewLogsPage.php | 5 +---- .../Logger/Database/WordPressDatabaseEntity.php | 2 +- .../Database/WordPressDatabaseLogService.php | 3 ++- .../wpunit/Admin/View/List/ListTableTest.php | 3 ++- 7 files changed, 7 insertions(+), 42 deletions(-) diff --git a/plugins/wpgraphql-logging/docs/reference/admin.md b/plugins/wpgraphql-logging/docs/reference/admin.md index bf48cb62..348b4a9a 100644 --- a/plugins/wpgraphql-logging/docs/reference/admin.md +++ b/plugins/wpgraphql-logging/docs/reference/admin.md @@ -231,22 +231,6 @@ add_filter( 'wpgraphql_logging_filter_redirect_url', function( $redirect_url, $f return add_query_arg( 'my_flag', '1', $redirect_url ); }, 10, 2 ); ``` - -#### Filter: `wpgraphql_logging_view_template` -Filters the template path for the single log view. - -Parameters: -- `$template_path` (string) - -Returns: string - -Example: -```php -add_filter( 'wpgraphql_logging_view_template', function( $template_path ) { - return plugin_dir_path( __FILE__ ) . 'templates/custom-view.php'; -}); -``` - --- ### Class: `View\List\ListTable` diff --git a/plugins/wpgraphql-logging/docs/reference/logging.md b/plugins/wpgraphql-logging/docs/reference/logging.md index dc197f57..bc8cdce3 100644 --- a/plugins/wpgraphql-logging/docs/reference/logging.md +++ b/plugins/wpgraphql-logging/docs/reference/logging.md @@ -221,21 +221,6 @@ Source: prefix . 'wpgraphql_logging' ); + return $wpdb->prefix . 'wpgraphql_logging'; } /** diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php index 30de1487..e08fc51c 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php @@ -93,7 +93,7 @@ public function find_entities_by_where(array $args = []): array { $orderby = 'id'; } - $order = $args['order'] ?? 'DESC'; + $order = strtoupper( $args['order'] ?? 'DESC' ); if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) { $order = 'DESC'; } @@ -109,6 +109,7 @@ public function find_entities_by_where(array $args = []): array { $offset = 0; } + $sql .= " ORDER BY $orderby $order LIMIT %d, %d"; $this->where_values[] = $offset; $this->where_values[] = $limit; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php index 6a665d0b..c929ab11 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php @@ -320,7 +320,8 @@ public function test_prepare_items_handles_orderby_and_order_params(): void { $_REQUEST = [ 'orderby' => 'date', - 'order' => 'DESC' + 'order' => 'DESC', + '_wpnonce' => wp_create_nonce('wpgraphql-logging-sort'), ]; $this->list_table->prepare_items(); From 541aa0bef1ed76741d8dc0436a5354fd3e16036c Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 30 Oct 2025 12:10:40 +0000 Subject: [PATCH 38/59] Minor fixes. --- plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php | 4 ++++ .../src/Logger/Database/WordPressDatabaseLogService.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 0222a884..e62bb975 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -217,6 +217,10 @@ public function add_sri_to_styles($tag, $handle): void { 'integrity' => 'sha512-LT9fy1J8pE4Cy6ijbg96UkExgOjCqcxAC7xsnv+mLJxSvftGVmmc236jlPTZXPcBRQcVOWoK1IJhb1dAjtb4lQ==', 'crossorigin' => 'anonymous', ], + 'jquery-ui-style' => [ + 'integrity' => 'sha512-sOC1C3U/7L42Ao1++jwVCpnllhbxnfD525JBZE2h1+cYnLg3aIE3G1RBWKSr/9cF5LxB1CxPckAvHqzz7O4apQ==', + 'crossorigin' => 'anonymous', + ], ]; $styles = apply_filters( 'wpgraphql_logging_view_logs_add_sri_to_styles', $styles ); diff --git a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php index e08fc51c..c72a9279 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/WordPressDatabaseLogService.php @@ -54,7 +54,7 @@ public function find_entity_by_id(int $id): ?LogEntityInterface { global $wpdb; $table_name = $this->get_table_name(); - $query = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $query = $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $table_name, $id ); $row = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared if ( ! $row ) { From 98b17bd33a45a1dedfd4251e375951a3999da9c4 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Thu, 30 Oct 2025 14:55:07 +0000 Subject: [PATCH 39/59] Remove date autocomplete to make it easier to pick a date for the filter. --- .../View/Templates/WPGraphQLLoggerFilters.php | 2 + .../src/Admin/ViewLogsPage.php | 104 +----------------- 2 files changed, 4 insertions(+), 102 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerFilters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerFilters.php index 2561e3c3..169e650c 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerFilters.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/WPGraphQLLoggerFilters.php @@ -28,6 +28,7 @@ class="wpgraphql-logging-datepicker" placeholder="Start Date" value="" + autocomplete="off" style="width: 120px;" /> multiple ? '[]' : '' ) . '" '; diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php index 57e2fae7..11097f3b 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php @@ -49,7 +49,7 @@ public function __construct( */ 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 ); + $field_value = sanitize_text_field( $this->get_field_value( $option_value, $tab_key, $this->default_value ) ); return sprintf( diff --git a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php index cb104433..9a151862 100644 --- a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php @@ -159,21 +159,17 @@ public function get_current_tab( array $tabs = [] ): string { if ( empty( $tabs ) ) { return $this->get_default_tab(); } - - if ( ! isset( $_GET['tab'] ) || ! is_string( $_GET['tab'] ) ) { + if ( ! isset( $_GET['tab'] ) || ! is_string( $_GET['tab'] ) || ! isset( $_GET['wpgraphql_logging_settings_tab_nonce'] ) || ! is_string( $_GET['wpgraphql_logging_settings_tab_nonce'] ) ) { return $this->get_default_tab(); } - if ( ! isset( $_GET['wpgraphql_logging_settings_tab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['wpgraphql_logging_settings_tab_nonce'] ) ), 'wpgraphql-logging-settings-tab-action' ) ) { + $nonce = sanitize_text_field( $_GET['wpgraphql_logging_settings_tab_nonce'] ); + if ( false === wp_verify_nonce( $nonce, 'wpgraphql-logging-settings-tab-action' ) ) { return $this->get_default_tab(); } $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) ); - if ( '' === $tab ) { - return $this->get_default_tab(); - } - if ( array_key_exists( $tab, $tabs ) ) { return $tab; } diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index dd194468..514c078b 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -89,7 +89,7 @@ public function register_settings_page(): void { 'manage_options', self::ADMIN_PAGE_SLUG, [ $this, 'render_admin_page' ], - 'dashicons-list-view', + 'dashicons-chart-line', 25 ); @@ -97,7 +97,7 @@ public function register_settings_page(): void { add_action( 'load-' . $this->page_hook, [ $this, 'process_page_actions_before_rendering' ], 10, 0 ); // Enqueue scripts for the admin page. - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts_styles' ] ); } /** @@ -105,7 +105,7 @@ public function register_settings_page(): void { * * @param string $hook_suffix The current admin page. */ - public function enqueue_admin_scripts( string $hook_suffix ): void { + public function enqueue_admin_scripts_styles( string $hook_suffix ): void { if ( $hook_suffix !== $this->page_hook ) { return; } @@ -153,7 +153,6 @@ public function enqueue_admin_scripts( string $hook_suffix ): void { */ public function render_admin_page(): void { - $action = isset( $_REQUEST['action'] ) && is_string( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : 'list'; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php index fee7142d..a509575d 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php @@ -63,12 +63,12 @@ public function test_init_returns_same_instance_on_multiple_calls(): void { $this->assertSame($instance1, $instance2); } - public function test_enqueue_admin_scripts_only_on_correct_page(): void { + public function test_enqueue_admin_scripts_styles_only_on_correct_page(): void { $this->set_as_admin(); $instance = ViewLogsPage::init(); // Test with wrong hook suffix - $instance->enqueue_admin_scripts('different-page'); + $instance->enqueue_admin_scripts_styles('different-page'); $this->assertFalse(wp_script_is('jquery-ui-datepicker', 'enqueued')); // Test with correct hook suffix (simulate the page hook) @@ -77,7 +77,7 @@ public function test_enqueue_admin_scripts_only_on_correct_page(): void { $pageHookProperty->setAccessible(true); $pageHookProperty->setValue($instance, 'test-page-hook'); - $instance->enqueue_admin_scripts('test-page-hook'); + $instance->enqueue_admin_scripts_styles('test-page-hook'); $this->assertTrue(wp_script_is('jquery-ui-datepicker', 'enqueued')); $this->assertTrue(wp_script_is('jquery-ui-slider', 'enqueued')); } From d29d4dde9b34ebb1173f13cd76c34cd20c4e9173 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 4 Nov 2025 10:17:40 +0000 Subject: [PATCH 48/59] Fixed issue with view log page. --- plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 514c078b..59fe3573 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -281,7 +281,7 @@ protected function process_log_download(): void { */ protected function render_view_page(): void { $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; - $this->verify_admin_page_nonce( self::ADMIN_PAGE_DOWNLOAD_NONCE . '_' . $log_id ); + $this->verify_admin_page_nonce( self::ADMIN_PAGE_VIEW_NONCE . '_' . $log_id ); if ( 0 === (int) $log_id ) { echo '

' . esc_html__( 'Invalid log ID.', 'wpgraphql-logging' ) . '

'; From 7231933800331fdcda2c8e9b722760729f09f91a Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 4 Nov 2025 14:36:49 +0000 Subject: [PATCH 49/59] Refactor logging rules interface and update imports Moved LoggingRuleInterface to a new Api namespace and updated all references accordingly. Cleaned up admin asset enqueuing logic, improved documentation in EventManager, and made minor code style improvements. No functional changes to logging logic. --- plugins/wpgraphql-logging/composer.json | 2 +- plugins/wpgraphql-logging/index.php | 2 ++ .../Admin/Settings/ConfigurationHelper.php | 5 ---- .../src/Admin/SettingsPage.php | 8 ++---- .../src/Admin/ViewLogsPage.php | 8 ++---- .../src/Events/EventManager.php | 28 ++++++++++--------- .../{Rules => Api}/LoggingRuleInterface.php | 2 +- .../src/Logger/Rules/EnabledRule.php | 1 + .../src/Logger/Rules/ExcludeQueryRule.php | 1 + .../src/Logger/Rules/IpRestrictionsRule.php | 1 + .../src/Logger/Rules/LogResponseRule.php | 1 + .../src/Logger/Rules/QueryNullRule.php | 2 ++ .../src/Logger/Rules/RuleManager.php | 4 ++- .../src/Logger/Rules/SamplingRateRule.php | 1 + .../wpunit/Logger/Rules/RuleManagerTest.php | 2 +- 15 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 plugins/wpgraphql-logging/index.php rename plugins/wpgraphql-logging/src/Logger/{Rules => Api}/LoggingRuleInterface.php (93%) diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json index 131d875e..c2e85c9b 100644 --- a/plugins/wpgraphql-logging/composer.json +++ b/plugins/wpgraphql-logging/composer.json @@ -2,7 +2,7 @@ "name": "wpengine/wpgraphql-logging", "type": "wordpress-plugin", "description": "A plugin for logging WPGraphQL request lifecycle tp help with debugging and performance analysis for WPGraphQL queries.", - "license": "GPL-2.0", + "license": "GPLv2 or later", "version": "0.1.0", "authors": [ { diff --git a/plugins/wpgraphql-logging/index.php b/plugins/wpgraphql-logging/index.php new file mode 100644 index 00000000..97611c0c --- /dev/null +++ b/plugins/wpgraphql-logging/index.php @@ -0,0 +1,2 @@ +config = $cached_config; return; } - // Try to get from the WordPress object cache (could be Redis, Memcached, etc.). $cached_config = wp_cache_get( $option_key, $this->get_settings_group() ); if ( is_array( $cached_config ) ) { $this->config = $cached_config; - // Store in our custom cache group for faster access next time. wp_cache_set( $option_key, $cached_config, self::CACHE_GROUP, $cache_duration ); return; } - // Load from database. $this->config = $this->get_option_value( $option_key, [] ); - // Cache the result in both cache groups. wp_cache_set( $option_key, $this->config, self::CACHE_GROUP, $cache_duration ); wp_cache_set( $option_key, $this->config, $this->get_settings_group(), $cache_duration ); } diff --git a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php index 9a151862..65f1e499 100644 --- a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php @@ -196,23 +196,19 @@ public function load_scripts_styles( string $hook_suffix ): void { 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, + trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/css/settings/wp-graphql-logging-settings.css', [], 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' ) ) { wp_enqueue_script( 'wpgraphql-logging-settings-js', - $script_path, + trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/js/settings/wp-graphql-logging-settings.js', [], WPGRAPHQL_LOGGING_VERSION, true diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 59fe3573..c764f7aa 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -130,12 +130,10 @@ public function enqueue_admin_scripts_styles( string $hook_suffix ): void { wp_enqueue_style( 'jquery-ui-style', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css', [], '1.12.1' ); - // Enqueue admin scripts if they exist. - $script_path = trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/js/settings/wp-graphql-logging-view.js'; if ( file_exists( trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'assets/js/settings/wp-graphql-logging-view.js' ) ) { wp_enqueue_script( 'wpgraphql-logging-view-js', - $script_path, + trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_URL ) . 'assets/js/settings/wp-graphql-logging-view.js', [ 'jquery' ], WPGRAPHQL_LOGGING_VERSION, true @@ -233,8 +231,8 @@ public function get_redirect_url(): string { $redirect_url = add_query_arg( array_filter( $filters, static function ( $value ) { return '' !== $value; } ), $redirect_url ); - $redirect_url = apply_filters( 'wpgraphql_logging_filter_redirect_url', $redirect_url, $filters ); - return (string) $redirect_url; + + return (string) apply_filters( 'wpgraphql_logging_filter_redirect_url', $redirect_url, $filters ); } /** diff --git a/plugins/wpgraphql-logging/src/Events/EventManager.php b/plugins/wpgraphql-logging/src/Events/EventManager.php index a07d4a3b..d17befe3 100644 --- a/plugins/wpgraphql-logging/src/Events/EventManager.php +++ b/plugins/wpgraphql-logging/src/Events/EventManager.php @@ -5,25 +5,27 @@ namespace WPGraphQL\Logging\Events; /** - * Simple pub/sub Event Manager for WPGraphQL Logging + * Pub/sub Event Manager for WPGraphQL Logging. * - * Provides a lightweight event bus with optional WordPress bridge. + * This class provides a lightweight event bus with a WordPress bridge. * - * Users can: - * - subscribe to events using subscribe() - * - publish events using publish() - * - also listen via WordPress hooks: `wpgraphql_logging_event_{event_name}` + * Users can subscribe to events using subscribe() and publish events using publish(). + * They can also listen via WordPress hooks: `wpgraphql_logging_event_{event_name}`. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 */ final class EventManager { /** - * In-memory map of event name to priority to listeners. + * Events that can be subscribed to. * * @var array>> */ - private static array $events = []; + protected static array $events = []; /** - * Transform listeners that can modify a payload. + * Transformers that can modify an event's payload. * * @var array>> */ @@ -52,13 +54,14 @@ public static function subscribe(string $event_name, callable $listener, int $pr * * @param string $event_name Event name (see Events constants). * @param array $payload Arbitrary payload for listeners. + * + * @psalm-suppress HookNotFound */ public static function publish(string $event_name, array $payload = []): void { $ordered_listeners = self::get_ordered_listeners( $event_name ); if ( [] === $ordered_listeners ) { - /** @psalm-suppress HookNotFound */ do_action( 'wpgraphql_logging_event_' . $event_name, $payload ); return; } @@ -67,7 +70,6 @@ public static function publish(string $event_name, array $payload = []): void { self::invoke_listener( $listener, $payload ); } - /** @psalm-suppress HookNotFound */ do_action( 'wpgraphql_logging_event_' . $event_name, $payload ); } @@ -95,13 +97,14 @@ public static function subscribe_to_transform(string $event_name, callable $tran * @param string $event_name Event name. * @param array $payload Initial payload. * + * @psalm-suppress HookNotFound + * * @return array Modified payload. */ public static function transform(string $event_name, array $payload): array { $ordered_transforms = self::get_ordered_transforms( $event_name ); if ( [] === $ordered_transforms ) { - /** @psalm-suppress HookNotFound */ return apply_filters( 'wpgraphql_logging_filter_' . $event_name, $payload ); } @@ -109,7 +112,6 @@ public static function transform(string $event_name, array $payload): array { $payload = self::invoke_transform( $transform, $payload ); } - /** @psalm-suppress HookNotFound */ return apply_filters( 'wpgraphql_logging_filter_' . $event_name, $payload ); } diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/LoggingRuleInterface.php b/plugins/wpgraphql-logging/src/Logger/Api/LoggingRuleInterface.php similarity index 93% rename from plugins/wpgraphql-logging/src/Logger/Rules/LoggingRuleInterface.php rename to plugins/wpgraphql-logging/src/Logger/Api/LoggingRuleInterface.php index cab5abd0..80ff2685 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/LoggingRuleInterface.php +++ b/plugins/wpgraphql-logging/src/Logger/Api/LoggingRuleInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\Logger\Rules; +namespace WPGraphQL\Logging\Logger\Api; /** * Interface for logging rules. diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/EnabledRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/EnabledRule.php index 2b95ffba..41315531 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/EnabledRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/EnabledRule.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging\Logger\Rules; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; /** * Rule to check if logging is enabled. diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php index e422d580..ffff5b35 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging\Logger\Rules; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; /** * Rule to check if the query is excluded from logging. diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/IpRestrictionsRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/IpRestrictionsRule.php index 720f4a93..09e45f0e 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/IpRestrictionsRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/IpRestrictionsRule.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging\Logger\Rules; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; /** * Rule to check if logging should occur based on IP restrictions. diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/LogResponseRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/LogResponseRule.php index 4c2b1c09..58107d28 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/LogResponseRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/LogResponseRule.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging\Logger\Rules; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; /** * Rule to check if we should log the response. diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/QueryNullRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/QueryNullRule.php index b455ef5d..b4aab072 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/QueryNullRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/QueryNullRule.php @@ -4,6 +4,8 @@ namespace WPGraphQL\Logging\Logger\Rules; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; + /** * Rule to check if logging should occur based on query null setting. * diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/RuleManager.php b/plugins/wpgraphql-logging/src/Logger/Rules/RuleManager.php index 4849f245..62f4a8a4 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/RuleManager.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/RuleManager.php @@ -4,6 +4,8 @@ namespace WPGraphQL\Logging\Logger\Rules; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; + /** * Manages a set of logging rules and checks if all pass. * @@ -12,7 +14,7 @@ * @since 0.0.1 */ class RuleManager { - /** @var array<\WPGraphQL\Logging\Logger\Rules\LoggingRuleInterface> */ + /** @var array<\WPGraphQL\Logging\Logger\Api\LoggingRuleInterface> */ private array $rules = []; /** diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php index 72a9ddb3..a63e5d82 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging\Logger\Rules; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; /** * Rule to check if logging is enabled. diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php index 96236882..5455f032 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php @@ -5,7 +5,7 @@ namespace WPGraphQL\Logging\Tests\Logging\Rules; use WPGraphQL\Logging\Logger\Rules\RuleManager; -use WPGraphQL\Logging\Logger\Rules\LoggingRuleInterface; +use WPGraphQL\Logging\Logger\Api\LoggingRuleInterface; use lucatume\WPBrowser\TestCase\WPTestCase; /** From 35cfc3f2ca94ca9a3e2dceb17cab1ee0240e790c Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 4 Nov 2025 14:48:50 +0000 Subject: [PATCH 50/59] Refactored rule manager to make it easier for developers to change default rules. --- .../src/Logger/LoggingHelper.php | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php index 3fbc50e1..1c81318c 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -77,13 +77,29 @@ protected function get_rule_manager(): RuleManager { if ( null !== $this->rule_manager ) { return $this->rule_manager; } + + $default_rules = [ + new QueryNullRule(), + new SamplingRateRule(), + new EnabledRule(), + new IpRestrictionsRule(), + new ExcludeQueryRule(), + ]; + + /** + * Filter the logging rules before they are added to the manager. + * + * @param array<\WPGraphQL\Logging\Logger\Api\LoggingRuleInterface> $rules Array of rule objects. + */ + $rules = apply_filters( 'wpgraphql_logging_rules', $default_rules ); + $this->rule_manager = new RuleManager(); - $this->rule_manager->add_rule( new QueryNullRule() ); - $this->rule_manager->add_rule( new SamplingRateRule() ); - $this->rule_manager->add_rule( new EnabledRule() ); - $this->rule_manager->add_rule( new IpRestrictionsRule() ); - $this->rule_manager->add_rule( new ExcludeQueryRule() ); - apply_filters( 'wpgraphql_logging_rule_manager', $this->rule_manager ); + + /** @var \WPGraphQL\Logging\Logger\Api\LoggingRuleInterface $rule */ + foreach ( $rules as $rule ) { + $this->rule_manager->add_rule( $rule ); + } + return $this->rule_manager; } From 91d7eecc596d3e88cde83471e80c490e18e5e2e8 Mon Sep 17 00:00:00 2001 From: ahuseyn Date: Tue, 4 Nov 2025 16:14:36 +0100 Subject: [PATCH 51/59] rearrange menu items under GraphQL --- .../src/Admin/Settings/Menu/MenuPage.php | 4 +++- .../src/Admin/Settings/Templates/admin.php | 1 + .../wpgraphql-logging/src/Admin/SettingsPage.php | 4 ++-- .../wpgraphql-logging/src/Admin/ViewLogsPage.php | 13 +++++++++++-- .../reset-wpgraphql-logging-settings.php | 2 +- plugins/wpgraphql-logging/tests/e2e/utils.js | 4 ++-- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Menu/MenuPage.php b/plugins/wpgraphql-logging/src/Admin/Settings/Menu/MenuPage.php index 19e309dd..cd10c770 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Menu/MenuPage.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Menu/MenuPage.php @@ -4,6 +4,8 @@ namespace WPGraphQL\Logging\Admin\Settings\Menu; +use WPGraphQL\Logging\Admin\ViewLogsPage; + /** * Menu class for WordPress admin settings page. * @@ -78,7 +80,7 @@ public function __construct( */ public function register_page(): void { add_submenu_page( - 'options-general.php', + ViewLogsPage::ADMIN_PAGE_SLUG, $this->page_title, $this->menu_title, 'manage_options', diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php index 89cc6243..c34f170b 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php @@ -22,6 +22,7 @@

+