From 6a9509c12616d16c1bf530c3d1d63483e8f15009 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Tue, 3 Jun 2025 10:56:58 -0400 Subject: [PATCH 01/24] local env --- .../wp-graphql-headless-webhooks/.wp-env.json | 24 + .../composer.json | 2 +- .../composer.lock | 812 +++++++++++++----- .../wp-graphql-headless-webhooks/package.json | 18 + pnpm-lock.yaml | 6 + 5 files changed, 624 insertions(+), 238 deletions(-) create mode 100644 plugins/wp-graphql-headless-webhooks/.wp-env.json create mode 100644 plugins/wp-graphql-headless-webhooks/package.json diff --git a/plugins/wp-graphql-headless-webhooks/.wp-env.json b/plugins/wp-graphql-headless-webhooks/.wp-env.json new file mode 100644 index 00000000..51dc3958 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/.wp-env.json @@ -0,0 +1,24 @@ +{ + "core": null, + "phpVersion": "8.0", + "plugins": [ + "https://downloads.wordpress.org/plugin/wp-graphql.latest-stable.zip", + "." + ], + "port": 8889, + "testsPort": 8890, + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false, + "WP_ENVIRONMENT_TYPE": "development" + }, + "mappings": { + "wp-content/plugins/wp-graphql-headless-webhooks": "." + }, + "env": { + "tests": { + "port": 8890 + } + } +} diff --git a/plugins/wp-graphql-headless-webhooks/composer.json b/plugins/wp-graphql-headless-webhooks/composer.json index 69dde89d..e7a4962f 100644 --- a/plugins/wp-graphql-headless-webhooks/composer.json +++ b/plugins/wp-graphql-headless-webhooks/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": "^7.4 || ^8.0", + "php": "^7.4 || ^8.0 || ^8.4", "axepress/wp-graphql-plugin-boilerplate": "^0.1.0" }, "require-dev": { diff --git a/plugins/wp-graphql-headless-webhooks/composer.lock b/plugins/wp-graphql-headless-webhooks/composer.lock index b2fa58da..3d4b6bcb 100644 --- a/plugins/wp-graphql-headless-webhooks/composer.lock +++ b/plugins/wp-graphql-headless-webhooks/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c95ce760de9d78f5a1b0c1e16bbc33a4", + "content-hash": "038a2fe0d6aef203f3bc661d6115e6ed", "packages": [ { "name": "axepress/wp-graphql-plugin-boilerplate", @@ -69,43 +69,36 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v2.6.4", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^7 | ^8 | ^9", - "react/promise": "^2", - "vimeo/psalm": "^3.12" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "files": [ - "lib/functions.php", - "lib/Internal/functions.php" + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\": "lib" + "Amp\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -113,10 +106,6 @@ "MIT" ], "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, { "name": "Aaron Piotrowski", "email": "aaron@trowski.com" @@ -128,6 +117,10 @@ { "name": "Niklas Keller", "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], "description": "A non-blocking concurrency framework for PHP applications.", @@ -144,9 +137,8 @@ "promise" ], "support": { - "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.4" + "source": "https://github.com/amphp/amp/tree/v3.1.0" }, "funding": [ { @@ -154,41 +146,45 @@ "type": "github" } ], - "time": "2024-03-21T18:52:26+00:00" + "time": "2025-01-26T16:07:39+00:00" }, { "name": "amphp/byte-stream", - "version": "v1.8.2", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/amphp/byte-stream.git", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", "shasum": "" }, "require": { - "amphp/amp": "^2", - "php": ">=7.1" + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1.4", - "friendsofphp/php-cs-fixer": "^2.3", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6 || ^7 || ^8", - "psalm/phar": "^3.11.4" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" }, "type": "library", "autoload": { "files": [ - "lib/functions.php" + "src/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\ByteStream\\": "lib" + "Amp\\ByteStream\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -217,7 +213,136 @@ ], "support": { "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" }, "funding": [ { @@ -225,7 +350,140 @@ "type": "github" } ], - "time": "2024-04-13T18:00:56+00:00" + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" }, { "name": "automattic/vipwpcs", @@ -392,25 +650,31 @@ }, { "name": "behat/gherkin", - "version": "v4.10.0", + "version": "v4.14.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6" + "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6", - "reference": "cbb83c4c435dd8d05a161f2a5ae322e61b2f4db6", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", + "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", "shasum": "" }, "require": { - "php": "~7.2|~8.0" + "composer-runtime-api": "^2.2", + "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" }, "require-dev": { - "cucumber/cucumber": "dev-gherkin-24.1.0", - "phpunit/phpunit": "~8|~9", - "symfony/yaml": "~3|~4|~5|~6|~7" + "cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1", + "friendsofphp/php-cs-fixer": "^3.65", + "mikey179/vfsstream": "^1.6", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpunit/phpunit": "^10.5", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "suggest": { "symfony/yaml": "If you want to parse features, represented in YAML files" @@ -422,8 +686,8 @@ } }, "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" + "psr-4": { + "Behat\\Gherkin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -434,11 +698,11 @@ { "name": "Konstantin Kudryashov", "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "homepage": "https://everzet.com" } ], "description": "Gherkin DSL parser for PHP", - "homepage": "http://behat.org/", + "homepage": "https://behat.org/", "keywords": [ "BDD", "Behat", @@ -449,9 +713,9 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.10.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.14.0" }, - "time": "2024-10-19T14:46:06+00:00" + "time": "2025-05-23T15:06:40+00:00" }, { "name": "codeception/codeception", @@ -1136,16 +1400,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.6", + "version": "1.5.7", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" + "reference": "d665d22c417056996c59019579f1967dfe5c1e82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", - "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82", + "reference": "d665d22c417056996c59019579f1967dfe5c1e82", "shasum": "" }, "require": { @@ -1192,7 +1456,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.6" + "source": "https://github.com/composer/ca-bundle/tree/1.5.7" }, "funding": [ { @@ -1208,7 +1472,7 @@ "type": "tidelift" } ], - "time": "2025-03-06T14:30:56+00:00" + "time": "2025-05-26T15:08:54+00:00" }, { "name": "composer/class-map-generator", @@ -1937,30 +2201,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.5.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -1987,7 +2251,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -2003,7 +2267,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "eftec/bladeone", @@ -3178,25 +3442,27 @@ }, { "name": "nikic/php-parser", - "version": "v4.19.4", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -3204,7 +3470,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -3228,9 +3494,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-09-29T15:01:53+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -4377,16 +4643,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.16", + "version": "2.1.17", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9" + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", - "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", "shasum": "" }, "require": { @@ -4431,7 +4697,7 @@ "type": "github" } ], - "time": "2025-05-16T09:40:10+00:00" + "time": "2025-05-21T20:55:28+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -4960,22 +5226,27 @@ }, { "name": "psr/container", - "version": "1.1.2", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -5002,9 +5273,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:50:12+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/event-dispatcher", @@ -5218,30 +5489,30 @@ }, { "name": "psr/log", - "version": "1.1.4", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -5262,9 +5533,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "source": "https://github.com/php-fig/log/tree/2.0.0" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2021-07-14T16:41:46+00:00" }, { "name": "ralouphie/getallheaders", @@ -5383,6 +5654,78 @@ ], "time": "2024-05-24T10:39:05+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, { "name": "sebastian/cli-parser", "version": "1.0.2", @@ -6578,32 +6921,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.18.0", + "version": "8.18.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593" + "reference": "06b18b3f64979ab31d27c37021838439f3ed5919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/f3b23cb9b26301b8c3c7bb03035a1bee23974593", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/06b18b3f64979ab31d27c37021838439f3ed5919", + "reference": "06b18b3f64979ab31d27c37021838439f3ed5919", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.12.2" + "squizlabs/php_codesniffer": "^3.13.0" }, "require-dev": { "phing/phing": "3.0.1", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.13", - "phpstan/phpstan-deprecation-rules": "2.0.2", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-deprecation-rules": "2.0.3", "phpstan/phpstan-phpunit": "2.0.6", "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.17|12.1.3" + "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" }, "type": "phpcodesniffer-standard", "extra": { @@ -6627,7 +6970,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.18.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.18.1" }, "funding": [ { @@ -6639,33 +6982,32 @@ "type": "tidelift" } ], - "time": "2025-05-01T09:40:50+00:00" + "time": "2025-05-22T14:32:30+00:00" }, { "name": "softcreatr/jsonpath", - "version": "0.7.6", + "version": "0.8.3", "source": { "type": "git", "url": "https://github.com/SoftCreatR/JSONPath.git", - "reference": "e04c02cb78bcc242c69d17dac5b29436bf3e1076" + "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/e04c02cb78bcc242c69d17dac5b29436bf3e1076", - "reference": "e04c02cb78bcc242c69d17dac5b29436bf3e1076", + "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/fc12dee0b46f3fa3a175c4051dbab60984acef4b", + "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.1,<8.0" + "php": ">=8.0" }, "replace": { "flow/jsonpath": "*" }, "require-dev": { - "phpunit/phpunit": ">=7.0", - "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "^3.5" + "phpunit/phpunit": "^9.6", + "roave/security-advisories": "dev-latest" }, "type": "library", "autoload": { @@ -6708,33 +7050,37 @@ "type": "github" } ], - "time": "2022-09-27T09:27:12+00:00" + "time": "2023-08-17T20:14:00+00:00" }, { "name": "spatie/array-to-xml", - "version": "2.17.1", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/spatie/array-to-xml.git", - "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46" + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", - "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", "shasum": "" }, "require": { "ext-dom": "*", - "php": "^7.4|^8.0" + "php": "^8.0" }, "require-dev": { "mockery/mockery": "^1.2", "pestphp/pest": "^1.21", - "phpunit/phpunit": "^9.0", "spatie/pest-plugin-snapshots": "^1.1" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { "Spatie\\ArrayToXml\\": "src" @@ -6760,7 +7106,7 @@ "xml" ], "support": { - "source": "https://github.com/spatie/array-to-xml/tree/2.17.1" + "source": "https://github.com/spatie/array-to-xml/tree/3.4.0" }, "funding": [ { @@ -6772,7 +7118,7 @@ "type": "github" } ], - "time": "2022-12-26T08:22:07+00:00" + "time": "2024-12-16T12:45:15+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -6932,38 +7278,34 @@ }, { "name": "symfony/config", - "version": "v5.4.46", + "version": "v6.4.22", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "977c88a02d7d3f16904a81907531b19666a08e78" + "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/977c88a02d7d3f16904a81907531b19666a08e78", - "reference": "977c88a02d7d3f16904a81907531b19666a08e78", + "url": "https://api.github.com/repos/symfony/config/zipball/af5917a3b1571f54689e56677a3f06440d2fe4c7", + "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<4.4" + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -6991,7 +7333,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v5.4.46" + "source": "https://github.com/symfony/config/tree/v6.4.22" }, "funding": [ { @@ -7007,7 +7349,7 @@ "type": "tidelift" } ], - "time": "2024-10-30T07:58:02+00:00" + "time": "2025-05-14T06:00:01+00:00" }, { "name": "symfony/console", @@ -7176,20 +7518,20 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.4", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { @@ -7198,7 +7540,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -7223,7 +7565,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -7239,7 +7581,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/dom-crawler", @@ -7403,25 +7745,22 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.4", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", - "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "type": "library", "extra": { "thanks": { @@ -7429,7 +7768,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -7462,7 +7801,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.4" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -7478,30 +7817,29 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.45", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", - "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^5.4|^6.4" + "symfony/process": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -7529,7 +7867,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.45" + "source": "https://github.com/symfony/filesystem/tree/v7.3.0" }, "funding": [ { @@ -7545,7 +7883,7 @@ "type": "tidelift" } ], - "time": "2024-10-22T13:05:35+00:00" + "time": "2024-10-25T15:15:23+00:00" }, { "name": "symfony/finder", @@ -8163,29 +8501,26 @@ }, { "name": "symfony/service-contracts", - "version": "v2.5.4", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", - "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "thanks": { @@ -8193,13 +8528,16 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8226,7 +8564,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -8242,25 +8580,25 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.4.45", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004" + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/fb2c199cf302eb207f8c23e7ee174c1c31a5c004", - "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1|^2|^3" + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -8288,7 +8626,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.45" + "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" }, "funding": [ { @@ -8304,38 +8642,38 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2025-02-24T10:49:57+00:00" }, { "name": "symfony/string", - "version": "v5.4.47", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", - "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", + "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": ">=3.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" + "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", + "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8374,7 +8712,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.47" + "source": "https://github.com/symfony/string/tree/v6.4.21" }, "funding": [ { @@ -8390,7 +8728,7 @@ "type": "tidelift" } ], - "time": "2024-11-10T20:33:58+00:00" + "time": "2025-04-18T15:23:29+00:00" }, { "name": "symfony/yaml", @@ -8581,21 +8919,21 @@ }, { "name": "vimeo/psalm", - "version": "5.26.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0" + "reference": "b8e96bb617bf59382113b1b56cef751f648a7dc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/b8e96bb617bf59382113b1b56cef751f648a7dc9", + "reference": "b8e96bb617bf59382113b1b56cef751f648a7dc9", "shasum": "" }, "require": { - "amphp/amp": "^2.4.2", - "amphp/byte-stream": "^1.5", + "amphp/amp": "^3", + "amphp/byte-stream": "^2", "composer-runtime-api": "^2", "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^2.0 || ^3.0", @@ -8608,26 +8946,24 @@ "ext-simplexml": "*", "ext-tokenizer": "*", "felixfbecker/advanced-json-rpc": "^3.1", - "felixfbecker/language-server-protocol": "^1.5.2", + "felixfbecker/language-server-protocol": "^1.5.3", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.17", - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.17 || ~8.2.4 || ~8.3.0 || ~8.4.0", "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" }, - "conflict": { - "nikic/php-parser": "4.17.0" - }, "provide": { "psalm/psalm": "self.version" }, "require-dev": { - "amphp/phpunit-util": "^2.0", + "amphp/phpunit-util": "^3", "bamarni/composer-bin-plugin": "^1.4", "brianium/paratest": "^6.9", + "dg/bypass-finals": "^1.5", "ext-curl": "*", "mockery/mockery": "^1.5", "nunomaduro/mock-final-classes": "^1.1", @@ -8635,7 +8971,7 @@ "phpstan/phpdoc-parser": "^1.6", "phpunit/phpunit": "^9.6", "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18", + "psalm/plugin-phpunit": "^0.19", "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.6", "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" @@ -8658,7 +8994,9 @@ "dev-2.x": "2.x-dev", "dev-3.x": "3.x-dev", "dev-4.x": "4.x-dev", - "dev-master": "5.x-dev" + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" } }, "autoload": { @@ -8687,7 +9025,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2024-09-08T18:53:08+00:00" + "time": "2025-01-26T12:03:19+00:00" }, { "name": "webmozart/assert", @@ -11230,8 +11568,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.4 || ^8.0" + "php": "^7.4 || ^8.0 || ^8.4" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/plugins/wp-graphql-headless-webhooks/package.json b/plugins/wp-graphql-headless-webhooks/package.json new file mode 100644 index 00000000..025a6ed2 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/package.json @@ -0,0 +1,18 @@ +{ + "name": "@placeholder/wp-graphql-headless-webhooks", + "version": "0.0.1", + "private": true, + "description": "WPGraphQL Headless Webhooks plugin", + "author": "WPEngine Headless OSS Team", + "license": "BSD-3-Clause", + "scripts": { + "dev": "wp-env start && wp-env run cli wp plugin activate wp-graphql wp-graphql-headless-webhooks", + "start": "wp-env start", + "stop": "wp-env stop", + "clean": "wp-env destroy", + "cli": "wp-env run cli wp" + }, + "devDependencies": { + "@wordpress/env": "^8.0.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f6d9344..eab0e4c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,12 @@ importers: specifier: ^8.13.0 version: 8.13.0 + plugins/wp-graphql-headless-webhooks: + devDependencies: + '@wordpress/env': + specifier: ^8.0.0 + version: 8.13.0 + packages: '@isaacs/cliui@8.0.2': From 39b8e5ef05bde1b634040ddcd0991ae05b5f2ad8 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Mon, 9 Jun 2025 16:22:28 -0400 Subject: [PATCH 02/24] Consolidate clean up scripts --- scripts/clean.sh | 19 ++++--------------- scripts/cleanup-wp-env.sh | 10 ---------- 2 files changed, 4 insertions(+), 25 deletions(-) delete mode 100755 scripts/cleanup-wp-env.sh diff --git a/scripts/clean.sh b/scripts/clean.sh index 365fb2bf..fa6778ca 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,21 +1,10 @@ #!/bin/bash -echo "Stopping wp-env containers..." -docker-compose -f ~/.wp-env/*/docker-compose.yml down 2>/dev/null || true - -echo "Removing wp-env containers..." +# Stop and remove all Docker containers with wp-env prefix docker ps -a -q -f name=wp-env | xargs -r docker rm -f -echo "Removing wp-env volumes..." -docker volume ls -q -f name=wp-env | xargs -r docker volume rm -f - -echo "Removing WordPress volumes..." -docker volume ls -q -f name=wordpress | xargs -r docker volume rm -f +# Remove wp-env state directories +rm -rf ~/.wp-env -echo "Removing all test volumes..." -docker volume ls -q -f name=tests-wordpress | xargs -r docker volume rm -f - -echo "Cleaning up networks..." +# Prune Docker networks docker network prune -f - -echo "Cleanup complete! You may need to wait a few seconds before starting wp-env again." diff --git a/scripts/cleanup-wp-env.sh b/scripts/cleanup-wp-env.sh deleted file mode 100755 index bcb0da4b..00000000 --- a/scripts/cleanup-wp-env.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# Stop and remove all Docker containers with wp-env prefix -docker ps -a -q -f name=wp-env | xargs -r docker rm -f - -# Remove wp-env state directories -sudo rm -rf ~/.wp-env/* - -# Prune Docker networks -docker network prune -f From d5cdde9e24f8cf88c1c3fa491b2a83a8714a093b Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Mon, 9 Jun 2025 17:22:04 -0400 Subject: [PATCH 03/24] Hide default CPT since we use our own UI --- .../src/PostTypes/WebhookPostType.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php b/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php index ae78d8ca..23f0c7b3 100644 --- a/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php +++ b/plugins/wp-graphql-headless-webhooks/src/PostTypes/WebhookPostType.php @@ -44,8 +44,8 @@ public static function register_webhook_cpt(): void { 'description' => 'Manages GraphQL Webhooks', 'taxonomies' => [], 'public' => false, - 'show_ui' => true, - 'show_in_menu' => true, + 'show_ui' => false, + 'show_in_menu' => false, 'show_in_admin_bar' => false, 'menu_icon' => 'dashicons-share-alt', 'show_in_nav_menus' => false, From e3e5e5568c7ac4ed41588975b9b9c4de089c9188 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Mon, 9 Jun 2025 17:59:33 -0400 Subject: [PATCH 04/24] Webhooks admin UI --- .../src/Admin/assets/admin.css | 77 +++++++++ .../src/Admin/assets/admin.js | 48 ++++++ .../src/Admin/views/admin-notice.php | 20 +++ .../views/partials/webhook-header-row.php | 23 +++ .../src/Admin/views/webhook-form.php | 113 +++++++++++++ .../src/Admin/views/webhooks-list.php | 78 +++++++++ .../src/Plugin.php | 35 +++- .../src/Repository/WebhookRepository.php | 149 ++++++++++-------- 8 files changed, 476 insertions(+), 67 deletions(-) create mode 100644 plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css create mode 100644 plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js create mode 100644 plugins/wp-graphql-headless-webhooks/src/Admin/views/admin-notice.php create mode 100644 plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php create mode 100644 plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php create mode 100644 plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css new file mode 100644 index 00000000..0845bc07 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -0,0 +1,77 @@ +/** + * Admin styles for WPGraphQL Webhooks + */ + +/* Webhook header rows */ +.webhook-header-row { + margin-bottom: 10px; + display: flex; + gap: 10px; + align-items: center; +} + +.webhook-header-row input[type="text"] { + flex: 1; + min-width: 150px; +} + +.webhook-header-row .remove-header { + flex-shrink: 0; +} + +/* Add some spacing */ +#webhook-headers { + margin-bottom: 10px; +} + +/* Delete link styling */ +.delete-webhook { + color: #a00; +} + +.delete-webhook:hover { + color: #dc3232; +} + +/* Form table adjustments */ +.form-table th { + width: 200px; +} + +/* Webhook list table */ +.wp-list-table.webhooks-table td { + vertical-align: middle; +} + +.wp-list-table.webhooks-table .column-actions { + white-space: nowrap; +} + +/* Empty state */ +.webhooks-empty-state { + text-align: center; + padding: 40px 20px; +} + +.webhooks-empty-state p { + font-size: 16px; + margin-bottom: 20px; +} + +/* Responsive adjustments */ +@media screen and (max-width: 782px) { + .webhook-header-row { + flex-wrap: wrap; + } + + .webhook-header-row input[type="text"] { + width: 100%; + margin-bottom: 5px; + } + + .wp-list-table.webhooks-table { + display: block; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js new file mode 100644 index 00000000..8eb557b5 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js @@ -0,0 +1,48 @@ +/** + * Admin JavaScript for WPGraphQL Webhooks + */ + +(function ($) { + 'use strict'; + + $( document ).ready( + function () { + // Handle adding new header fields + $( '#add-header' ).on( + 'click', + function () { + var headerRow = $( graphqlWebhooksAdmin.headerRowTemplate ); + $( '#webhook-headers' ).append( headerRow ); + } + ); + + // Handle removing header fields + $( document ).on( + 'click', + '.remove-header', + function () { + var headerRows = $( '.webhook-header-row' ); + + // Keep at least one header row + if (headerRows.length > 1) { + $( this ).closest( '.webhook-header-row' ).remove(); + } else { + // Clear the values instead of removing the last row + $( this ).closest( '.webhook-header-row' ).find( 'input' ).val( '' ); + } + } + ); + + // Confirm webhook deletion + $( '.delete-webhook' ).on( + 'click', + function (e) { + if ( ! confirm( graphqlWebhooksAdmin.confirmDelete )) { + e.preventDefault(); + } + } + ); + } + ); + +})( jQuery ); diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/admin-notice.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/admin-notice.php new file mode 100644 index 00000000..4f0de032 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/admin-notice.php @@ -0,0 +1,20 @@ + +
+

+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php new file mode 100644 index 00000000..f10f68a9 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php @@ -0,0 +1,23 @@ + +
+ + + +
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php new file mode 100644 index 00000000..9099e927 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php @@ -0,0 +1,113 @@ + +
+

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

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + +
+ + $value ) : ?> + + + + + +
+ +

+
+ +

+ + + + +

+
+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php new file mode 100644 index 00000000..74be7891 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -0,0 +1,78 @@ + +
+

+ + + +
+ + +
+

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + +
name ); ?> + event ] ) ? $events[ $webhook->event ] : $webhook->event; + echo esc_html( $event_label ); + ?> + url ); ?>method ); ?> + + + + + + +
+ +
diff --git a/plugins/wp-graphql-headless-webhooks/src/Plugin.php b/plugins/wp-graphql-headless-webhooks/src/Plugin.php index a31713b6..db4784d0 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Plugin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Plugin.php @@ -10,10 +10,14 @@ namespace WPGraphQL\Webhooks; use AxeWP\GraphQL\Helper\Helper; +use WPGraphQL\Webhooks\Admin\WebhooksAdmin; use WPGraphQL\Webhooks\Handlers\WebhookHandler; use WPGraphQL\Webhooks\PostTypes\WebhookPostType; use WPGraphQL\Webhooks\Repository\WebhookRepository; use WPGraphQL\Webhooks\Events\WebhookEventManager; +use WPGraphQL\Webhooks\Rest\WebhookEventsEndpoint; +use WPGraphQL\Webhooks\Rest\WebhookTestEndpoint; +use WPGraphQL\Webhooks\Mutation\CreateWebhook; /** * Plugin singleton class. @@ -50,6 +54,13 @@ final class Plugin { */ private WebhookEventManager $event_manager; + /** + * Webhooks admin. + * + * @var WebhooksAdmin + */ + private WebhooksAdmin $admin; + /** * Get singleton instance. * @@ -79,10 +90,28 @@ private function setup(): void { Helper::set_hook_prefix( 'graphql_webhooks' ); WebhookPostType::init(); - $this->repository = new WebhookRepository(); - $this->handler = new WebhookHandler(); + $this->repository = new WebhookRepository(); + $this->handler = new WebhookHandler(); $this->event_manager = new WebhookEventManager( $this->repository, $this->handler ); $this->event_manager->register_hooks(); + + // Register REST endpoints + if ( class_exists( WebhookEventsEndpoint::class ) ) { + $events_endpoint = new WebhookEventsEndpoint( $this->repository ); + add_action( 'rest_api_init', array( $events_endpoint, 'register' ) ); + } + + // Register test endpoint + if ( class_exists( WebhookTestEndpoint::class ) ) { + $test_endpoint = new WebhookTestEndpoint( $this->repository ); + add_action( 'rest_api_init', array( $test_endpoint, 'register' ) ); + } + + // Initialize admin UI + if ( is_admin() ) { + $this->admin = new WebhooksAdmin( $this->repository ); + $this->admin->init(); + } } /** @@ -113,4 +142,4 @@ public function __wakeup(): void { } } -endif; \ No newline at end of file +endif; diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php index e1f102e4..167a58e0 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php +++ b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php @@ -16,7 +16,7 @@ class WebhookRepository implements WebhookRepositoryInterface { /** * Allowed event keys and labels for UI. */ - private array $default_events = [ + private array $default_events = array( 'post_published' => 'Post Published', 'post_updated' => 'Post Updated', 'post_deleted' => 'Post Deleted', @@ -35,111 +35,132 @@ class WebhookRepository implements WebhookRepositoryInterface { 'media_deleted' => 'Media Deleted', 'comment_inserted' => 'Comment Inserted', 'comment_status' => 'Comment Status Changed', - ]; + ); public function get_allowed_events(): array { - return apply_filters('graphql_webhooks_allowed_events', $this->default_events); + return apply_filters( 'graphql_webhooks_allowed_events', $this->default_events ); + } + + /** + * Get allowed HTTP methods for webhooks. + * + * @return array Array of method values and labels. + */ + public function get_allowed_methods(): array { + return apply_filters( + 'graphql_webhooks_allowed_methods', + array( + 'POST' => __( 'POST', 'wp-graphql-headless-webhooks' ), + 'GET' => __( 'GET', 'wp-graphql-headless-webhooks' ), + ) + ); } public function get_all(): array { - $webhooks = []; - - $posts = get_posts([ - 'post_type' => 'graphql_webhook', - 'post_status' => 'publish', - 'numberposts' => -1, - ]); - - foreach ($posts as $post) { - $webhooks[] = $this->mapPostToEntity($post); + $webhooks = array(); + + $posts = get_posts( + array( + 'post_type' => 'graphql_webhook', + 'post_status' => 'publish', + 'numberposts' => -1, + ) + ); + + foreach ( $posts as $post ) { + $webhooks[] = $this->mapPostToEntity( $post ); } return $webhooks; } - public function get(int $id): ?Webhook { - $post = get_post($id); - if (!$post || $post->post_type !== 'graphql_webhook') { + public function get( int $id ): ?Webhook { + $post = get_post( $id ); + if ( ! $post || $post->post_type !== 'graphql_webhook' ) { return null; } - return $this->mapPostToEntity($post); + return $this->mapPostToEntity( $post ); } - public function create(string $name, string $event, string $url, string $method, array $headers): int|WP_Error { - $validation = $this->validate_data($event, $url, $method); - if (is_wp_error($validation)) { + public function create( string $name, string $event, string $url, string $method, array $headers ): int|WP_Error { + $validation = $this->validate_data( $event, $url, $method ); + if ( is_wp_error( $validation ) ) { return $validation; } - $postId = wp_insert_post([ - 'post_title' => $name, - 'post_type' => 'graphql_webhook', - 'post_status' => 'publish', - ], true); + $postId = wp_insert_post( + array( + 'post_title' => $name, + 'post_type' => 'graphql_webhook', + 'post_status' => 'publish', + ), + true + ); - if (is_wp_error($postId)) { + if ( is_wp_error( $postId ) ) { return $postId; } - update_post_meta($postId, '_webhook_event', sanitize_text_field($event)); - update_post_meta($postId, '_webhook_url', esc_url_raw($url)); - update_post_meta($postId, '_webhook_method', strtoupper($method)); - update_post_meta($postId, '_webhook_headers', wp_json_encode($headers)); + update_post_meta( $postId, '_webhook_event', sanitize_text_field( $event ) ); + update_post_meta( $postId, '_webhook_url', esc_url_raw( $url ) ); + update_post_meta( $postId, '_webhook_method', strtoupper( $method ) ); + update_post_meta( $postId, '_webhook_headers', wp_json_encode( $headers ) ); return $postId; } - public function update(int $id, string $name, string $event, string $url, string $method, array $headers): bool|WP_Error { - $post = get_post($id); - if (!$post || $post->post_type !== 'graphql_webhook') { - return new WP_Error('invalid_webhook', __('Webhook not found.', 'wp-graphql-headless-webhooks')); + public function update( int $id, string $name, string $event, string $url, string $method, array $headers ): bool|WP_Error { + $post = get_post( $id ); + if ( ! $post || $post->post_type !== 'graphql_webhook' ) { + return new WP_Error( 'invalid_webhook', __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ) ); } - $validation = $this->validate_data($event, $url, $method); - if (is_wp_error($validation)) { + $validation = $this->validate_data( $event, $url, $method ); + if ( is_wp_error( $validation ) ) { return $validation; } - $postData = [ + $postData = array( 'ID' => $id, - 'post_title' => sanitize_text_field($name), - ]; + 'post_title' => sanitize_text_field( $name ), + ); - $updated = wp_update_post($postData, true); - if (is_wp_error($updated)) { + $updated = wp_update_post( $postData, true ); + if ( is_wp_error( $updated ) ) { return $updated; } - update_post_meta($id, '_webhook_event', sanitize_text_field($event)); - update_post_meta($id, '_webhook_url', esc_url_raw($url)); - update_post_meta($id, '_webhook_method', strtoupper($method)); - update_post_meta($id, '_webhook_headers', wp_json_encode($headers)); + update_post_meta( $id, '_webhook_event', sanitize_text_field( $event ) ); + update_post_meta( $id, '_webhook_url', esc_url_raw( $url ) ); + update_post_meta( $id, '_webhook_method', strtoupper( $method ) ); + update_post_meta( $id, '_webhook_headers', wp_json_encode( $headers ) ); return true; } - public function delete(int $id): bool { - $post = get_post($id); - if (!$post || $post->post_type !== 'graphql_webhook') { + public function delete( int $id ): bool { + $post = get_post( $id ); + if ( ! $post || $post->post_type !== 'graphql_webhook' ) { return false; } - $deleted = wp_delete_post($id, true); + $deleted = wp_delete_post( $id, true ); return (bool) $deleted; } - public function validate_data(string $event, string $url, string $method): bool|WP_Error { - if (!isset($this->get_allowed_events()[$event])) { - return new WP_Error('invalid_event', 'Invalid event type.'); + public function validate_data( string $event, string $url, string $method ): bool|WP_Error { + if ( ! isset( $this->get_allowed_events()[ $event ] ) ) { + return new WP_Error( 'invalid_event', 'Invalid event type.' ); } - if (!filter_var($url, FILTER_VALIDATE_URL)) { - return new WP_Error('invalid_url', 'Invalid URL.'); + if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { + return new WP_Error( 'invalid_url', 'Invalid URL.' ); } - if (!in_array(strtoupper($method), ['GET', 'POST'], true)) { - return new WP_Error('invalid_method', 'Invalid HTTP method.'); + $allowed_methods = array_keys( $this->get_allowed_methods() ); + if ( ! in_array( strtoupper( $method ), $allowed_methods, true ) ) { + return new WP_Error( 'invalid_method', 'Invalid HTTP method.' ); } - return apply_filters('graphql_webhooks_validate_data', true, $event, $url, $method); + return apply_filters( 'graphql_webhooks_validate_data', true, $event, $url, $method ); } /** @@ -149,12 +170,12 @@ public function validate_data(string $event, string $url, string $method): bool| * * @return Webhook The mapped Webhook entity. */ - private function mapPostToEntity(WP_Post $post): Webhook { - $event = get_post_meta($post->ID, '_webhook_event', true); - $url = get_post_meta($post->ID, '_webhook_url', true); - $method = get_post_meta($post->ID, '_webhook_method', true) ?: 'POST'; - $headers = get_post_meta($post->ID, '_webhook_headers', true); - $headers = $headers ? json_decode($headers, true) : []; + private function mapPostToEntity( WP_Post $post ): Webhook { + $event = get_post_meta( $post->ID, '_webhook_event', true ); + $url = get_post_meta( $post->ID, '_webhook_url', true ); + $method = get_post_meta( $post->ID, '_webhook_method', true ) ?: 'POST'; + $headers = get_post_meta( $post->ID, '_webhook_headers', true ); + $headers = $headers ? json_decode( $headers, true ) : array(); return new Webhook( $post->ID, @@ -165,4 +186,4 @@ private function mapPostToEntity(WP_Post $post): Webhook { $headers ); } -} \ No newline at end of file +} From 9cc8176f6529df52dd3b7364a93bd7198770d5cf Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Mon, 9 Jun 2025 18:29:02 -0400 Subject: [PATCH 05/24] Add missing file --- .../src/Admin/WebhooksAdmin.php | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php new file mode 100644 index 00000000..d5ccec09 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -0,0 +1,295 @@ +repository = $repository; + } + + /** + * Initialize admin hooks + */ + public function init(): void { + add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); + add_action( 'admin_init', array( $this, 'handle_actions' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + } + + /** + * Add admin menu + */ + public function add_admin_menu(): void { + add_options_page( + __( 'Webhooks', 'wp-graphql-headless-webhooks' ), + __( 'Webhooks', 'wp-graphql-headless-webhooks' ), + 'manage_options', + self::ADMIN_PAGE_SLUG, + array( $this, 'render_admin_page' ) + ); + } + + /** + * Get admin URL helper + * + * @param array $args Query arguments. + * @return string + */ + public function get_admin_url( array $args = array() ): string { + $defaults = array( 'page' => self::ADMIN_PAGE_SLUG ); + $args = wp_parse_args( $args, $defaults ); + return admin_url( 'options-general.php?' . http_build_query( $args ) ); + } + + /** + * Enqueue admin assets + * + * @param string $hook_suffix Current admin page. + */ + public function enqueue_assets( string $hook_suffix ): void { + if ( 'settings_page_' . self::ADMIN_PAGE_SLUG !== $hook_suffix ) { + return; + } + + $plugin_url = plugin_dir_url( dirname( __DIR__ ) ); + + wp_enqueue_style( + 'wp-graphql-webhooks-admin', + $plugin_url . 'src/Admin/assets/admin.css', + array(), + '1.0.0' + ); + + wp_enqueue_script( + 'wp-graphql-webhooks-admin', + $plugin_url . 'src/Admin/assets/admin.js', + array( 'jquery' ), + '1.0.0', + true + ); + + wp_localize_script( + 'wp-graphql-webhooks-admin', + 'wpGraphQLWebhooks', + array( + 'restUrl' => rest_url( 'graphql-webhooks/v1/' ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'headerTemplate' => $this->get_header_row_template(), + ) + ); + } + + /** + * Get header row template for JavaScript + * + * @return string + */ + private function get_header_row_template(): string { + ob_start(); + include __DIR__ . '/views/partials/webhook-header-row.php'; + return ob_get_clean(); + } + + /** + * Handle admin actions + */ + public function handle_actions(): void { + if ( ! isset( $_GET['page'] ) || self::ADMIN_PAGE_SLUG !== $_GET['page'] ) { + return; + } + + if ( isset( $_POST['action'] ) ) { + if ( 'save_webhook' === $_POST['action'] ) { + $this->handle_webhook_save(); + } + } + + if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] ) { + $this->handle_webhook_delete(); + } + } + + /** + * Verify admin permission + * + * @return bool + */ + private function verify_admin_permission(): bool { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-graphql-headless-webhooks' ) ); + return false; + } + return true; + } + + /** + * Verify nonce + * + * @param string $action Nonce action. + * @param string $nonce_field Nonce field name. + * @return bool + */ + private function verify_nonce( string $action, string $nonce_field = '_wpnonce' ): bool { + if ( ! isset( $_REQUEST[ $nonce_field ] ) || ! wp_verify_nonce( $_REQUEST[ $nonce_field ], $action ) ) { + wp_die( esc_html__( 'Security check failed.', 'wp-graphql-headless-webhooks' ) ); + return false; + } + return true; + } + + /** + * Handle webhook save + */ + private function handle_webhook_save(): void { + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'webhook_save' ) ) { + return; + } + + $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0; + $name = sanitize_text_field( $_POST['webhook_name'] ?? '' ); + $event = sanitize_text_field( $_POST['webhook_event'] ?? '' ); + $url = esc_url_raw( $_POST['webhook_url'] ?? '' ); + $method = sanitize_text_field( $_POST['webhook_method'] ?? 'POST' ); + + // Process headers + $headers = array(); + if ( ! empty( $_POST['webhook_headers']['name'] ) && is_array( $_POST['webhook_headers']['name'] ) ) { + foreach ( $_POST['webhook_headers']['name'] as $index => $header_name ) { + $header_name = sanitize_text_field( $header_name ); + $header_value = sanitize_text_field( $_POST['webhook_headers']['value'][ $index ] ?? '' ); + if ( ! empty( $header_name ) && ! empty( $header_value ) ) { + $headers[ $header_name ] = $header_value; + } + } + } + + if ( $webhook_id > 0 ) { + $result = $this->repository->update( $webhook_id, $name, $event, $url, $method, $headers ); + } else { + $result = $this->repository->create( $name, $event, $url, $method, $headers ); + } + + if ( is_wp_error( $result ) ) { + $redirect_url = $this->get_admin_url( array( 'error' => urlencode( $result->get_error_message() ) ) ); + } else { + $redirect_url = $this->get_admin_url( array( 'updated' => 'true' ) ); + } + + wp_safe_redirect( $redirect_url ); + exit; + } + + /** + * Handle webhook delete + */ + private function handle_webhook_delete(): void { + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'delete_webhook' ) ) { + return; + } + + $webhook_id = isset( $_GET['webhook_id'] ) ? intval( $_GET['webhook_id'] ) : 0; + + if ( $webhook_id > 0 ) { + $this->repository->delete( $webhook_id ); + } + + wp_safe_redirect( $this->get_admin_url( array( 'deleted' => 'true' ) ) ); + exit; + } + + /** + * Render admin page + */ + public function render_admin_page(): void { + $action = $_GET['action'] ?? ''; + $admin = $this; + + // Display admin notices + if ( isset( $_GET['updated'] ) ) { + $message = __( 'Webhook saved successfully.', 'wp-graphql-headless-webhooks' ); + $type = 'success'; + include __DIR__ . '/views/admin-notice.php'; + } + + if ( isset( $_GET['deleted'] ) ) { + $message = __( 'Webhook deleted successfully.', 'wp-graphql-headless-webhooks' ); + $type = 'success'; + include __DIR__ . '/views/admin-notice.php'; + } + + if ( isset( $_GET['error'] ) ) { + $message = sanitize_text_field( $_GET['error'] ); + $type = 'error'; + include __DIR__ . '/views/admin-notice.php'; + } + + // Render appropriate view + if ( 'add' === $action || 'edit' === $action ) { + $webhook_id = isset( $_GET['webhook_id'] ) ? intval( $_GET['webhook_id'] ) : 0; + $webhook = null; + + if ( 'edit' === $action && $webhook_id > 0 ) { + $webhook = $this->repository->get( $webhook_id ); + if ( ! $webhook ) { + wp_die( esc_html__( 'Webhook not found.', 'wp-graphql-headless-webhooks' ) ); + } + } + + $events = $this->repository->get_allowed_events(); + $methods = $this->repository->get_allowed_methods(); + + // Set form variables + $form_title = 'edit' === $action ? __( 'Edit Webhook', 'wp-graphql-headless-webhooks' ) : __( 'Add New Webhook', 'wp-graphql-headless-webhooks' ); + $submit_text = 'edit' === $action ? __( 'Update Webhook', 'wp-graphql-headless-webhooks' ) : __( 'Add Webhook', 'wp-graphql-headless-webhooks' ); + + // Set default values for new webhook + if ( 'add' === $action ) { + $name = ''; + $event = ''; + $url = ''; + $method = 'POST'; + $headers = array(); + } else { + // Extract values from webhook entity + $name = $webhook->name; + $event = $webhook->event; + $url = $webhook->url; + $method = $webhook->method; + $headers = $webhook->headers; + } + + include __DIR__ . '/views/webhook-form.php'; + } else { + $webhooks = $this->repository->get_all(); + include __DIR__ . '/views/webhooks-list.php'; + } + } +} From 660de7016cf50a4f3cdf309adbb8d0dd6eba160b Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Mon, 9 Jun 2025 19:35:49 -0400 Subject: [PATCH 06/24] Fix headers --- .../src/Admin/WebhooksAdmin.php | 17 +++++++++-------- .../src/Admin/assets/admin.js | 4 ++-- .../Admin/views/partials/webhook-header-row.php | 4 ++-- .../src/Admin/views/webhooks-list.php | 9 ++++++++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php index d5ccec09..cbdab529 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -42,6 +42,9 @@ public function init(): void { add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); add_action( 'admin_init', array( $this, 'handle_actions' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + + // Register admin-post.php handlers + add_action( 'admin_post_graphql_webhook_save', array( $this, 'handle_webhook_save' ) ); } /** @@ -103,6 +106,7 @@ public function enqueue_assets( string $hook_suffix ): void { 'restUrl' => rest_url( 'graphql-webhooks/v1/' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'headerTemplate' => $this->get_header_row_template(), + 'confirmDelete' => __( 'Are you sure you want to delete this webhook?', 'wp-graphql-headless-webhooks' ), ) ); } @@ -126,12 +130,7 @@ public function handle_actions(): void { return; } - if ( isset( $_POST['action'] ) ) { - if ( 'save_webhook' === $_POST['action'] ) { - $this->handle_webhook_save(); - } - } - + // Only handle delete action here since save is handled by admin-post.php if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] ) { $this->handle_webhook_delete(); } @@ -168,8 +167,8 @@ private function verify_nonce( string $action, string $nonce_field = '_wpnonce' /** * Handle webhook save */ - private function handle_webhook_save(): void { - if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'webhook_save' ) ) { + public function handle_webhook_save(): void { + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'graphql_webhook_save', 'graphql_webhook_nonce' ) ) { return; } @@ -272,6 +271,7 @@ public function render_admin_page(): void { // Set default values for new webhook if ( 'add' === $action ) { + $webhook_id = 0; $name = ''; $event = ''; $url = ''; @@ -279,6 +279,7 @@ public function render_admin_page(): void { $headers = array(); } else { // Extract values from webhook entity + $webhook_id = $webhook->id; $name = $webhook->name; $event = $webhook->event; $url = $webhook->url; diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js index 8eb557b5..b73648a5 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js @@ -11,7 +11,7 @@ $( '#add-header' ).on( 'click', function () { - var headerRow = $( graphqlWebhooksAdmin.headerRowTemplate ); + var headerRow = $( wpGraphQLWebhooks.headerTemplate ); $( '#webhook-headers' ).append( headerRow ); } ); @@ -37,7 +37,7 @@ $( '.delete-webhook' ).on( 'click', function (e) { - if ( ! confirm( graphqlWebhooksAdmin.confirmDelete )) { + if ( ! confirm( wpGraphQLWebhooks.confirmDelete )) { e.preventDefault(); } } diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php index f10f68a9..da7e0b6a 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/partials/webhook-header-row.php @@ -17,7 +17,7 @@ $value = isset( $value ) ? $value : ''; ?>
- - + +
diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php index 74be7891..fd665e73 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -20,7 +20,7 @@
- +

@@ -36,6 +36,7 @@ + @@ -51,6 +52,12 @@ url ); ?> method ); ?> + + headers ) ? count( $webhook->headers ) : 0; + echo esc_html( $header_count ); + ?> + 'Test Post for Smart Cache', + 'post_content' => 'This is a test post', + 'post_status' => 'publish', + 'post_type' => 'post' + ]); + + // Generate the Relay global ID (base64 encoded "post:ID") + $relay_id = base64_encode( 'post:' . $post_id ); + + do_action( 'graphql_purge', $relay_id, 'post_UPDATE', 'mysite.local/graphql' ); + echo "✓ Triggered: graphql_purge with Relay ID: $relay_id\n"; + echo " Decodes to: post:$post_id\n\n"; + + // Test 3: Simulate a graphql_purge event for post deletion + echo "Test 3: Simulating post DELETE event\n"; + echo "====================================\n"; + + do_action( 'graphql_purge', $relay_id, 'post_DELETE', 'mysite.local/graphql' ); + echo "✓ Triggered: graphql_purge for deletion\n\n"; + + // Test 4: Simulate cache purge nodes event + echo "Test 4: Simulating cache purge nodes event\n"; + echo "==========================================\n"; + + $test_nodes = [ + ['id' => $relay_id, 'type' => 'post'], + ['id' => 'dGVybTox', 'type' => 'term'], // Example term + ]; + + do_action( 'wpgraphql_cache_purge_nodes', 'list:post', $test_nodes ); + echo "✓ Triggered: wpgraphql_cache_purge_nodes with " . count($test_nodes) . " nodes\n\n"; + + // Clean up test post + wp_delete_post( $post_id, true ); + + echo "Test completed!\n\n"; + echo "Check your webhook logs to see if the events were captured.\n"; + echo "Expected webhook events:\n"; + echo "- smart_cache_created (from Test 1)\n"; + echo "- smart_cache_updated (from Test 2)\n"; + echo "- smart_cache_deleted (from Test 3)\n"; + echo "- smart_cache_nodes_purged (from Test 4)\n"; + + echo ''; + + // Add a button to view webhooks + echo '

View Webhooks Admin

'; + + die(); // Stop WordPress execution +}); + +// Add logging to see when Smart Cache events are triggered +add_action( 'graphql_webhooks_before_trigger', function( $event, $payload ) { + if ( strpos( $event, 'smart_cache' ) === 0 ) { + error_log( '[Smart Cache Webhook] Event: ' . $event ); + error_log( '[Smart Cache Webhook] Payload: ' . print_r( $payload, true ) ); + } +}, 10, 2 ); From 35afecfc44724cc8b2438eb0029da677ee1cde7f Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Wed, 11 Jun 2025 10:57:44 -0400 Subject: [PATCH 10/24] Move smart cache into it's own class --- .../src/Events/SmartCacheEventHandler.php | 370 ++++++++++++++++++ .../src/Events/WebhookEventManager.php | 134 +------ 2 files changed, 383 insertions(+), 121 deletions(-) create mode 100644 plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php new file mode 100644 index 00000000..2e36216b --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php @@ -0,0 +1,370 @@ + 'smart_cache_created', + 'update' => 'smart_cache_updated', + 'delete' => 'smart_cache_deleted', + ]; + + /** + * Constructor + * + * @param callable $webhook_trigger_callback Callback to trigger webhooks + */ + public function __construct( callable $webhook_trigger_callback ) { + $this->webhook_trigger_callback = $webhook_trigger_callback; + } + + /** + * Initialize hooks + */ + public function init() { + add_action( 'graphql_purge', [ $this, 'handle_graphql_purge' ], 10, 3 ); + add_action( 'wpgraphql_cache_purge_nodes', [ $this, 'handle_cache_purge_nodes' ], 10, 2 ); + add_action( 'shutdown', [ $this, 'process_buffer' ] ); + } + + /** + * Handle graphql_purge event + * + * @param string $key Cache key being purged + * @param string $event Event type (e.g., post_UPDATE) + * @param string $graphql_endpoint GraphQL endpoint URL + */ + public function handle_graphql_purge( $key, $event, $graphql_endpoint ) { + $parsed = $this->parse_event( $event ); + if ( ! $parsed ) { + return; + } + + $this->buffer_event( $key, $parsed['post_type'], $parsed['action'], $graphql_endpoint ); + } + + /** + * Handle cache purge nodes event + * + * @param string $key Cache key + * @param array $nodes Nodes being purged + */ + public function handle_cache_purge_nodes( $key, $nodes ) { + $payload = [ + 'cache_key' => $key, + 'nodes' => $nodes, + 'nodes_count' => count( $nodes ), + 'timestamp' => current_time( 'c' ), + ]; + + call_user_func( $this->webhook_trigger_callback, 'smart_cache_nodes_purged', $payload ); + } + + /** + * Parse event string into components + * + * @param string $event Event string (e.g., post_UPDATE) + * @return array|null Array with 'post_type' and 'action' keys, or null if invalid + */ + private function parse_event( string $event ): ?array { + $parts = explode( '_', $event ); + if ( count( $parts ) !== 2 ) { + return null; + } + + return [ + 'post_type' => $parts[0], + 'action' => strtolower( $parts[1] ), + ]; + } + + /** + * Buffer an event for consolidated processing + * + * @param string $key Cache key + * @param string $post_type Post type + * @param string $action Action (create, update, delete) + * @param string $graphql_endpoint GraphQL endpoint URL + */ + private function buffer_event( string $key, string $post_type, string $action, string $graphql_endpoint ) { + $buffer_key = "{$post_type}_{$action}"; + + if ( ! isset( $this->buffer[ $buffer_key ] ) ) { + $this->buffer[ $buffer_key ] = [ + 'post_type' => $post_type, + 'action' => $action, + 'graphql_endpoint' => $graphql_endpoint, + 'keys' => [], + 'objects' => [], + ]; + } + + $key_info = $this->analyze_cache_key( $key ); + + // Extract object information if it's a Relay ID + if ( $key_info['type'] === 'relay_id' && isset( $key_info['decoded'] ) ) { + $this->add_object_to_buffer( $buffer_key, $key_info['decoded'], $action ); + } + + $this->buffer[ $buffer_key ]['keys'][] = $key_info; + $this->schedule_processing(); + } + + /** + * Analyze a cache key and determine its type + * + * @param string $key Cache key + * @return array Key information with 'key', 'type', and optionally 'decoded' + */ + private function analyze_cache_key( string $key ): array { + $info = [ + 'key' => $key, + 'type' => $this->classify_key_type( $key ), + ]; + + // Try to decode Relay IDs + if ( $info['type'] === 'relay_id' && class_exists( Relay::class ) ) { + try { + $decoded = Relay::fromGlobalId( $key ); + if ( ! empty( $decoded['type'] ) && ! empty( $decoded['id'] ) ) { + $info['decoded'] = $decoded; + } + } catch ( \Exception $e ) { + // Not a valid Relay ID after all + $info['type'] = 'unknown'; + } + } + + return $info; + } + + /** + * Classify the type of cache key + * + * @param string $key Cache key + * @return string Key type: 'list', 'skipped', 'relay_id', or 'unknown' + */ + private function classify_key_type( string $key ): string { + if ( strpos( $key, 'list:' ) === 0 ) { + return 'list'; + } + + if ( strpos( $key, 'skipped:' ) === 0 ) { + return 'skipped'; + } + + // Assume it might be a Relay ID if it looks like base64 + if ( preg_match( '/^[A-Za-z0-9+\/]+=*$/', $key ) ) { + return 'relay_id'; + } + + return 'unknown'; + } + + /** + * Add object data to buffer + * + * @param string $buffer_key Buffer key + * @param array $decoded Decoded Relay ID data + * @param string $action Action being performed + */ + private function add_object_to_buffer( string $buffer_key, array $decoded, string $action ) { + $object_key = "{$decoded['type']}:{$decoded['id']}"; + + if ( isset( $this->buffer[ $buffer_key ]['objects'][ $object_key ] ) ) { + return; // Already added + } + + $object_data = $this->fetch_object_data( $decoded['type'], (int) $decoded['id'], $action ); + if ( $object_data ) { + $this->buffer[ $buffer_key ]['objects'][ $object_key ] = $object_data; + } + } + + /** + * Fetch object data based on type and ID + * + * @param string $type Object type + * @param int $id Object ID + * @param string $action The action being performed + * @return array|null Object data or null if not found + */ + private function fetch_object_data( string $type, int $id, string $action ): ?array { + // For delete actions, just return minimal data + if ( $action === 'delete' ) { + return [ + 'id' => $id, + 'type' => $type, + 'deleted' => true, + ]; + } + + $fetchers = [ + 'post' => [ $this, 'fetch_post_data' ], + 'term' => [ $this, 'fetch_term_data' ], + 'user' => [ $this, 'fetch_user_data' ], + ]; + + if ( isset( $fetchers[ $type ] ) ) { + return call_user_func( $fetchers[ $type ], $id ); + } + + return null; + } + + /** + * Fetch post data + * + * @param int $id Post ID + * @return array|null + */ + private function fetch_post_data( int $id ): ?array { + $post = get_post( $id ); + if ( ! $post ) { + return null; + } + + return [ + 'id' => $post->ID, + 'title' => $post->post_title, + 'status' => $post->post_status, + 'type' => $post->post_type, + 'url' => get_permalink( $post ), + ]; + } + + /** + * Fetch term data + * + * @param int $id Term ID + * @return array|null + */ + private function fetch_term_data( int $id ): ?array { + $term = get_term( $id ); + if ( ! $term || is_wp_error( $term ) ) { + return null; + } + + return [ + 'id' => $term->term_id, + 'name' => $term->name, + 'taxonomy' => $term->taxonomy, + 'url' => get_term_link( $term ), + ]; + } + + /** + * Fetch user data + * + * @param int $id User ID + * @return array|null + */ + private function fetch_user_data( int $id ): ?array { + $user = get_user_by( 'id', $id ); + if ( ! $user ) { + return null; + } + + return [ + 'id' => $user->ID, + 'login' => $user->user_login, + 'display_name' => $user->display_name, + 'url' => get_author_posts_url( $user->ID ), + ]; + } + + /** + * Schedule buffer processing + */ + private function schedule_processing() { + if ( $this->timer !== false ) { + return; // Already scheduled + } + + $this->timer = wp_schedule_single_event( time() + 1, 'wpgraphql_webhooks_process_smart_cache' ); + add_action( 'wpgraphql_webhooks_process_smart_cache', [ $this, 'process_buffer' ] ); + } + + /** + * Process the buffered events + */ + public function process_buffer() { + if ( empty( $this->buffer ) ) { + return; + } + + foreach ( $this->buffer as $data ) { + $webhook_event = self::EVENT_MAP[ $data['action'] ] ?? null; + if ( ! $webhook_event ) { + continue; + } + + $payload = $this->build_payload( $data ); + call_user_func( $this->webhook_trigger_callback, $webhook_event, $payload ); + } + + $this->buffer = []; + $this->timer = false; + } + + /** + * Build webhook payload from buffered data + * + * @param array $data Buffered event data + * @return array + */ + private function build_payload( array $data ): array { + return [ + 'post_type' => $data['post_type'], + 'action' => $data['action'], + 'graphql_endpoint' => $data['graphql_endpoint'], + 'timestamp' => current_time( 'c' ), + 'cache_keys_purged' => count( $data['keys'] ), + 'objects_affected' => array_values( $data['objects'] ), + 'cache_key_summary' => $this->summarize_keys( $data['keys'] ), + ]; + } + + /** + * Summarize cache keys by type + * + * @param array $keys Array of key information + * @return array Summary with counts by type + */ + private function summarize_keys( array $keys ): array { + $summary = array_fill_keys( [ 'relay_id', 'list', 'skipped', 'unknown' ], 0 ); + + foreach ( $keys as $key_info ) { + $summary[ $key_info['type'] ]++; + } + + return array_filter( $summary ); // Remove zeros + } +} diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php index 7919d85c..f974b5ca 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php +++ b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php @@ -5,6 +5,7 @@ use WPGraphQL\Webhooks\Events\Interfaces\EventManager; use WPGraphQL\Webhooks\Repository\Interfaces\WebhookRepositoryInterface; use WPGraphQL\Webhooks\Handlers\Interfaces\Handler; +use WPGraphQL\Webhooks\Events\SmartCacheEventHandler; /** * Webhook Event Manager @@ -16,15 +17,24 @@ class WebhookEventManager implements EventManager { private WebhookRepositoryInterface $repository; private Handler $handler; + /** + * Smart Cache event handler + * @var SmartCacheEventHandler + */ + private SmartCacheEventHandler $smart_cache_handler; + /** * Constructor * * @param WebhookRepositoryInterface $repository - * @param Handler $sender + * @param Handler $handler */ - public function __construct( WebhookRepositoryInterface $repository, $handler ) { + public function __construct( WebhookRepositoryInterface $repository, Handler $handler ) { $this->repository = $repository; $this->handler = $handler; + + // Initialize Smart Cache handler + $this->smart_cache_handler = new SmartCacheEventHandler( [ $this, 'trigger_webhooks' ] ); } /** @@ -49,8 +59,7 @@ public function register_hooks(): void { add_action( 'transition_comment_status', [ $this, 'on_comment_status' ], 10, 3 ); // Smart Cache integration - add_action( 'graphql_purge', [ $this, 'on_graphql_purge' ], 10, 3 ); - add_action( 'wpgraphql_cache_purge_nodes', [ $this, 'on_cache_purge_nodes' ], 10, 2 ); + $this->smart_cache_handler->init(); } /** @@ -183,121 +192,4 @@ public function on_comment_status( $new_status, $old_status, $comment ) { 'new_status' => $new_status, ] ); } - - /** - * Handle WPGraphQL Smart Cache purge events - * - * @param string $key Cache key being purged - * @param string $event Event type (e.g., post_UPDATE) - * @param string $graphql_endpoint GraphQL endpoint URL - */ - public function on_graphql_purge( $key, $event, $graphql_endpoint ) { - // Parse the event to extract post type and action - $event_parts = explode( '_', $event ); - if ( count( $event_parts ) !== 2 ) { - return; - } - - $post_type = $event_parts[0]; - $action = strtolower( $event_parts[1] ); - - // Map Smart Cache actions to our webhook events - $event_map = [ - 'create' => 'smart_cache_created', - 'update' => 'smart_cache_updated', - 'delete' => 'smart_cache_deleted', - ]; - - if ( ! isset( $event_map[ $action ] ) ) { - return; - } - - $webhook_event = $event_map[ $action ]; - - // Build payload with decoded information - $payload = [ - 'cache_key' => $key, - 'key_type' => $this->classify_cache_key( $key ), - 'post_type' => $post_type, - 'action' => $action, - 'graphql_endpoint' => $graphql_endpoint, - 'timestamp' => current_time( 'c' ), - ]; - - // Try to decode the key if it's a Relay global ID - if ( class_exists( '\GraphQLRelay\Relay' ) ) { - try { - $decoded = \GraphQLRelay\Relay::fromGlobalId( $key ); - if ( ! empty( $decoded['type'] ) && ! empty( $decoded['id'] ) ) { - $payload['decoded_key'] = $decoded; - $payload['object_id'] = absint( $decoded['id'] ); - - // Add object details based on type - if ( $decoded['type'] === 'post' && $action !== 'delete' ) { - $post = get_post( $decoded['id'] ); - if ( $post ) { - $payload['object'] = [ - 'id' => $post->ID, - 'title' => $post->post_title, - 'status' => $post->post_status, - 'type' => $post->post_type, - 'url' => get_permalink( $post ), - ]; - } - } - } - } catch ( \Exception $e ) { - // Not a valid Relay ID, continue without decoding - } - } - - $this->trigger_webhooks( $webhook_event, $payload ); - } - - /** - * Handle WPGraphQL cache purge nodes event - * - * @param string $key Cache key - * @param array $nodes Nodes being purged - */ - public function on_cache_purge_nodes( $key, $nodes ) { - $payload = [ - 'cache_key' => $key, - 'nodes' => $nodes, - 'nodes_count' => count( $nodes ), - 'timestamp' => current_time( 'c' ), - ]; - - $this->trigger_webhooks( 'smart_cache_nodes_purged', $payload ); - } - - /** - * Classify the type of cache key - * - * @param string $key Cache key - * @return string - */ - private function classify_cache_key( string $key ): string { - if ( strpos( $key, 'list:' ) === 0 ) { - return 'list'; - } - - if ( strpos( $key, 'skipped:' ) === 0 ) { - return 'skipped'; - } - - // Check if it's a Relay ID - if ( class_exists( '\GraphQLRelay\Relay' ) ) { - try { - $decoded = \GraphQLRelay\Relay::fromGlobalId( $key ); - if ( ! empty( $decoded['type'] ) && ! empty( $decoded['id'] ) ) { - return 'relay_id'; - } - } catch ( \Exception $e ) { - // Not a valid Relay ID - } - } - - return 'unknown'; - } } \ No newline at end of file From 9f0b2ba2710778d4d49b6dd3c18f0ebf86d8019c Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Wed, 11 Jun 2025 14:36:22 -0400 Subject: [PATCH 11/24] Update SmartCacheEventHandler.php --- .../src/Events/SmartCacheEventHandler.php | 292 ++---------------- 1 file changed, 34 insertions(+), 258 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php index 2e36216b..2f671a4e 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php +++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php @@ -6,6 +6,9 @@ /** * Handles Smart Cache events and consolidates them before triggering webhooks + * + * This is a lightweight handler that simply consolidates multiple cache purge + * events into single webhook triggers to avoid webhook spam. */ class SmartCacheEventHandler { /** @@ -62,58 +65,13 @@ public function init() { * @param string $graphql_endpoint GraphQL endpoint URL */ public function handle_graphql_purge( $key, $event, $graphql_endpoint ) { - $parsed = $this->parse_event( $event ); - if ( ! $parsed ) { - return; - } - - $this->buffer_event( $key, $parsed['post_type'], $parsed['action'], $graphql_endpoint ); - } - - /** - * Handle cache purge nodes event - * - * @param string $key Cache key - * @param array $nodes Nodes being purged - */ - public function handle_cache_purge_nodes( $key, $nodes ) { - $payload = [ - 'cache_key' => $key, - 'nodes' => $nodes, - 'nodes_count' => count( $nodes ), - 'timestamp' => current_time( 'c' ), - ]; - - call_user_func( $this->webhook_trigger_callback, 'smart_cache_nodes_purged', $payload ); - } - - /** - * Parse event string into components - * - * @param string $event Event string (e.g., post_UPDATE) - * @return array|null Array with 'post_type' and 'action' keys, or null if invalid - */ - private function parse_event( string $event ): ?array { $parts = explode( '_', $event ); if ( count( $parts ) !== 2 ) { - return null; + return; } - return [ - 'post_type' => $parts[0], - 'action' => strtolower( $parts[1] ), - ]; - } - - /** - * Buffer an event for consolidated processing - * - * @param string $key Cache key - * @param string $post_type Post type - * @param string $action Action (create, update, delete) - * @param string $graphql_endpoint GraphQL endpoint URL - */ - private function buffer_event( string $key, string $post_type, string $action, string $graphql_endpoint ) { + $post_type = $parts[0]; + $action = strtolower( $parts[1] ); $buffer_key = "{$post_type}_{$action}"; if ( ! isset( $this->buffer[ $buffer_key ] ) ) { @@ -122,194 +80,34 @@ private function buffer_event( string $key, string $post_type, string $action, s 'action' => $action, 'graphql_endpoint' => $graphql_endpoint, 'keys' => [], - 'objects' => [], ]; } - $key_info = $this->analyze_cache_key( $key ); - - // Extract object information if it's a Relay ID - if ( $key_info['type'] === 'relay_id' && isset( $key_info['decoded'] ) ) { - $this->add_object_to_buffer( $buffer_key, $key_info['decoded'], $action ); - } - - $this->buffer[ $buffer_key ]['keys'][] = $key_info; - $this->schedule_processing(); - } - - /** - * Analyze a cache key and determine its type - * - * @param string $key Cache key - * @return array Key information with 'key', 'type', and optionally 'decoded' - */ - private function analyze_cache_key( string $key ): array { - $info = [ - 'key' => $key, - 'type' => $this->classify_key_type( $key ), - ]; + // Just store the key - let webhook consumers decode if needed + $this->buffer[ $buffer_key ]['keys'][] = $key; - // Try to decode Relay IDs - if ( $info['type'] === 'relay_id' && class_exists( Relay::class ) ) { - try { - $decoded = Relay::fromGlobalId( $key ); - if ( ! empty( $decoded['type'] ) && ! empty( $decoded['id'] ) ) { - $info['decoded'] = $decoded; - } - } catch ( \Exception $e ) { - // Not a valid Relay ID after all - $info['type'] = 'unknown'; - } + // Schedule processing if not already scheduled + if ( $this->timer === false ) { + $this->timer = wp_schedule_single_event( time() + 1, 'wpgraphql_webhooks_process_smart_cache' ); + add_action( 'wpgraphql_webhooks_process_smart_cache', [ $this, 'process_buffer' ] ); } - - return $info; } /** - * Classify the type of cache key + * Handle cache purge nodes event - this fires immediately * * @param string $key Cache key - * @return string Key type: 'list', 'skipped', 'relay_id', or 'unknown' - */ - private function classify_key_type( string $key ): string { - if ( strpos( $key, 'list:' ) === 0 ) { - return 'list'; - } - - if ( strpos( $key, 'skipped:' ) === 0 ) { - return 'skipped'; - } - - // Assume it might be a Relay ID if it looks like base64 - if ( preg_match( '/^[A-Za-z0-9+\/]+=*$/', $key ) ) { - return 'relay_id'; - } - - return 'unknown'; - } - - /** - * Add object data to buffer - * - * @param string $buffer_key Buffer key - * @param array $decoded Decoded Relay ID data - * @param string $action Action being performed - */ - private function add_object_to_buffer( string $buffer_key, array $decoded, string $action ) { - $object_key = "{$decoded['type']}:{$decoded['id']}"; - - if ( isset( $this->buffer[ $buffer_key ]['objects'][ $object_key ] ) ) { - return; // Already added - } - - $object_data = $this->fetch_object_data( $decoded['type'], (int) $decoded['id'], $action ); - if ( $object_data ) { - $this->buffer[ $buffer_key ]['objects'][ $object_key ] = $object_data; - } - } - - /** - * Fetch object data based on type and ID - * - * @param string $type Object type - * @param int $id Object ID - * @param string $action The action being performed - * @return array|null Object data or null if not found - */ - private function fetch_object_data( string $type, int $id, string $action ): ?array { - // For delete actions, just return minimal data - if ( $action === 'delete' ) { - return [ - 'id' => $id, - 'type' => $type, - 'deleted' => true, - ]; - } - - $fetchers = [ - 'post' => [ $this, 'fetch_post_data' ], - 'term' => [ $this, 'fetch_term_data' ], - 'user' => [ $this, 'fetch_user_data' ], - ]; - - if ( isset( $fetchers[ $type ] ) ) { - return call_user_func( $fetchers[ $type ], $id ); - } - - return null; - } - - /** - * Fetch post data - * - * @param int $id Post ID - * @return array|null - */ - private function fetch_post_data( int $id ): ?array { - $post = get_post( $id ); - if ( ! $post ) { - return null; - } - - return [ - 'id' => $post->ID, - 'title' => $post->post_title, - 'status' => $post->post_status, - 'type' => $post->post_type, - 'url' => get_permalink( $post ), - ]; - } - - /** - * Fetch term data - * - * @param int $id Term ID - * @return array|null - */ - private function fetch_term_data( int $id ): ?array { - $term = get_term( $id ); - if ( ! $term || is_wp_error( $term ) ) { - return null; - } - - return [ - 'id' => $term->term_id, - 'name' => $term->name, - 'taxonomy' => $term->taxonomy, - 'url' => get_term_link( $term ), - ]; - } - - /** - * Fetch user data - * - * @param int $id User ID - * @return array|null + * @param array $nodes Nodes being purged */ - private function fetch_user_data( int $id ): ?array { - $user = get_user_by( 'id', $id ); - if ( ! $user ) { - return null; - } - - return [ - 'id' => $user->ID, - 'login' => $user->user_login, - 'display_name' => $user->display_name, - 'url' => get_author_posts_url( $user->ID ), + public function handle_cache_purge_nodes( $key, $nodes ) { + // This event provides the actual nodes being purged, so we can fire immediately + $payload = [ + 'cache_key' => $key, + 'nodes' => $nodes, + 'timestamp' => current_time( 'c' ), ]; - } - /** - * Schedule buffer processing - */ - private function schedule_processing() { - if ( $this->timer !== false ) { - return; // Already scheduled - } - - $this->timer = wp_schedule_single_event( time() + 1, 'wpgraphql_webhooks_process_smart_cache' ); - add_action( 'wpgraphql_webhooks_process_smart_cache', [ $this, 'process_buffer' ] ); + call_user_func( $this->webhook_trigger_callback, 'smart_cache_nodes_purged', $payload ); } /** @@ -326,45 +124,23 @@ public function process_buffer() { continue; } - $payload = $this->build_payload( $data ); + // Simple payload with just the essential information + $payload = [ + 'post_type' => $data['post_type'], + 'action' => $data['action'], + 'graphql_endpoint' => $data['graphql_endpoint'], + 'cache_keys' => array_unique( $data['keys'] ), // Remove duplicates + 'cache_keys_count' => count( array_unique( $data['keys'] ) ), + 'timestamp' => current_time( 'c' ), + ]; + + // Let webhook consumers decode the keys if they need to + // They already have access to WPGraphQL and can use Relay::fromGlobalId() + call_user_func( $this->webhook_trigger_callback, $webhook_event, $payload ); } $this->buffer = []; $this->timer = false; } - - /** - * Build webhook payload from buffered data - * - * @param array $data Buffered event data - * @return array - */ - private function build_payload( array $data ): array { - return [ - 'post_type' => $data['post_type'], - 'action' => $data['action'], - 'graphql_endpoint' => $data['graphql_endpoint'], - 'timestamp' => current_time( 'c' ), - 'cache_keys_purged' => count( $data['keys'] ), - 'objects_affected' => array_values( $data['objects'] ), - 'cache_key_summary' => $this->summarize_keys( $data['keys'] ), - ]; - } - - /** - * Summarize cache keys by type - * - * @param array $keys Array of key information - * @return array Summary with counts by type - */ - private function summarize_keys( array $keys ): array { - $summary = array_fill_keys( [ 'relay_id', 'list', 'skipped', 'unknown' ], 0 ); - - foreach ( $keys as $key_info ) { - $summary[ $key_info['type'] ]++; - } - - return array_filter( $summary ); // Remove zeros - } } From 3ed40e5ec4b0aba3e2a2a7ed91c294c7f7df2cce Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Wed, 11 Jun 2025 14:45:59 -0400 Subject: [PATCH 12/24] Remove type --- .../src/Events/SmartCacheEventHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php index 2f671a4e..6bf6eb70 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php +++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php @@ -42,9 +42,9 @@ class SmartCacheEventHandler { /** * Constructor * - * @param callable $webhook_trigger_callback Callback to trigger webhooks + * @param $webhook_trigger_callback Callback to trigger webhooks */ - public function __construct( callable $webhook_trigger_callback ) { + public function __construct( $webhook_trigger_callback ) { $this->webhook_trigger_callback = $webhook_trigger_callback; } From ded1de5b385fd3b830c3c0ca176318ed8eeff314 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Wed, 11 Jun 2025 14:51:52 -0400 Subject: [PATCH 13/24] Add wpgraphql-smart-cache to plugin env --- plugins/wp-graphql-headless-webhooks/.wp-env.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/wp-graphql-headless-webhooks/.wp-env.json b/plugins/wp-graphql-headless-webhooks/.wp-env.json index 51dc3958..2b276711 100644 --- a/plugins/wp-graphql-headless-webhooks/.wp-env.json +++ b/plugins/wp-graphql-headless-webhooks/.wp-env.json @@ -3,6 +3,7 @@ "phpVersion": "8.0", "plugins": [ "https://downloads.wordpress.org/plugin/wp-graphql.latest-stable.zip", + "https://downloads.wordpress.org/plugin/wpgraphql-smart-cache.latest-stable.zip", "." ], "port": 8889, From 64b9324d666c7feaee721fec81ba3a2317ae155a Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Wed, 11 Jun 2025 16:36:21 -0400 Subject: [PATCH 14/24] Use native WordPress UI patterns --- .../src/Admin/WebhooksAdmin.php | 10 +- .../src/Admin/assets/admin.css | 42 +++-- .../src/Admin/views/webhooks-list.php | 153 ++++++++++++++---- 3 files changed, 145 insertions(+), 60 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php index ea3af8b3..c2514160 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -51,7 +51,8 @@ public function init(): void { * Add admin menu */ public function add_admin_menu(): void { - add_options_page( + add_submenu_page( + 'graphiql-ide', __( 'Webhooks', 'wp-graphql-headless-webhooks' ), __( 'Webhooks', 'wp-graphql-headless-webhooks' ), 'manage_options', @@ -69,7 +70,7 @@ public function add_admin_menu(): void { public function get_admin_url( array $args = array() ): string { $defaults = array( 'page' => self::ADMIN_PAGE_SLUG ); $args = wp_parse_args( $args, $defaults ); - return admin_url( 'options-general.php?' . http_build_query( $args ) ); + return admin_url( 'admin.php?' . http_build_query( $args ) ); } /** @@ -78,7 +79,7 @@ public function get_admin_url( array $args = array() ): string { * @param string $hook_suffix Current admin page. */ public function enqueue_assets( string $hook_suffix ): void { - if ( 'settings_page_' . self::ADMIN_PAGE_SLUG !== $hook_suffix ) { + if ( 'graphql_page_' . self::ADMIN_PAGE_SLUG !== $hook_suffix ) { return; } @@ -264,6 +265,9 @@ public function render_admin_page(): void { $events = $this->repository->get_allowed_events(); $methods = $this->repository->get_allowed_methods(); + + // Convert simple array to associative array for the form + $methods = array_combine($methods, $methods); // Set form variables $form_title = 'edit' === $action ? __( 'Edit Webhook', 'wp-graphql-headless-webhooks' ) : __( 'Add New Webhook', 'wp-graphql-headless-webhooks' ); diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css index 0845bc07..9408673f 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -24,38 +24,36 @@ margin-bottom: 10px; } -/* Delete link styling */ -.delete-webhook { - color: #a00; -} - -.delete-webhook:hover { - color: #dc3232; -} - /* Form table adjustments */ .form-table th { width: 200px; } -/* Webhook list table */ -.wp-list-table.webhooks-table td { - vertical-align: middle; +/* Test webhook link */ +.test-webhook { + cursor: pointer; } -.wp-list-table.webhooks-table .column-actions { - white-space: nowrap; +/* Webhooks table column widths */ +.wp-list-table.webhooks-table .column-event { + width: 15%; } -/* Empty state */ -.webhooks-empty-state { - text-align: center; - padding: 40px 20px; +.wp-list-table.webhooks-table .column-method { + width: 80px; } -.webhooks-empty-state p { - font-size: 16px; - margin-bottom: 20px; +.wp-list-table.webhooks-table .column-headers { + width: 15%; +} + +/* Prevent URL wrapping */ +.wp-list-table.webhooks-table td code { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } /* Responsive adjustments */ @@ -68,7 +66,7 @@ width: 100%; margin-bottom: 5px; } - + .wp-list-table.webhooks-table { display: block; overflow-x: auto; diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php index fd665e73..90e18b0f 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -20,66 +20,149 @@
-
+ -
-

- - - +
+

+ + +

+

+ + + +

- +
+
+ + + +
+
+
+ +
- - - - - - + + + + + - + - - + + + + + + + + + +
+ + + + + + + + + +
name ); ?> + + + name ); ?> + + +
+ + + + | + + + + + | + + + + + + +
+ +
event ] ) ? $events[ $webhook->event ] : $webhook->event; echo esc_html( $event_label ); ?> url ); ?>method ); ?> - headers ) ? count( $webhook->headers ) : 0; - echo esc_html( $header_count ); - ?> + method ) ); ?> - " style="cursor: help;"> + + headers ) ? count( $webhook->headers ) : 0; + if ( $header_count > 0 ) { + $header_names = array_keys( $webhook->headers ); + foreach ( $header_names as $header_name ) { + echo esc_html( $header_name ) . '
'; + } + } else { + echo ''; + } ?> - " class="button button-small"> - - - - -
+ + + + + + + + + +
+ +
+
+ + + +
+
+
From a615ec481a39c05c7bf1e93c437199cd32f02d09 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Wed, 11 Jun 2025 16:59:05 -0400 Subject: [PATCH 15/24] Remove test file --- .../tests/smart-cache-test.php | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 plugins/wp-graphql-headless-webhooks/tests/smart-cache-test.php diff --git a/plugins/wp-graphql-headless-webhooks/tests/smart-cache-test.php b/plugins/wp-graphql-headless-webhooks/tests/smart-cache-test.php deleted file mode 100644 index 9771bf17..00000000 --- a/plugins/wp-graphql-headless-webhooks/tests/smart-cache-test.php +++ /dev/null @@ -1,95 +0,0 @@ -Smart Cache Webhook Integration Test'; - echo '
';
-
-    // Test 1: Simulate a graphql_purge event for post creation
-    echo "Test 1: Simulating post CREATE event\n";
-    echo "=====================================\n";
-    
-    do_action( 'graphql_purge', 'list:post', 'post_CREATE', 'mysite.local/graphql' );
-    echo "✓ Triggered: graphql_purge with list:post key\n\n";
-
-    // Test 2: Simulate a graphql_purge event for post update with Relay ID
-    echo "Test 2: Simulating post UPDATE event with Relay ID\n";
-    echo "==================================================\n";
-    
-    // Create a test post first
-    $post_id = wp_insert_post([
-        'post_title' => 'Test Post for Smart Cache',
-        'post_content' => 'This is a test post',
-        'post_status' => 'publish',
-        'post_type' => 'post'
-    ]);
-    
-    // Generate the Relay global ID (base64 encoded "post:ID")
-    $relay_id = base64_encode( 'post:' . $post_id );
-    
-    do_action( 'graphql_purge', $relay_id, 'post_UPDATE', 'mysite.local/graphql' );
-    echo "✓ Triggered: graphql_purge with Relay ID: $relay_id\n";
-    echo "  Decodes to: post:$post_id\n\n";
-
-    // Test 3: Simulate a graphql_purge event for post deletion
-    echo "Test 3: Simulating post DELETE event\n";
-    echo "====================================\n";
-    
-    do_action( 'graphql_purge', $relay_id, 'post_DELETE', 'mysite.local/graphql' );
-    echo "✓ Triggered: graphql_purge for deletion\n\n";
-
-    // Test 4: Simulate cache purge nodes event
-    echo "Test 4: Simulating cache purge nodes event\n";
-    echo "==========================================\n";
-    
-    $test_nodes = [
-        ['id' => $relay_id, 'type' => 'post'],
-        ['id' => 'dGVybTox', 'type' => 'term'], // Example term
-    ];
-    
-    do_action( 'wpgraphql_cache_purge_nodes', 'list:post', $test_nodes );
-    echo "✓ Triggered: wpgraphql_cache_purge_nodes with " . count($test_nodes) . " nodes\n\n";
-
-    // Clean up test post
-    wp_delete_post( $post_id, true );
-    
-    echo "Test completed!\n\n";
-    echo "Check your webhook logs to see if the events were captured.\n";
-    echo "Expected webhook events:\n";
-    echo "- smart_cache_created (from Test 1)\n";
-    echo "- smart_cache_updated (from Test 2)\n";
-    echo "- smart_cache_deleted (from Test 3)\n";
-    echo "- smart_cache_nodes_purged (from Test 4)\n";
-    
-    echo '
'; - - // Add a button to view webhooks - echo '

View Webhooks Admin

'; - - die(); // Stop WordPress execution -}); - -// Add logging to see when Smart Cache events are triggered -add_action( 'graphql_webhooks_before_trigger', function( $event, $payload ) { - if ( strpos( $event, 'smart_cache' ) === 0 ) { - error_log( '[Smart Cache Webhook] Event: ' . $event ); - error_log( '[Smart Cache Webhook] Payload: ' . print_r( $payload, true ) ); - } -}, 10, 2 ); From 8623cf31a3f21a3194cef5578f4558acd2bafd9f Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Wed, 11 Jun 2025 17:41:26 -0400 Subject: [PATCH 16/24] Refactor test webhook --- .../src/Admin/WebhooksAdmin.php | 156 +++++++++++- .../src/Admin/assets/admin.css | 20 ++ .../src/Admin/views/webhooks-list.php | 229 +++++++++++++----- 3 files changed, 338 insertions(+), 67 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php index c2514160..298f0577 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -45,6 +45,9 @@ public function init(): void { // Register admin-post.php handlers add_action( 'admin_post_graphql_webhook_save', array( $this, 'handle_webhook_save' ) ); + + // Register AJAX handlers + add_action( 'wp_ajax_test_webhook', array( $this, 'handle_test_webhook' ) ); } /** @@ -79,7 +82,8 @@ public function get_admin_url( array $args = array() ): string { * @param string $hook_suffix Current admin page. */ public function enqueue_assets( string $hook_suffix ): void { - if ( 'graphql_page_' . self::ADMIN_PAGE_SLUG !== $hook_suffix ) { + // Only load on our admin page - check if we're on the webhooks page + if ( ! isset( $_GET['page'] ) || $_GET['page'] !== self::ADMIN_PAGE_SLUG ) { return; } @@ -106,6 +110,7 @@ public function enqueue_assets( string $hook_suffix ): void { array( 'restUrl' => rest_url( 'graphql-webhooks/v1/' ), 'nonce' => wp_create_nonce( 'wp_rest' ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'headerTemplate' => $this->get_header_row_template(), 'confirmDelete' => __( 'Are you sure you want to delete this webhook?', 'wp-graphql-headless-webhooks' ), ) @@ -225,6 +230,155 @@ public function handle_webhook_delete(): void { exit; } + /** + * Handle webhook test via AJAX + */ + public function handle_test_webhook(): void { + // Verify nonce + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'wp_rest' ) ) { + wp_send_json_error( __( 'Security check failed.', 'wp-graphql-headless-webhooks' ) ); + } + + // Verify permissions + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( __( 'Insufficient permissions.', 'wp-graphql-headless-webhooks' ) ); + } + + $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0; + if ( ! $webhook_id ) { + wp_send_json_error( __( 'Invalid webhook ID.', 'wp-graphql-headless-webhooks' ) ); + } + + // Get the webhook + $webhook = $this->repository->get( $webhook_id ); + if ( ! $webhook ) { + wp_send_json_error( __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ) ); + } + + // Create test payload based on the event type + $test_payload = $this->get_test_payload_for_event( $webhook->event ); + + // Send the webhook using a synchronous request for testing + $args = [ + 'headers' => $webhook->headers ?: [ 'Content-Type' => 'application/json' ], + 'timeout' => 10, + 'blocking' => true, // We need blocking for test to get response + ]; + + $payload = apply_filters( 'graphql_webhooks_payload', $test_payload, $webhook ); + + if ( strtoupper( $webhook->method ) === 'GET' ) { + $url = add_query_arg( $payload, $webhook->url ); + $args['method'] = 'GET'; + } else { + $url = $webhook->url; + $args['method'] = strtoupper( $webhook->method ); + $args['body'] = wp_json_encode( $payload ); + if ( empty( $args['headers']['Content-Type'] ) ) { + $args['headers']['Content-Type'] = 'application/json'; + } + } + + $response = wp_remote_request( $url, $args ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( $response->get_error_message() ); + } + + // Get response details + $response_code = wp_remote_retrieve_response_code( $response ); + $response_body = wp_remote_retrieve_body( $response ); + + // Strip HTML tags from response body to remove any links + $response_body = wp_strip_all_tags( $response_body ); + + $message = sprintf( + __( 'Webhook sent successfully!\n\nResponse Code: %d\nResponse Body: %s', 'wp-graphql-headless-webhooks' ), + $response_code, + substr( $response_body, 0, 200 ) // Limit response body to 200 chars + ); + + wp_send_json_success( array( 'message' => $message ) ); + } + + /** + * Get test payload for a specific event + * + * @param string $event Event type. + * @return array + */ + private function get_test_payload_for_event( string $event ): array { + $base_payload = array( + 'event' => $event, + 'timestamp' => current_time( 'mysql' ), + 'test' => true, + 'test_mode' => true, // Additional flag to clearly indicate test mode + 'message' => 'This is a TEST webhook payload - no production data was affected', + ); + + // Add event-specific test data + switch ( $event ) { + case 'smart_cache_created': + case 'smart_cache_updated': + case 'smart_cache_deleted': + $base_payload['data'] = array( + 'key' => 'test:post:999999', + 'action' => str_replace( 'smart_cache_', '', $event ), + 'purge_url' => home_url( '/test-graphql-endpoint' ), + 'test_note' => 'This is test data - no actual cache was purged', + ); + break; + + case 'smart_cache_nodes_purged': + $base_payload['data'] = array( + 'key' => 'test:list:post', + 'nodes' => array( + array( 'id' => 'test_node_1', 'type' => 'post' ), + array( 'id' => 'test_node_2', 'type' => 'term' ), + ), + 'test_note' => 'This is test data - no actual nodes were purged', + ); + break; + + case 'post.published': + case 'post.updated': + case 'post.deleted': + $base_payload['data'] = array( + 'id' => 999999, + 'title' => 'Test Post (Not Real)', + 'status' => 'test', + 'author' => 0, + 'test_note' => 'This is test data - no actual post exists', + ); + break; + + case 'user.created': + $base_payload['data'] = array( + 'id' => 999999, + 'username' => 'test_webhook_user', + 'email' => 'test@webhook.local', + 'role' => 'test', + 'test_note' => 'This is test data - no actual user exists', + ); + break; + + default: + $base_payload['data'] = array( + 'message' => 'This is a test webhook payload', + 'test_note' => 'This is test data for event: ' . $event, + ); + break; + } + + /** + * Filter the test payload for webhook testing + * + * @param array $base_payload The test payload data + * @param string $event The event type being tested + */ + return apply_filters( 'graphql_webhooks_test_payload', $base_payload, $event ); + } + /** * Render admin page */ diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css index 9408673f..6f93a65e 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -19,6 +19,11 @@ flex-shrink: 0; } +.webhook-header-row button.remove-header:hover { + background: #d63638; + color: #fff; +} + /* Add some spacing */ #webhook-headers { margin-bottom: 10px; @@ -34,6 +39,21 @@ cursor: pointer; } +/* Test webhook button states */ +.test-webhook.testing { + color: #3858e9; + cursor: not-allowed; + pointer-events: none; +} + +.test-webhook.success { + color: #00a32a; +} + +.test-webhook.error { + color: #d63638; +} + /* Webhooks table column widths */ .wp-list-table.webhooks-table .column-event { width: 15%; diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php index 90e18b0f..9880f4eb 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -14,44 +14,50 @@ exit; } ?> +

+ +
- -
-

- - -

-

- - - -

-
- -
-
- - - + + + +
+

-
-
- - + + + + +
- - - - - - -
+ @@ -71,25 +77,25 @@
+ - + name ); ?>
- + - | + | - | + | - + @@ -98,41 +104,30 @@
- event ] ) ? $events[ $webhook->event ] : $webhook->event; - echo esc_html( $event_label ); - ?> + + event ); ?> + method ) ); ?> - url; - $truncated_url = strlen($url) > 50 ? substr($url, 0, 50) . '...' : $url; - ?> - + + url ); ?> - headers ) ? count( $webhook->headers ) : 0; - if ( $header_count > 0 ) { - $header_names = array_keys( $webhook->headers ); - foreach ( $header_names as $header_name ) { - echo esc_html( $header_name ) . '
'; - } - } else { - echo ''; - } - ?> +
+ headers ) ) : ?> + headers as $key => $value ) : ?> +
+ + + +
+ @@ -152,17 +147,119 @@
-
- - +

