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/.wp-env.json b/plugins/wp-graphql-headless-webhooks/.wp-env.json new file mode 100644 index 00000000..2b276711 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/.wp-env.json @@ -0,0 +1,25 @@ +{ + "core": null, + "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, + "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/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..97daeb31 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -0,0 +1,608 @@ +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' ) ); + + // 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' ) ); + } + + /** + * Add admin menu + */ + public function add_admin_menu(): void { + add_submenu_page( + 'graphiql-ide', + __( '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( 'admin.php?' . http_build_query( $args ) ); + } + + /** + * Enqueue admin assets + * + * @param string $hook_suffix Current admin page. + */ + public function enqueue_assets( string $hook_suffix ): void { + // 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; + } + + $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' ), + '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' ), + ) + ); + } + + /** + * 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; + } + + // Only handle delete action here since save is handled by admin-post.php + 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 + */ + public function handle_webhook_save(): void { + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'graphql_webhook_save', 'graphql_webhook_nonce' ) ) { + 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 + */ + public function handle_webhook_delete(): void { + $webhook_id = isset( $_GET['webhook_id'] ) ? intval( $_GET['webhook_id'] ) : 0; + + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'delete_webhook_' . $webhook_id ) ) { + return; + } + + if ( $webhook_id > 0 ) { + $this->repository->delete( $webhook_id ); + } + + wp_safe_redirect( $this->get_admin_url( array( 'deleted' => 'true' ) ) ); + 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 + */ + 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'; + } + } + + // 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( 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; + + // 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' => $message, + 'success' => $is_success, + 'response_code' => $response_code, + '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 + ) ); + } + + /** + * 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': + // 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 '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( + 'username' => 'test_webhook_user', + 'email' => 'test@webhook.local', + '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: + $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 + */ + 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'] ) ) { + $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'; + } + + 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(); + + // 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' ); + $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 ) { + $webhook_id = 0; + $name = ''; + $event = ''; + $url = ''; + $method = 'POST'; + $headers = array(); + } else { + // Extract values from webhook entity + $webhook_id = $webhook->id; + $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'; + } + } +} 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..392f4f96 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.css @@ -0,0 +1,294 @@ +/** + * 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; +} + +.webhook-header-row button.remove-header:hover { + background: #d63638; + color: #fff; +} + +/* Add some spacing */ +#webhook-headers { + margin-bottom: 10px; +} + +/* Form table adjustments */ +.form-table th { + width: 200px; +} + +/* Webhooks table column widths */ +.wp-list-table.webhooks .column-cb { + width: 2.2em; +} + +.wp-list-table.webhooks .column-name { + width: 25%; +} + +.wp-list-table.webhooks .column-event { + width: 15%; +} + +.wp-list-table.webhooks .column-method { + width: 10%; +} + +.wp-list-table.webhooks .column-url { + width: auto; +} + +.wp-list-table.webhooks .column-headers { + width: 15%; +} + +/* Improve table styling */ +.wp-list-table.webhooks { + margin-top: 0.5em; +} + +.wp-list-table.webhooks th:not(.check-column), +.wp-list-table.webhooks td:not(.check-column) { + vertical-align: top; +} + +/* Checkbox column specific styling */ +.wp-list-table.webhooks .check-column { + vertical-align: top; + padding: 8px 0 0 3px; +} + +/* 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; +} + +/* 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: 60px 20px; + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + margin-top: 20px; +} + +.webhooks-empty-state h2 { + font-size: 21px; + font-weight: 400; + margin: 0 0 0.5em; + line-height: 1.3; +} + +.webhooks-empty-state p { + 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; + text-decoration: none; +} + +/* Test webhook button states */ +.test-webhook.testing { + color: #666; + cursor: not-allowed; + pointer-events: none; +} + +.test-webhook.success { + color: #46b450; +} + +.test-webhook.error { + 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-details { + padding: 5px 0; + margin: 0; +} + +.webhook-test-details p { + 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 { + 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 { + 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..d453635e --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/assets/admin.js @@ -0,0 +1,195 @@ +/** + * Admin JavaScript for WPGraphQL Webhooks + */ + +(function ($) { + 'use strict'; + + $( document ).ready( + function () { + // Handle adding new header fields + $( '#add-header' ).on( + 'click', + function () { + var headerRow = $( wpGraphQLWebhooks.headerTemplate ); + $( '#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( wpGraphQLWebhooks.confirmDelete )) { + e.preventDefault(); + } + } + ); + + // 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' ).removeClass( 'testing' ).addClass( 'success' ); + if (response.data) { + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td' ).length; + + // Build detailed result HTML + var resultHtml = '
'; + resultHtml += ''; + resultHtml += '

' + response.data.message + '

'; + + // Response details + if (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 += '
'; + } + + resultHtml += '
'; + + var noticeClass = response.data.success ? 'notice-success' : 'notice-warning'; + var $resultRow = $( '
' + resultHtml + '
' ); + $row.after( $resultRow ); + + // Handle dismiss button + $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() { + $( '#' + resultId ).fadeOut(function() { + $( this ).remove(); + }); + }); + } + } else { + $link.text( 'Failed' ).removeClass( 'testing' ).addClass( 'error' ); + var errorData = response.data || {}; + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td' ).length; + + // 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 ); + + // Handle dismiss button + $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() { + $( '#' + resultId ).fadeOut(function() { + $( this ).remove(); + }); + }); + } + + // Reset button after 3 seconds + setTimeout(function() { + $link.text( originalText ).removeClass( 'testing success error' ).css( 'pointer-events', 'auto' ); + }, 3000); + }, + error: function(xhr, status, error) { + $link.text( 'Error' ).removeClass( 'testing' ).addClass( 'error' ); + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td' ).length; + + var errorHtml = '
'; + errorHtml += ''; + errorHtml += '