+ - +

-
+
+ +
+

+

+ + + +

+ + From 5bef67c72567ca2cbe80c20b5ad0b525ad66662f Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 12 Jun 2025 10:41:19 -0400 Subject: [PATCH 17/24] Improve response notification --- .../src/Admin/WebhooksAdmin.php | 18 ++-- .../src/Admin/assets/admin.css | 47 ++++++++ .../src/Admin/assets/admin.js | 95 +++++++++++++++++ .../src/Admin/views/webhooks-list.php | 100 +----------------- 4 files changed, 154 insertions(+), 106 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php index 298f0577..15e977dd 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -289,16 +289,16 @@ public function handle_test_webhook(): void { $response_code = wp_remote_retrieve_response_code( $response ); $response_body = wp_remote_retrieve_body( $response ); - // Strip HTML tags from response body to remove any links + // Strip HTML tags and decode entities from response body $response_body = wp_strip_all_tags( $response_body ); - - $message = sprintf( - __( 'Webhook sent successfully!\n\nResponse Code: %d\nResponse Body: %s', 'wp-graphql-headless-webhooks' ), - $response_code, - substr( $response_body, 0, 200 ) // Limit response body to 200 chars - ); - - wp_send_json_success( array( 'message' => $message ) ); + $response_body = html_entity_decode( $response_body, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + + // Send structured response data + wp_send_json_success( array( + 'message' => __( 'Webhook sent successfully!', 'wp-graphql-headless-webhooks' ), + 'response_code' => $response_code, + 'response_body' => substr( $response_body, 0, 200 ) // Limit response body to 200 chars + ) ); } /** diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css index 6f93a65e..84b4400f 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -76,6 +76,53 @@ max-width: 100%; } +/* Empty state */ +.webhooks-empty-state { + text-align: center; + padding: 50px 20px; +} + +.webhooks-empty-state h2 { + font-size: 24px; + font-weight: 400; + margin: 0 0 10px; +} + +.webhooks-empty-state p { + font-size: 16px; + color: #646970; + margin: 0 0 20px; +} + +/* Webhook test response styling */ +.webhook-test-details { + margin: 0; +} + +.webhook-test-details p { + margin: 0.5em 0; +} + +.webhook-test-details .notice-message-body { + margin-top: 1em; +} + +.webhook-test-details pre.webhook-response-body { + background: #f6f7f7; + border: 1px solid #c3c4c7; + border-radius: 4px; + padding: 12px; + margin: 0.5em 0 0; + overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; + font-family: Consolas, Monaco, monospace; + font-size: 13px; + line-height: 1.5; + max-height: 200px; + overflow-y: auto; +} + /* Responsive adjustments */ @media screen and (max-width: 782px) { .webhook-header-row { diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js index b73648a5..253d8463 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js @@ -42,6 +42,101 @@ } } ); + + // Handle test webhook clicks using event delegation + $( document ).on( + 'click', + '.test-webhook', + function (e) { + e.preventDefault(); + + var $link = $( this ); + var webhookId = $link.data( 'webhook-id' ); + var originalText = $link.text(); + + // Prevent multiple clicks + if ($link.hasClass( 'testing' )) { + return false; + } + + // Generate unique ID for this test result + var resultId = 'webhook-test-result-' + webhookId + '-' + Date.now(); + + // Update UI to show testing + $link.text( 'Testing...' ).addClass( 'testing' ).css( 'pointer-events', 'none' ); + + // Send test request + $.ajax({ + url: wpGraphQLWebhooks.ajaxUrl, + type: 'POST', + data: { + action: 'test_webhook', + webhook_id: webhookId, + nonce: wpGraphQLWebhooks.nonce + }, + success: function(response) { + if (response.success) { + $link.text( 'Success' ); + if (response.data && response.data.message) { + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td' ).length; + var message = response.data.message; + if (response.data.response_code) { + message += ' (Response: ' + response.data.response_code + ')'; + } + var $resultRow = $( '

' + message + '

' ); + $row.after( $resultRow ); + + // Remove this specific message after 7 seconds + setTimeout(function() { + $( '#' + resultId ).fadeOut(function() { + $( this ).remove(); + }); + }, 7000); + } + } else { + $link.text( 'Failed' ); + var error = response.data || 'Unknown error'; + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td' ).length; + var $resultRow = $( '

Test failed: ' + error + '

' ); + $row.after( $resultRow ); + + // Remove this specific message after 7 seconds + setTimeout(function() { + $( '#' + resultId ).fadeOut(function() { + $( this ).remove(); + }); + }, 7000); + } + + // Reset button after 3 seconds + setTimeout(function() { + $link.text( originalText ).removeClass( 'testing' ).css( 'pointer-events', 'auto' ); + }, 3000); + }, + error: function(xhr, status, error) { + $link.text( 'Error' ); + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td' ).length; + var $resultRow = $( '

Test error: ' + error + '

' ); + $row.after( $resultRow ); + + // Remove this specific message after 7 seconds + setTimeout(function() { + $( '#' + resultId ).fadeOut(function() { + $( this ).remove(); + }); + }, 7000); + + // Reset button after 3 seconds + setTimeout(function() { + $link.text( originalText ).removeClass( 'testing' ).css( 'pointer-events', 'auto' ); + }, 3000); + } + }); + } + ); } ); diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php index 9880f4eb..80a3048f 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -79,13 +79,13 @@ - + name ); ?>
- + | @@ -95,7 +95,7 @@ | - + @@ -169,97 +169,3 @@
- - From b5044e000daf03abadfc75ad1513d76b0e21aa03 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 12 Jun 2025 11:12:41 -0400 Subject: [PATCH 18/24] Fix ability to delete --- .../src/Admin/WebhooksAdmin.php | 6 +- .../wp-graphql-headless-webhooks-test.xml | 180 ++++++++++++++++++ 2 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 plugins/wp-graphql-headless-webhooks/tests/wp-graphql-headless-webhooks-test.xml diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php index 15e977dd..22039f32 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -216,12 +216,12 @@ public function handle_webhook_save(): void { * Handle webhook delete */ public function handle_webhook_delete(): void { - if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'delete_webhook' ) ) { + $webhook_id = isset( $_GET['webhook_id'] ) ? intval( $_GET['webhook_id'] ) : 0; + + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'delete_webhook_' . $webhook_id ) ) { return; } - $webhook_id = isset( $_GET['webhook_id'] ) ? intval( $_GET['webhook_id'] ) : 0; - if ( $webhook_id > 0 ) { $this->repository->delete( $webhook_id ); } diff --git a/plugins/wp-graphql-headless-webhooks/tests/wp-graphql-headless-webhooks-test.xml b/plugins/wp-graphql-headless-webhooks/tests/wp-graphql-headless-webhooks-test.xml new file mode 100644 index 00000000..bbe5a3c2 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/tests/wp-graphql-headless-webhooks-test.xml @@ -0,0 +1,180 @@ + + + + + wp-graphql-headless-webhooks + http://localhost:8889 + + Thu, 12 Jun 2025 14:45:36 +0000 + en-US + 1.2 + + + + 1adminwordpress@example.comadmin + + + https://wordpress.org/?v=6.8.1 + + + Smart Cache - Post Created + + Wed, 11 Jun 2025 20:12:11 +0000 + admin + + + + + + 2025-06-11 20:12:11 + 2025-06-11 20:12:11 + 2025-06-11 20:34:52 + 2025-06-11 20:34:52 + closed + closed + smart-cache-post-created + publish + 0 + 0 + graphql_webhook + + 0 + + _webhook_event + smart_cache_created + + + _webhook_url + https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc + + + _webhook_method + POST + + + _webhook_headers + {"X-Webhook-Event":"smart_cache_created","X-Another-Webhook-Event":"main_event"} + + + + Smart Cache Updated Webhook + + Wed, 11 Jun 2025 20:37:47 +0000 + admin + + + + + + 2025-06-11 20:37:47 + 2025-06-11 20:37:47 + 2025-06-11 20:37:47 + 2025-06-11 20:37:47 + closed + closed + smart-cache-updated-webhook + publish + 0 + 0 + graphql_webhook + + 0 + + _webhook_event + smart_cache_updated + + + _webhook_url + https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc + + + _webhook_method + POST + + + _webhook_headers + {"X-Webhook-Event":"smart_cache_updated"} + + + + Smart Cache - Post Deleted + + Wed, 11 Jun 2025 20:38:23 +0000 + admin + + + + + + 2025-06-11 20:38:23 + 2025-06-11 20:38:23 + 2025-06-12 13:54:33 + 2025-06-12 13:54:33 + closed + closed + smart-cache-post-deleted + publish + 0 + 0 + graphql_webhook + + 0 + + _webhook_event + smart_cache_deleted + + + _webhook_url + https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc + + + _webhook_method + POST + + + _webhook_headers + {"X-Webhook-Event":"smart_cache_deleted"} + + + + Smart Cache Nodes Purged Webhook + + Wed, 11 Jun 2025 20:39:46 +0000 + admin + + + + + + 2025-06-11 20:39:46 + 2025-06-11 20:39:46 + 2025-06-11 20:39:46 + 2025-06-11 20:39:46 + closed + closed + smart-cache-nodes-purged-webhook + publish + 0 + 0 + graphql_webhook + + 0 + + _webhook_event + smart_cache_nodes_purged + + + _webhook_url + https://webhook.site/649f6fda-2f6b-48c1-b85e-f016285083dc + + + _webhook_method + POST + + + _webhook_headers + {"X-Webhook-Event":"smart_cache_nodes_purged"} + + + + \ No newline at end of file From 166eae69059f8f68d3473e7a6ce3cbcf8fc2332d Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 12 Jun 2025 11:15:45 -0400 Subject: [PATCH 19/24] Add native WordPress filters --- .../src/Admin/WebhooksAdmin.php | 55 +++- .../src/Admin/views/webhooks-list.php | 236 ++++++++++-------- 2 files changed, 190 insertions(+), 101 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php index 22039f32..1d1b4534 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -45,6 +45,7 @@ public function init(): void { // Register admin-post.php handlers add_action( 'admin_post_graphql_webhook_save', array( $this, 'handle_webhook_save' ) ); + add_action( 'admin_post_graphql_webhook_bulk_delete', array( $this, 'handle_bulk_delete' ) ); // Register AJAX handlers add_action( 'wp_ajax_test_webhook', array( $this, 'handle_test_webhook' ) ); @@ -230,6 +231,43 @@ public function handle_webhook_delete(): void { exit; } + /** + * Handle bulk delete action + */ + public function handle_bulk_delete(): void { + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'bulk_delete_webhooks' ) ) { + return; + } + + $bulk_action = $_POST['bulk_action'] ?? $_POST['bulk_action2'] ?? ''; + $webhook_ids = $_POST['webhook_ids'] ?? array(); + + if ( 'delete' === $bulk_action && ! empty( $webhook_ids ) ) { + $deleted_count = 0; + foreach ( $webhook_ids as $webhook_id ) { + $webhook_id = intval( $webhook_id ); + if ( $webhook_id > 0 && $this->repository->delete( $webhook_id ) ) { + $deleted_count++; + } + } + + $redirect_args = array(); + if ( $deleted_count > 0 ) { + $redirect_args['deleted'] = 'true'; + $redirect_args['count'] = $deleted_count; + } else { + $redirect_args['error'] = __( 'Failed to delete webhooks.', 'wp-graphql-headless-webhooks' ); + } + + wp_safe_redirect( $this->get_admin_url( $redirect_args ) ); + exit; + } + + // If no valid action, redirect back + wp_safe_redirect( $this->get_admin_url() ); + exit; + } + /** * Handle webhook test via AJAX */ @@ -394,8 +432,21 @@ public function render_admin_page(): void { } if ( isset( $_GET['deleted'] ) ) { - $message = __( 'Webhook deleted successfully.', 'wp-graphql-headless-webhooks' ); - $type = 'success'; + $count = isset( $_GET['count'] ) ? intval( $_GET['count'] ) : 1; + if ( $count > 1 ) { + $message = sprintf( + _n( + '%d webhook deleted successfully.', + '%d webhooks deleted successfully.', + $count, + 'wp-graphql-headless-webhooks' + ), + $count + ); + } else { + $message = __( 'Webhook deleted successfully.', 'wp-graphql-headless-webhooks' ); + } + $type = 'success'; include __DIR__ . '/views/admin-notice.php'; } diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php index 80a3048f..b11673ba 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -54,110 +54,148 @@ - - - - - - - - - - - - + + + + +
+
+ + + +
+
+ +
- - - - - - - - - -
+ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - -
- - - name ); ?> - - -
- - - - | - - - - - | - - - - - - -
- -
- event ); ?> - - method ) ); ?> - - url ); ?> + + + - headers ) ) : ?> - headers as $key => $value ) : ?> -
- - - - +
+ + + + + + + + + +
+ + + + + + name ); ?> + + +
+ + + + | + + + + + | + + + + + + +
+ +
+ event ); ?> + + method ) ); ?> + + url ); ?> + + headers ) ) : ?> + headers as $key => $value ) : ?> +
+ + + + +
+ + + + + + + + + + + +
- - - - - - - - - -
- -
-
-

- -

+ + + +
+
+ + + +
+
+

+ +

+
-
+

From a593781ee99d2f80d633340af5e3200f8bb4d11d Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 12 Jun 2025 11:27:43 -0400 Subject: [PATCH 20/24] Leverage native WP styles --- .../src/Admin/assets/admin.css | 141 ++++++++++++++---- .../src/Admin/views/webhooks-list.php | 106 +++++-------- 2 files changed, 149 insertions(+), 98 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css index 84b4400f..2d33ce15 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -34,64 +34,151 @@ width: 200px; } -/* Test webhook link */ -.test-webhook { - cursor: pointer; +/* Webhooks table column widths */ +.wp-list-table.webhooks .column-cb { + width: 2.2em; } -/* Test webhook button states */ -.test-webhook.testing { - color: #3858e9; - cursor: not-allowed; - pointer-events: none; +.wp-list-table.webhooks .column-name { + width: 25%; } -.test-webhook.success { - color: #00a32a; +.wp-list-table.webhooks .column-event { + width: 15%; } -.test-webhook.error { - color: #d63638; +.wp-list-table.webhooks .column-method { + width: 10%; } -/* Webhooks table column widths */ -.wp-list-table.webhooks-table .column-event { +.wp-list-table.webhooks .column-url { + width: auto; +} + +.wp-list-table.webhooks .column-headers { width: 15%; } -.wp-list-table.webhooks-table .column-method { - width: 80px; +/* Improve table styling */ +.wp-list-table.webhooks { + margin-top: 0.5em; } -.wp-list-table.webhooks-table .column-headers { - width: 15%; +.wp-list-table.webhooks th:not(.check-column), +.wp-list-table.webhooks td:not(.check-column) { + vertical-align: middle; +} + +/* Checkbox column specific styling */ +.wp-list-table.webhooks .check-column { + vertical-align: top; + padding-top: 8px; } -/* Prevent URL wrapping */ -.wp-list-table.webhooks-table td code { +/* Style webhook name as primary column */ +.wp-list-table.webhooks .column-name strong { + display: block; + margin-bottom: 0.2em; + font-size: 14px; +} + +.wp-list-table.webhooks .row-actions { + font-size: 13px; +} + +/* Method column styling */ +.wp-list-table.webhooks .column-method { + text-align: center; +} + +.wp-list-table.webhooks .column-method strong { display: inline-block; + padding: 3px 8px; + background: #f0f0f1; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +/* URL column styling */ +.wp-list-table.webhooks .column-url code { + display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; + font-size: 12px; + background: transparent; + padding: 0; } -/* Empty state */ +/* Headers column styling */ +.wp-list-table.webhooks .column-headers { + color: #50575e; + font-size: 12px; +} + +.wp-list-table.webhooks .column-headers code { + background: #f0f0f1; + padding: 2px 4px; + border-radius: 2px; + font-size: 11px; +} + +/* Bulk actions bar */ +.tablenav .actions { + padding: 8px 0; +} + +/* Empty state improvements */ .webhooks-empty-state { text-align: center; - padding: 50px 20px; + padding: 60px 20px; + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + margin-top: 20px; } .webhooks-empty-state h2 { - font-size: 24px; + font-size: 21px; font-weight: 400; - margin: 0 0 10px; + margin: 0 0 0.5em; + line-height: 1.3; } .webhooks-empty-state p { - font-size: 16px; - color: #646970; - margin: 0 0 20px; + font-size: 14px; + color: #50575e; + margin: 0 0 1.5em; + line-height: 1.5; +} + +.webhooks-empty-state .button-primary { + font-size: 14px; + padding: 6px 14px; + height: auto; +} + +/* Test webhook link */ +.test-webhook { + cursor: pointer; +} + +/* Test webhook button states */ +.test-webhook.testing { + color: #3858e9; + cursor: not-allowed; + pointer-events: none; +} + +.test-webhook.success { + color: #00a32a; +} + +.test-webhook.error { + color: #d63638; } /* Webhook test response styling */ diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php index b11673ba..95e9273e 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -53,10 +53,18 @@ - + +
+

+

+ + + +
+
+ -
@@ -67,42 +75,29 @@
+
- +
- - - - - + + + + + - - - + - - @@ -153,24 +146,13 @@ - - - - - + + + + +
- - + - - - - - - - - - -
- - + + name ); ?> @@ -129,22 +124,20 @@ - event ); ?> - + event ); ?> method ) ); ?> - url ); ?> + + url ); ?> + headers ) ) : ?> - headers as $key => $value ) : ?> -
+ headers as $header => $value ) : ?> +
- +
- - + - - - - - - - - - -
@@ -184,26 +166,8 @@
-
-