Test error: ' + error + '

'; + errorHtml += '
'; + + var $resultRow = $( '
' + errorHtml + '
' ); + $row.after( $resultRow ); + + // Handle dismiss button + $( '#' + resultId + ' .notice-dismiss' ).on( 'click', function() { + $( '#' + resultId ).fadeOut(function() { + $( this ).remove(); + }); + }); + + // Reset button after 3 seconds + setTimeout(function() { + $link.text( originalText ).removeClass( 'testing error' ).css( 'pointer-events', 'auto' ); + }, 3000); + } + }); + } + ); + } + ); + +})( 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..da7e0b6a --- /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..95e9273e --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -0,0 +1,173 @@ + + +
+

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

+
+ + + + +
+

+

+ + + +
+ +
+ + + +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + name ); ?> + + +
+ + + + | + + + + + | + + + + + + +
+ +
event ); ?> + method ) ); ?> + + url ); ?> + + headers ) ) : ?> + headers as $header => $value ) : ?> +
+ + + + +
+ +
+ +
+
+ + + +
+
+
+
+ +
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..29f5f4b3 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Events/SmartCacheEventHandler.php @@ -0,0 +1,185 @@ + 'smart_cache_created', + 'update' => 'smart_cache_updated', + 'delete' => 'smart_cache_deleted', + ]; + + /** + * Constructor + * + * @param $webhook_trigger_callback Callback to trigger webhooks + */ + public function __construct( $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 ) { + $parts = explode( '_', $event ); + if ( count( $parts ) !== 2 ) { + return; + } + + $post_type = $parts[0]; + $action = strtolower( $parts[1] ); + $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' => [], + ]; + } + + // Just store the key - let webhook consumers decode if needed + $this->buffer[ $buffer_key ]['keys'][] = $key; + + // 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' ] ); + } + } + + /** + * Handle cache purge nodes event - this fires immediately + * + * @param string $key Cache key + * @param array $nodes Nodes being purged + */ + 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' ), + ]; + + call_user_func( $this->webhook_trigger_callback, 'smart_cache_nodes_purged', $payload ); + } + + /** + * 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; + } + + // 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'] ), // 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' ), + ]; + + // 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 ); + } + + $this->buffer = []; + $this->timer = false; + } +} diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php index 3776fae7..a95d80ff 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' ] ); } /** @@ -47,6 +57,9 @@ public function register_hooks(): void { add_action( 'delete_attachment', [ $this, 'on_media_deleted' ], 10, 1 ); add_action( 'wp_insert_comment', [ $this, 'on_comment_inserted' ], 10, 2 ); add_action( 'transition_comment_status', [ $this, 'on_comment_status' ], 10, 3 ); + + // Smart Cache integration + $this->smart_cache_handler->init(); } /** @@ -57,12 +70,35 @@ public function register_hooks(): void { */ private function trigger_webhooks( string $event, array $payload ): void { $allowed_events = $this->repository->get_allowed_events(); + if ( ! array_key_exists( $event, $allowed_events ) ) { error_log( 'Event ' . $event . ' is not allowed. Allowed events: ' . implode( ', ', $allowed_events ) ); 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 ) { if ( $webhook->event === $event ) { $this->handler->handle( $webhook, $payload ); diff --git a/plugins/wp-graphql-headless-webhooks/src/Plugin.php b/plugins/wp-graphql-headless-webhooks/src/Plugin.php index 4eba694a..8dfdc0f2 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,60 @@ 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(); + + // 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 ); + 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(); + } } /** @@ -124,4 +185,4 @@ public function __wakeup(): void { } } -endif; \ No newline at end of file +endif; 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, diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php index 05cfe9dc..b391e17b 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php +++ b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php @@ -24,24 +24,11 @@ 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 only by default + 'smart_cache_created' => 'Smart Cache - Content Created', + 'smart_cache_updated' => 'Smart Cache - Content Updated', + 'smart_cache_deleted' => 'Smart Cache - Content Deleted', + 'smart_cache_nodes_purged' => 'Smart Cache - Nodes Purged', ]; /** @@ -53,6 +40,16 @@ public function get_allowed_events(): array { return apply_filters('graphql_webhooks_allowed_events', $this->default_events); } + /** + * Get the list of allowed HTTP methods. + * + * @return array Array of allowed HTTP methods. + */ + public function get_allowed_methods(): array { + $default_methods = ['POST', 'GET']; + return apply_filters('graphql_webhooks_allowed_methods', $default_methods); + } + /** * Retrieve all published webhook entities. * @@ -197,7 +194,8 @@ public function validate_data($event, $url, $method) { if (!filter_var($url, FILTER_VALIDATE_URL)) { return new WP_Error('invalid_url', 'Invalid URL.'); } - if (!in_array(strtoupper($method), ['GET', 'POST'], true)) { + $allowed_methods = $this->get_allowed_methods(); + if (!in_array(strtoupper($method), array_map('strtoupper', $allowed_methods), true)) { return new WP_Error('invalid_method', 'Invalid HTTP method.'); } return apply_filters('graphql_webhooks_validate_data', true, $event, $url, $method); 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 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': 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