- -

-
+
- -
-

-

- - - -

-
From ce91587ad8afcaa48d91dc00564e5b2d6f6f5f78 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Thu, 12 Jun 2025 11:50:24 -0400 Subject: [PATCH 21/24] Only use smart cache events by default --- .../src/Repository/WebhookRepository.php | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php index 7b27144e..b391e17b 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php +++ b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php @@ -24,25 +24,7 @@ class WebhookRepository implements WebhookRepositoryInterface { * @var array */ private $default_events = [ - 'post_published' => 'Post Published', - 'post_updated' => 'Post Updated', - 'post_deleted' => 'Post Deleted', - 'post_meta_change' => 'Post Meta Changed', - 'term_created' => 'Term Created', - 'term_assigned' => 'Term Assigned to Post', - 'term_unassigned' => 'Term Unassigned from Post', - 'term_deleted' => 'Term Deleted', - 'term_meta_change' => 'Term Meta Changed', - 'user_created' => 'User Created', - 'user_assigned' => 'User Assigned as Author', - 'user_deleted' => 'User Deleted', - 'user_reassigned' => 'User Author Reassigned', - 'media_uploaded' => 'Media Uploaded', - 'media_updated' => 'Media Updated', - 'media_deleted' => 'Media Deleted', - 'comment_inserted' => 'Comment Inserted', - 'comment_status' => 'Comment Status Changed', - // Smart Cache events + // Smart Cache events only by default 'smart_cache_created' => 'Smart Cache - Content Created', 'smart_cache_updated' => 'Smart Cache - Content Updated', 'smart_cache_deleted' => 'Smart Cache - Content Deleted', From 9e8b16b60ab2b2427efba499d3a1071ed5f85253 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Sat, 14 Jun 2025 19:25:47 -0400 Subject: [PATCH 22/24] top align content within webhooks table --- .../wp-graphql-headless-webhooks/src/Admin/assets/admin.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css index 2d33ce15..f9c1db3c 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -66,13 +66,13 @@ .wp-list-table.webhooks th:not(.check-column), .wp-list-table.webhooks td:not(.check-column) { - vertical-align: middle; + vertical-align: top; } /* Checkbox column specific styling */ .wp-list-table.webhooks .check-column { vertical-align: top; - padding-top: 8px; + padding: 8px 0 0 3px; } /* Style webhook name as primary column */ From 8c4b4c4dd224bdc446faa6009de1f18a78f48002 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Mon, 16 Jun 2025 10:36:15 -0400 Subject: [PATCH 23/24] Setup example --- examples/next/webhooks-isr/.wp-env.json | 2 +- .../example-app/src/pages/api/revalidate.js | 78 ++++--- .../example-app/src/pages/index.js | 211 ++++++++++-------- .../src/Events/SmartCacheEventHandler.php | 47 +++- .../src/Events/WebhookEventManager.php | 21 ++ .../src/Plugin.php | 32 +++ 6 files changed, 265 insertions(+), 126 deletions(-) diff --git a/examples/next/webhooks-isr/.wp-env.json b/examples/next/webhooks-isr/.wp-env.json index a15c20dd..6924b5d6 100644 --- a/examples/next/webhooks-isr/.wp-env.json +++ b/examples/next/webhooks-isr/.wp-env.json @@ -1,5 +1,5 @@ { - "phpVersion": "7.4", + "phpVersion": "8.0", "plugins": [ "https://github.com/wp-graphql/wp-graphql/releases/latest/download/wp-graphql.zip", "https://downloads.wordpress.org/plugin/code-snippets.3.6.8.zip", diff --git a/examples/next/webhooks-isr/example-app/src/pages/api/revalidate.js b/examples/next/webhooks-isr/example-app/src/pages/api/revalidate.js index 9423ab0a..2767a70d 100644 --- a/examples/next/webhooks-isr/example-app/src/pages/api/revalidate.js +++ b/examples/next/webhooks-isr/example-app/src/pages/api/revalidate.js @@ -1,48 +1,74 @@ import crypto from 'crypto'; export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ message: 'Method not allowed' }); + } + try { - console.log('[Webhook] Received revalidation request'); + // Log the full webhook payload + console.log('\n========== WEBHOOK RECEIVED =========='); + console.log('Timestamp:', new Date().toISOString()); + console.log('Headers:', JSON.stringify(req.headers, null, 2)); + console.log('Payload:', JSON.stringify(req.body, null, 2)); + console.log('=====================================\n'); + // Verify secret const secret = req.headers['x-webhook-secret']; const expectedSecret = process.env.WEBHOOK_REVALIDATE_SECRET; - - console.log('[Webhook] Secret from header:', secret ? 'Provided' : 'Missing'); - console.log('[Webhook] Expected secret is set:', expectedSecret ? 'Yes' : 'No'); - - // Securely compare secrets - if ( - !secret || - !expectedSecret || - secret.length !== expectedSecret.length || - !crypto.timingSafeEqual(Buffer.from(secret), Buffer.from(expectedSecret)) - ) { - console.warn('[Webhook] Invalid secret token'); - return res.status(401).json({ message: 'Invalid token' }); + + console.log('[Webhook] Secret header present:', !!secret); + console.log('[Webhook] Expected secret present:', !!expectedSecret); + + if (!secret || !expectedSecret) { + console.log('[Webhook] Missing secret configuration'); + return res.status(401).json({ message: 'Unauthorized' }); } - console.log('[Webhook] Secret token validated successfully'); - if (req.method !== 'POST') { - return res.status(405).json({ message: 'Method Not Allowed' }); + // Use timing-safe comparison + const secretBuffer = Buffer.from(secret); + const expectedBuffer = Buffer.from(expectedSecret); + + if (secretBuffer.length !== expectedBuffer.length || + !crypto.timingSafeEqual(secretBuffer, expectedBuffer)) { + console.log('[Webhook] Invalid secret'); + return res.status(401).json({ message: 'Unauthorized' }); } - const body = req.body; - console.log('[Webhook] Request body parsed:', body); + console.log('[Webhook] Secret validated successfully'); - const path = body.path; + // Extract path from various possible locations in the payload + let path = req.body?.path || + req.body?.post?.path || + req.body?.post?.uri || + req.body?.uri || + req.query?.path; - if (!path || typeof path !== 'string') { - console.warn('[Webhook] Invalid or missing path in request body'); + if (!path) { + console.log('[Webhook] No path found in payload'); return res.status(400).json({ message: 'Path is required' }); } - console.log('[Webhook] Path to revalidate:', path); + + console.log('\n========== ISR REVALIDATION =========='); + console.log('Path to revalidate:', path); + console.log('Starting at:', new Date().toISOString()); + // Perform revalidation await res.revalidate(path); - console.log('[Webhook] Successfully revalidated path:', path); + + console.log('✅ SUCCESS: Revalidated path:', path); + console.log('Completed at:', new Date().toISOString()); + console.log('=====================================\n'); - return res.status(200).json({ message: `Revalidated path: ${path}` }); + return res.status(200).json({ + message: `Revalidated path: ${path}`, + revalidatedAt: new Date().toISOString(), + info: 'Only this specific page was regenerated, not the entire site' + }); } catch (error) { - console.error('[Webhook] Revalidation error:', error); + console.error('\n========== REVALIDATION ERROR =========='); + console.error('Error:', error); + console.error('=======================================\n'); return res.status(500).json({ message: 'Error during revalidation' }); } } diff --git a/examples/next/webhooks-isr/example-app/src/pages/index.js b/examples/next/webhooks-isr/example-app/src/pages/index.js index 313112bb..b7594ca0 100644 --- a/examples/next/webhooks-isr/example-app/src/pages/index.js +++ b/examples/next/webhooks-isr/example-app/src/pages/index.js @@ -1,102 +1,123 @@ -import Image from "next/image"; +import Link from "next/link"; +import { getApolloClient } from "@/lib/client"; +import { gql } from "@apollo/client"; -export default function Home() { +export default function Home({ posts }) { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/pages/index.js - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
-
- - Vercel logomark - Deploy now - - - Read our docs - +
+
+

WordPress Webhooks ISR Demo

+

+ This example demonstrates Next.js ISR (Incremental Static Regeneration) with WordPress webhooks. + When you update a post in WordPress, the webhook triggers revalidation of the specific page. +

+ +

Recent Posts

+ + {posts && posts.length > 0 ? ( +
+ {posts.map((edge) => { + const post = edge.node; + return ( +
+ +

+ {post.title} +

+ +

+ By {post.author?.node?.name || 'Unknown'} on {new Date(post.date).toLocaleDateString()} +

+ {post.excerpt && ( +
+ )} + + Read more → + +
+ ); + })} +
+ ) : ( +

No posts found. Create some posts in WordPress admin.

+ )} + +
+

Quick Links:

+
-
); } + +const GET_POSTS = gql` + query GetPosts { + posts(first: 10) { + edges { + node { + id + title + uri + date + excerpt + author { + node { + name + } + } + } + } + } + } +`; + +export async function getStaticProps() { + try { + const { data } = await getApolloClient().query({ + query: GET_POSTS, + }); + + return { + props: { + posts: data?.posts?.edges || [], + }, + revalidate: 60, // ISR: revalidate every 60 seconds + }; + } catch (error) { + console.error("Error fetching posts:", error); + return { + props: { + posts: [], + }, + revalidate: 60, + }; + } +} diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php index 6bf6eb70..29f5f4b3 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php +++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php @@ -124,18 +124,57 @@ public function process_buffer() { continue; } - // Simple payload with just the essential information + // Decode cache keys to get actual post IDs + $decoded_items = []; + $paths = []; + + foreach ( $data['keys'] as $key ) { + // WPGraphQL cache keys are base64 encoded global IDs + // Format is typically: base64("post:123") or base64("page:456") + $decoded = base64_decode( $key ); + if ( $decoded && strpos( $decoded, ':' ) !== false ) { + list( $type, $id ) = explode( ':', $decoded, 2 ); + + // Get the post data + if ( $type === 'post' || $type === 'page' ) { + $post = get_post( $id ); + if ( $post ) { + $uri = str_replace( home_url(), '', get_permalink( $post ) ); + $path = '/' . trim( $uri, '/' ) . '/'; + + $decoded_items[] = [ + 'id' => $post->ID, + 'title' => $post->post_title, + 'slug' => $post->post_name, + 'uri' => $uri, + 'path' => $path, + 'type' => $post->post_type, + 'status' => $post->post_status, + ]; + + $paths[] = $path; + } + } + } + } + + // Enhanced payload with decoded post data $payload = [ 'post_type' => $data['post_type'], 'action' => $data['action'], 'graphql_endpoint' => $data['graphql_endpoint'], - 'cache_keys' => array_unique( $data['keys'] ), // Remove duplicates + 'cache_keys' => array_unique( $data['keys'] ), // Original cache keys 'cache_keys_count' => count( array_unique( $data['keys'] ) ), + 'posts' => $decoded_items, // Decoded post data + 'paths' => array_unique( $paths ), // Paths for ISR revalidation 'timestamp' => current_time( 'c' ), ]; - // Let webhook consumers decode the keys if they need to - // They already have access to WPGraphQL and can use Relay::fromGlobalId() + // If there's only one post, add it as the primary post/path for compatibility + if ( count( $decoded_items ) === 1 ) { + $payload['post'] = $decoded_items[0]; + $payload['path'] = $decoded_items[0]['path']; + } call_user_func( $this->webhook_trigger_callback, $webhook_event, $payload ); } diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php index f974b5ca..a95d80ff 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php +++ b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php @@ -76,6 +76,27 @@ private function trigger_webhooks( string $event, array $payload ): void { return; } + // Enrich payload with post data if post_id is present + if ( isset( $payload['post_id'] ) ) { + $post = get_post( $payload['post_id'] ); + if ( $post ) { + // Add post data to payload + $payload['post'] = [ + 'id' => $post->ID, + 'title' => $post->post_title, + 'slug' => $post->post_name, + 'uri' => str_replace( home_url(), '', get_permalink( $post ) ), + 'status' => $post->post_status, + 'type' => $post->post_type, + 'date' => $post->post_date, + 'modified' => $post->post_modified, + ]; + + // Add the path for Next.js revalidation + $payload['path'] = '/' . trim( $payload['post']['uri'], '/' ) . '/'; + } + } + do_action( 'graphql_webhooks_before_trigger', $event, $payload ); foreach ( $this->repository->get_all() as $webhook ) { diff --git a/plugins/wp-graphql-headless-webhooks/src/Plugin.php b/plugins/wp-graphql-headless-webhooks/src/Plugin.php index 5a5d63be..8dfdc0f2 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Plugin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Plugin.php @@ -95,6 +95,38 @@ private function setup(): void { $this->event_manager = new WebhookEventManager( $this->repository, $this->handler ); $this->event_manager->register_hooks(); + // Add standard WordPress events to the allowed events list + add_filter( 'graphql_webhooks_allowed_events', function( $events ) { + // Add standard WordPress post events + $events['post_published'] = 'Post Published'; + $events['post_updated'] = 'Post Updated'; + $events['post_deleted'] = 'Post Deleted'; + $events['post_meta_change'] = 'Post Meta Changed'; + + // Add term events + $events['term_created'] = 'Term Created'; + $events['term_assigned'] = 'Term Assigned'; + $events['term_unassigned'] = 'Term Unassigned'; + $events['term_deleted'] = 'Term Deleted'; + + // Add user events + $events['user_created'] = 'User Created'; + $events['user_deleted'] = 'User Deleted'; + $events['user_assigned'] = 'User Assigned to Post'; + $events['user_reassigned'] = 'User Reassigned on Post'; + + // Add media events + $events['media_uploaded'] = 'Media Uploaded'; + $events['media_updated'] = 'Media Updated'; + $events['media_deleted'] = 'Media Deleted'; + + // Add comment events + $events['comment_inserted'] = 'Comment Added'; + $events['comment_status'] = 'Comment Status Changed'; + + return $events; + } ); + // Register REST endpoints if ( class_exists( WebhookEventsEndpoint::class ) ) { $events_endpoint = new WebhookEventsEndpoint( $this->repository ); From 4b41af3870e5e36c571a43c9f9938b08429af8e8 Mon Sep 17 00:00:00 2001 From: Joe Fusco Date: Mon, 16 Jun 2025 10:52:46 -0400 Subject: [PATCH 24/24] Dismissable admin notice --- .../src/Admin/WebhooksAdmin.php | 141 +++++++++++++++--- .../src/Admin/assets/admin.css | 75 +++++++++- .../src/Admin/assets/admin.js | 94 +++++++++--- 3 files changed, 265 insertions(+), 45 deletions(-) diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php index 1d1b4534..97daeb31 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -317,25 +317,59 @@ public function handle_test_webhook(): void { } } + // Log the test request for debugging + error_log( sprintf( + '[Webhook Test] Sending %s request to %s with payload: %s', + $args['method'], + $url, + wp_json_encode( $payload ) + ) ); + $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { - wp_send_json_error( $response->get_error_message() ); + wp_send_json_error( array( + 'message' => sprintf( + __( 'Connection failed: %s', 'wp-graphql-headless-webhooks' ), + $response->get_error_message() + ), + 'error_code' => $response->get_error_code(), + 'error_data' => $response->get_error_data(), + ) ); } // Get response details $response_code = wp_remote_retrieve_response_code( $response ); $response_body = wp_remote_retrieve_body( $response ); + $response_headers = wp_remote_retrieve_headers( $response ); + + // Try to parse JSON response + $parsed_body = json_decode( $response_body, true ); + if ( json_last_error() === JSON_ERROR_NONE ) { + $response_body_display = wp_json_encode( $parsed_body, JSON_PRETTY_PRINT ); + } else { + // Strip HTML tags and decode entities from response body + $response_body = wp_strip_all_tags( $response_body ); + $response_body = html_entity_decode( $response_body, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + $response_body_display = $response_body; + } + + // Determine success based on response code + $is_success = $response_code >= 200 && $response_code < 300; - // Strip HTML tags and decode entities from response body - $response_body = wp_strip_all_tags( $response_body ); - $response_body = html_entity_decode( $response_body, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + // Build detailed response message + $message = $is_success + ? __( 'Webhook test completed successfully!', 'wp-graphql-headless-webhooks' ) + : sprintf( __( 'Webhook test failed with status %d', 'wp-graphql-headless-webhooks' ), $response_code ); // Send structured response data wp_send_json_success( array( - 'message' => __( 'Webhook sent successfully!', 'wp-graphql-headless-webhooks' ), + 'message' => $message, + 'success' => $is_success, 'response_code' => $response_code, - 'response_body' => substr( $response_body, 0, 200 ) // Limit response body to 200 chars + 'response_body' => substr( $response_body_display, 0, 500 ), // Limit response body to 500 chars + 'response_headers' => $response_headers->getAll(), + 'test_payload' => $payload, // Include what was sent for debugging ) ); } @@ -378,26 +412,95 @@ private function get_test_payload_for_event( string $event ): array { ); break; - case 'post.published': - case 'post.updated': - case 'post.deleted': - $base_payload['data'] = array( - 'id' => 999999, - 'title' => 'Test Post (Not Real)', - 'status' => 'test', - 'author' => 0, - 'test_note' => 'This is test data - no actual post exists', + case 'post_published': + case 'post_updated': + case 'post_deleted': + // Match the actual webhook payload structure + $test_post_id = 999999; + $base_payload = array( + 'post_id' => $test_post_id, + 'post' => array( + 'id' => $test_post_id, + 'title' => 'Test Post - Hello World', + 'slug' => 'test-post-hello-world', + 'uri' => '/test-post-hello-world/', + 'status' => 'publish', + 'type' => 'post', + 'date' => current_time( 'mysql' ), + 'modified' => current_time( 'mysql' ), + ), + 'path' => '/test-post-hello-world/', + 'test' => true, + 'test_mode' => true, + 'message' => 'This is a TEST webhook payload - no actual post was affected', ); break; - case 'user.created': + case 'post_meta_change': + $base_payload['post_id'] = 999999; + $base_payload['meta_key'] = 'test_meta_key'; + $base_payload['test_note'] = 'This is test data - no actual meta was changed'; + break; + + case 'term_created': + case 'term_assigned': + case 'term_unassigned': + case 'term_deleted': + $base_payload['term_id'] = 999999; + $base_payload['taxonomy'] = 'category'; + if ( $event === 'term_assigned' || $event === 'term_unassigned' ) { + $base_payload['object_id'] = 888888; + } + $base_payload['test_note'] = 'This is test data - no actual term was affected'; + break; + + case 'user_created': + case 'user_deleted': + $base_payload['user_id'] = 999999; $base_payload['data'] = array( - 'id' => 999999, 'username' => 'test_webhook_user', 'email' => 'test@webhook.local', - 'role' => 'test', - 'test_note' => 'This is test data - no actual user exists', + 'role' => 'subscriber', ); + $base_payload['test_note'] = 'This is test data - no actual user was affected'; + break; + + case 'user_assigned': + case 'user_reassigned': + $base_payload['post_id'] = 999999; + $base_payload['author_id'] = 888888; + if ( $event === 'user_reassigned' ) { + $base_payload['old_author_id'] = 777777; + $base_payload['new_author_id'] = 888888; + } + $base_payload['test_note'] = 'This is test data - no actual assignment was made'; + break; + + case 'media_uploaded': + case 'media_updated': + case 'media_deleted': + $base_payload['post_id'] = 999999; + $base_payload['post'] = array( + 'id' => 999999, + 'title' => 'Test Media File', + 'slug' => 'test-media-file', + 'uri' => '/test-media-file/', + 'status' => 'inherit', + 'type' => 'attachment', + 'date' => current_time( 'mysql' ), + 'modified' => current_time( 'mysql' ), + ); + $base_payload['path'] = '/test-media-file/'; + $base_payload['test_note'] = 'This is test data - no actual media was affected'; + break; + + case 'comment_inserted': + case 'comment_status': + $base_payload['comment_id'] = 999999; + if ( $event === 'comment_status' ) { + $base_payload['new_status'] = 'approved'; + } + $base_payload['test_note'] = 'This is test data - no actual comment was affected'; break; default: diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css index f9c1db3c..392f4f96 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -164,30 +164,95 @@ /* Test webhook link */ .test-webhook { cursor: pointer; + text-decoration: none; } /* Test webhook button states */ .test-webhook.testing { - color: #3858e9; + color: #666; cursor: not-allowed; pointer-events: none; } .test-webhook.success { - color: #00a32a; + color: #46b450; } .test-webhook.error { - color: #d63638; + color: #dc3232; +} + +/* Test webhook result display */ +.webhook-test-result td { + padding: 0 !important; + background: transparent !important; +} + +.webhook-test-result .notice { + margin: 10px 0; + border-left-width: 4px; } -/* Webhook test response styling */ .webhook-test-details { + padding: 5px 0; margin: 0; } .webhook-test-details p { - margin: 0.5em 0; + margin: 5px 0; +} + +.webhook-test-details .status-success { + color: #46b450; + font-weight: bold; +} + +.webhook-test-details .status-error { + color: #dc3232; + font-weight: bold; +} + +.webhook-test-details details { + margin: 10px 0; + border: 1px solid #ddd; + border-radius: 3px; + padding: 5px; + background: #f9f9f9; +} + +.webhook-test-details summary { + cursor: pointer; + font-weight: 600; + padding: 5px; + user-select: none; +} + +.webhook-test-details summary:hover { + background: #f1f1f1; +} + +.webhook-test-details pre { + margin: 10px 0 5px; + padding: 10px; + background: #fff; + border: 1px solid #e1e1e1; + border-radius: 3px; + overflow-x: auto; + font-size: 12px; + line-height: 1.4; + white-space: pre-wrap; + word-wrap: break-word; +} + +.webhook-test-details pre.webhook-test-payload { + max-height: 300px; + overflow-y: auto; +} + +.webhook-test-details pre.webhook-response-body { + background: #f7f7f7; + max-height: 200px; + overflow-y: auto; } .webhook-test-details .notice-message-body { diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js index 253d8463..d453635e 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js @@ -76,62 +76,114 @@ }, success: function(response) { if (response.success) { - $link.text( 'Success' ); - if (response.data && response.data.message) { + $link.text( 'Success' ).removeClass( 'testing' ).addClass( 'success' ); + if (response.data) { var $row = $link.closest( 'tr' ); var colspan = $row.find( 'td' ).length; - var message = response.data.message; + + // Build detailed result HTML + var resultHtml = '
'; + resultHtml += ''; + resultHtml += '

' + response.data.message + '

'; + + // Response details if (response.data.response_code) { - message += ' (Response: ' + response.data.response_code + ')'; + var statusClass = response.data.success ? 'success' : 'error'; + resultHtml += '

Response Status: ' + response.data.response_code + '

'; + } + + // Show payload sent + if (response.data.test_payload) { + resultHtml += '
'; + resultHtml += 'Test Payload Sent'; + resultHtml += '
' + JSON.stringify(response.data.test_payload, null, 2) + '
'; + resultHtml += '
'; + } + + // Show response body if available + if (response.data.response_body) { + resultHtml += '
'; + resultHtml += 'Response Body'; + resultHtml += '
' + response.data.response_body + '
'; + resultHtml += '
'; } - var $resultRow = $( '

' + message + '

' ); + + resultHtml += '
'; + + var noticeClass = response.data.success ? 'notice-success' : 'notice-warning'; + var $resultRow = $( '
' + resultHtml + '
' ); $row.after( $resultRow ); - // Remove this specific message after 7 seconds - setTimeout(function() { + // Handle dismiss button + $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() { $( '#' + resultId ).fadeOut(function() { $( this ).remove(); }); - }, 7000); + }); } } else { - $link.text( 'Failed' ); - var error = response.data || 'Unknown error'; + $link.text( 'Failed' ).removeClass( 'testing' ).addClass( 'error' ); + var errorData = response.data || {}; var $row = $link.closest( 'tr' ); var colspan = $row.find( 'td' ).length; - var $resultRow = $( '

Test failed: ' + error + '

' ); + + // Build error HTML + var errorHtml = '
'; + errorHtml += ''; + errorHtml += '

Test failed: ' + (errorData.message || 'Unknown error') + '

'; + + if (errorData.error_code) { + errorHtml += '

Error Code: ' + errorData.error_code + '

'; + } + + if (errorData.error_data) { + errorHtml += '
'; + errorHtml += 'Error Details'; + errorHtml += '
' + JSON.stringify(errorData.error_data, null, 2) + '
'; + errorHtml += '
'; + } + + errorHtml += '
'; + + var $resultRow = $( '
' + errorHtml + '
' ); $row.after( $resultRow ); - // Remove this specific message after 7 seconds - setTimeout(function() { + // Handle dismiss button + $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() { $( '#' + resultId ).fadeOut(function() { $( this ).remove(); }); - }, 7000); + }); } // Reset button after 3 seconds setTimeout(function() { - $link.text( originalText ).removeClass( 'testing' ).css( 'pointer-events', 'auto' ); + $link.text( originalText ).removeClass( 'testing success error' ).css( 'pointer-events', 'auto' ); }, 3000); }, error: function(xhr, status, error) { - $link.text( 'Error' ); + $link.text( 'Error' ).removeClass( 'testing' ).addClass( 'error' ); var $row = $link.closest( 'tr' ); var colspan = $row.find( 'td' ).length; - var $resultRow = $( '

Test error: ' + error + '

' ); + + var errorHtml = '
'; + errorHtml += ''; + errorHtml += '

Test error: ' + error + '

'; + errorHtml += '
'; + + var $resultRow = $( '
' + errorHtml + '
' ); $row.after( $resultRow ); - // Remove this specific message after 7 seconds - setTimeout(function() { + // Handle dismiss button + $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() { $( '#' + resultId ).fadeOut(function() { $( this ).remove(); }); - }, 7000); + }); // Reset button after 3 seconds setTimeout(function() { - $link.text( originalText ).removeClass( 'testing' ).css( 'pointer-events', 'auto' ); + $link.text( originalText ).removeClass( 'testing error' ).css( 'pointer-events', 'auto' ); }, 3000); } });