diff --git a/examples/next/webhooks-isr/.wp-env.json b/examples/next/webhooks-isr/.wp-env.json index a15c20dd..b18d87b2 100644 --- a/examples/next/webhooks-isr/.wp-env.json +++ b/examples/next/webhooks-isr/.wp-env.json @@ -1,7 +1,8 @@ { - "phpVersion": "7.4", + "phpVersion": "8.0", "plugins": [ "https://github.com/wp-graphql/wp-graphql/releases/latest/download/wp-graphql.zip", + "https://github.com/wp-graphql/wp-graphql-smart-cache/releases/download/v2.0.0/wpgraphql-smart-cache.zip", "https://downloads.wordpress.org/plugin/code-snippets.3.6.8.zip", "../../../plugins/wp-graphql-headless-webhooks" ], diff --git a/plugins/wp-graphql-headless-webhooks/assets/css/admin.css b/plugins/wp-graphql-headless-webhooks/assets/css/admin.css new file mode 100644 index 00000000..7bfc7661 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/assets/css/admin.css @@ -0,0 +1,207 @@ +/** + * Minimal custom styles for Webhooks admin + * Leveraging WordPress core admin classes + */ + +/* Use core WordPress styles for most elements */ + +/* Webhook method badge styling */ +.webhook-method { + font-weight: 600; + text-transform: uppercase; + font-size: 11px; +} + +/* Header row styling using core form-table classes */ +.webhook-headers .form-table td { + padding: 5px 10px 5px 0; +} + +/* Minimal adjustment for header inputs */ +.webhook-header-row { + margin-bottom: 10px; +} + +.webhook-header-row input[type="text"] { + width: calc(50% - 50px); + margin-right: 10px; +} + +/* Use core button styles, just add spacing */ +.test-webhook { + margin-right: 5px; +} + +/* Test button states */ +.test-webhook.testing { + opacity: 0.6; + cursor: not-allowed; +} + +.test-webhook.success { + color: #46b450; +} + +.test-webhook.error { + color: #dc3232; +} + +/* Webhook test result row */ +.webhook-test-result td { + padding: 0 !important; + background: transparent !important; +} + +.webhook-test-result .notice { + margin: 10px; + position: relative; +} + +.webhook-test-result .notice.inline { + display: block; +} + +/* Test details styling */ +.webhook-test-details { + padding: 12px; +} + +.webhook-test-details p { + margin: 0.5em 0; +} + +.webhook-test-details strong { + font-weight: 600; +} + +/* Status indicators */ +.webhook-test-details .status-success { + color: #46b450; + font-weight: 600; +} + +.webhook-test-details .status-error { + color: #dc3232; + font-weight: 600; +} + +/* Expandable details */ +.webhook-test-details details { + margin: 10px 0; + border: 1px solid #ddd; + border-radius: 3px; + padding: 0; +} + +.webhook-test-details summary { + padding: 10px; + cursor: pointer; + background: #f7f7f7; + border-bottom: 1px solid #ddd; + font-weight: 600; +} + +.webhook-test-details details[open] summary { + border-bottom: 1px solid #ddd; +} + +.webhook-test-details pre { + margin: 0; + padding: 10px; + background: #f9f9f9; + overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; + font-size: 12px; + line-height: 1.4; +} + +/* Payload and response styling */ +.webhook-test-payload, +.webhook-response-body { + font-family: Consolas, Monaco, monospace; + max-height: 300px; + overflow-y: auto; +} + +/* Notice dismiss button positioning */ +.webhook-test-result .notice-dismiss { + position: absolute; + top: 0; + right: 1px; + padding: 10px 15px; + font-size: 13px; + line-height: 1.23076923; + text-decoration: none; +} + +.webhook-test-result .notice-dismiss:before { + content: "\f153"; + font-family: dashicons; + font-size: 20px; + font-style: normal; + font-weight: 400; + height: 20px; + width: 20px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Webhook URL column styling */ +.webhook-url { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: Consolas, Monaco, monospace; + font-size: 13px; + color: #2271b1; +} + +/* Ensure proper column spacing */ +.wp-list-table .column-url { + width: 35%; + min-width: 200px; +} + +.wp-list-table .column-name { + width: 25%; +} + +.wp-list-table .column-event { + width: 20%; +} + +.wp-list-table .column-method { + width: 10%; +} + +.wp-list-table .column-headers { + width: 10%; +} + +/* Row actions test link styling */ +.row-actions .test-webhook { + color: #2271b1; + text-decoration: none; +} + +.row-actions .test-webhook:hover { + color: #135e96; + text-decoration: underline; +} + +.row-actions .test-webhook.testing { + color: #666; + cursor: default; + pointer-events: none; +} + +.row-actions .test-webhook.success { + color: #46b450; +} + +.row-actions .test-webhook.error { + color: #dc3232; +} diff --git a/plugins/wp-graphql-headless-webhooks/assets/js/admin.js b/plugins/wp-graphql-headless-webhooks/assets/js/admin.js new file mode 100644 index 00000000..26e58e08 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/assets/js/admin.js @@ -0,0 +1,205 @@ +/** + * 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 || wpGraphQLWebhooks.headerRowTemplate ); + $( '#webhook-headers-container, #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' ); + + // 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( originalText ).removeClass( 'testing' ).addClass( 'success' ); + if (response.data) { + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td, th' ).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( originalText ).removeClass( 'testing' ).addClass( 'error' ); + var errorData = response.data || {}; + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td, th' ).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(); + }); + }); + } + }, + error: function(xhr, status, error) { + $link.text( originalText ).removeClass( 'testing' ).addClass( 'error' ); + var $row = $link.closest( 'tr' ); + var colspan = $row.find( 'td, th' ).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(); + }); + }); + } + }); + } + ); + + // Handle bulk actions if using WP_List_Table + $('#doaction, #doaction2').on('click', function(e) { + var action = $(this).prev('select').val(); + + if (action === 'delete') { + var checked = $('input[name="webhook[]"]:checked'); + + if (checked.length === 0) { + alert('Please select at least one webhook to delete.'); + e.preventDefault(); + return false; + } + + if (!confirm(wpGraphQLWebhooks.confirmDelete)) { + e.preventDefault(); + return false; + } + } + }); + } + ); + +})( jQuery ); diff --git a/plugins/wp-graphql-headless-webhooks/composer.lock b/plugins/wp-graphql-headless-webhooks/composer.lock index b2fa58da..cd233022 100644 --- a/plugins/wp-graphql-headless-webhooks/composer.lock +++ b/plugins/wp-graphql-headless-webhooks/composer.lock @@ -8,28 +8,29 @@ "packages": [ { "name": "axepress/wp-graphql-plugin-boilerplate", - "version": "0.1.0", + "version": "0.1.1", "source": { "type": "git", "url": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate.git", - "reference": "49096512cd599068edbbcff8e46684e207789001" + "reference": "09495b61346453baabdf4c71a38ada3cfc91c3a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AxeWP/wp-graphql-plugin-boilerplate/zipball/49096512cd599068edbbcff8e46684e207789001", - "reference": "49096512cd599068edbbcff8e46684e207789001", + "url": "https://api.github.com/repos/AxeWP/wp-graphql-plugin-boilerplate/zipball/09495b61346453baabdf4c71a38ada3cfc91c3a7", + "reference": "09495b61346453baabdf4c71a38ada3cfc91c3a7", "shasum": "" }, "require": { "php": ">=7.4" }, "require-dev": { - "axepress/wp-graphql-cs": "^2.0.0-beta", - "axepress/wp-graphql-stubs": "^1.12.2", + "axepress/wp-graphql-cs": "^2.0.0", + "axepress/wp-graphql-stubs": "^2.3.0", "phpcompatibility/php-compatibility": "dev-develop as 9.9.9", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.2", - "szepeviktor/phpstan-wordpress": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "szepeviktor/phpstan-wordpress": "^2.0", "wp-cli/wp-cli-bundle": "^2.8.1" }, "type": "library", @@ -55,7 +56,7 @@ "description": "Boilerplate for creating WPGraphQL extensions", "support": { "issues": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate/issues", - "source": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate/tree/0.1.0" + "source": "https://github.com/AxeWP/wp-graphql-plugin-boilerplate/tree/0.1.1" }, "funding": [ { @@ -63,49 +64,42 @@ "type": "github" } ], - "time": "2024-04-06T17:12:24+00:00" + "time": "2025-06-07T02:03:50+00:00" } ], "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 +107,6 @@ "MIT" ], "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, { "name": "Aaron Piotrowski", "email": "aaron@trowski.com" @@ -128,6 +118,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 +138,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 +147,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 +214,269 @@ ], "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": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "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": [ { @@ -225,7 +484,7 @@ "type": "github" } ], - "time": "2024-04-13T18:00:56+00:00" + "time": "2024-08-03T19:31:26+00:00" }, { "name": "automattic/vipwpcs", @@ -392,25 +651,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 +687,8 @@ } }, "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" + "psr-4": { + "Behat\\Gherkin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -434,11 +699,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 +714,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 +1401,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 +1457,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 +1473,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 +2202,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 +2252,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 +2268,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "eftec/bladeone", @@ -3178,25 +3443,27 @@ }, { "name": "nikic/php-parser", - "version": "v4.19.4", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "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/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "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 +3471,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -3228,9 +3495,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.5.0" }, - "time": "2024-09-29T15:01:53+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "phar-io/manifest", @@ -3586,16 +3853,16 @@ }, { "name": "php-stubs/wp-cli-stubs", - "version": "v2.11.0", + "version": "v2.12.0", "source": { "type": "git", "url": "https://github.com/php-stubs/wp-cli-stubs.git", - "reference": "f27ff9e8e29d7962cb070e58de70dfaf63183007" + "reference": "af16401e299a3fd2229bd0fa9a037638a4174a9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wp-cli-stubs/zipball/f27ff9e8e29d7962cb070e58de70dfaf63183007", - "reference": "f27ff9e8e29d7962cb070e58de70dfaf63183007", + "url": "https://api.github.com/repos/php-stubs/wp-cli-stubs/zipball/af16401e299a3fd2229bd0fa9a037638a4174a9d", + "reference": "af16401e299a3fd2229bd0fa9a037638a4174a9d", "shasum": "" }, "require": { @@ -3624,9 +3891,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wp-cli-stubs/issues", - "source": "https://github.com/php-stubs/wp-cli-stubs/tree/v2.11.0" + "source": "https://github.com/php-stubs/wp-cli-stubs/tree/v2.12.0" }, - "time": "2024-11-25T10:09:13+00:00" + "time": "2025-06-10T09:58:05+00:00" }, { "name": "php-webdriver/webdriver", @@ -3937,29 +4204,29 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd" + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/46d08eb86eec622b96c466adec3063adfed280dd", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.12.1" + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -4015,33 +4282,33 @@ "type": "thanks_dev" } ], - "time": "2025-04-20T23:35:32+00:00" + "time": "2025-06-14T07:40:39+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.12", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad", + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -4078,6 +4345,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -4101,9 +4369,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-05-20T13:34:27+00:00" + "time": "2025-06-12T04:32:33+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4377,16 +4649,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 +4703,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 +5232,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 +5279,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 +5495,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 +5539,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 +5660,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 +6927,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.18.0", + "version": "8.19.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593" + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" }, "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/458d665acd49009efebd7e0cb385d71ae9ac3220", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", "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 +6976,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.19.1" }, "funding": [ { @@ -6639,33 +6988,32 @@ "type": "tidelift" } ], - "time": "2025-05-01T09:40:50+00:00" + "time": "2025-06-09T17:53:57+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 +7056,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 +7112,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,20 +7124,20 @@ "type": "github" } ], - "time": "2022-12-26T08:22:07+00:00" + "time": "2024-12-16T12:45:15+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.0", + "version": "3.13.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186" + "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1b71b4dd7e7ef651ac749cea67e513c0c832f4bd", + "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd", "shasum": "" }, "require": { @@ -6856,7 +7208,7 @@ "type": "thanks_dev" } ], - "time": "2025-05-11T03:36:00+00:00" + "time": "2025-06-12T15:04:34+00:00" }, { "name": "symfony/browser-kit", @@ -6932,38 +7284,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 +7339,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 +7355,7 @@ "type": "tidelift" } ], - "time": "2024-10-30T07:58:02+00:00" + "time": "2025-05-14T06:00:01+00:00" }, { "name": "symfony/console", @@ -7176,20 +7524,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 +7546,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -7223,7 +7571,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 +7587,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 +7751,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 +7774,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -7462,7 +7807,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 +7823,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 +7873,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 +7889,7 @@ "type": "tidelift" } ], - "time": "2024-10-22T13:05:35+00:00" + "time": "2024-10-25T15:15:23+00:00" }, { "name": "symfony/finder", @@ -8163,29 +8507,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 +8534,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 +8570,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 +8586,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 +8632,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 +8648,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 +8718,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 +8734,7 @@ "type": "tidelift" } ], - "time": "2024-11-10T20:33:58+00:00" + "time": "2025-04-18T15:23:29+00:00" }, { "name": "symfony/yaml", @@ -8581,21 +8925,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 +8952,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 +8977,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 +9000,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 +9031,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", @@ -11232,6 +11576,6 @@ "platform": { "php": "^7.4 || ^8.0" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.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..c2b6d408 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php @@ -0,0 +1,508 @@ +repository = $repository; + + // Hook into WordPress admin + add_action( 'admin_menu', [ $this, 'add_admin_menu' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); + + // Handle form submissions via admin-post.php + add_action( 'admin_post_graphql_webhook_save', [ $this, 'handle_webhook_save' ] ); + add_action( 'admin_post_graphql_webhook_delete', [ $this, 'handle_webhook_delete' ] ); + + // Handle admin actions + add_action( 'admin_init', [ $this, 'handle_admin_actions' ] ); + + // Handle AJAX webhook test + add_action( 'wp_ajax_test_webhook', [ $this, 'ajax_test_webhook' ] ); + } + + /** + * Initialize admin hooks. + */ + public function init(): void { + add_action( 'admin_init', [ $this, 'handle_actions' ] ); + } + + /** + * 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, + [ $this, 'render_admin_page' ] + ); + } + + /** + * Generate admin URL. + * + * @param array $args Query arguments. + * @return string Admin URL. + */ + public function get_admin_url( array $args = [] ): string { + $defaults = [ + 'page' => self::ADMIN_PAGE_SLUG, + ]; + $args = array_merge( $defaults, $args ); + return add_query_arg( $args, admin_url( 'admin.php' ) ); + } + + /** + * Enqueue admin assets. + * + * @param string $hook Current admin page hook. + */ + public function enqueue_assets( string $hook ): void { + if ( 'graphql_page_' . self::ADMIN_PAGE_SLUG !== $hook ) { + return; + } + + wp_enqueue_style( + 'graphql-webhooks-admin', + WPGRAPHQL_HEADLESS_WEBHOOKS_PLUGIN_URL . 'assets/css/admin.css', + [], + WPGRAPHQL_HEADLESS_WEBHOOKS_VERSION + ); + + wp_enqueue_script( + 'graphql-webhooks-admin', + WPGRAPHQL_HEADLESS_WEBHOOKS_PLUGIN_URL . 'assets/js/admin.js', + [ 'jquery' ], + WPGRAPHQL_HEADLESS_WEBHOOKS_VERSION, + true + ); + + wp_localize_script( + 'graphql-webhooks-admin', + 'wpGraphQLWebhooks', + [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'restUrl' => rest_url( 'graphql-webhooks/v1/' ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'confirmDelete' => __( 'Are you sure you want to delete this webhook?', 'wp-graphql-headless-webhooks' ), + 'headerTemplate' => $this->get_header_row_template(), + ] + ); + } + + /** + * Get header row template for JavaScript. + * + * @return string HTML template. + */ + private function get_header_row_template(): string { + ob_start(); + include __DIR__ . '/views/partials/webhook-header-row.php'; + return ob_get_clean(); + } + + /** + * Handle admin actions. + */ + public function handle_actions(): void { + if ( ! isset( $_GET['page'] ) || self::ADMIN_PAGE_SLUG !== $_GET['page'] ) { + return; + } + + if ( isset( $_POST['action'] ) && 'save_webhook' === $_POST['action'] ) { + $this->handle_webhook_save(); + } + + if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] && isset( $_GET['webhook_id'] ) ) { + $this->handle_webhook_delete(); + } + } + + /** + * Verify admin permission. + * + * @return bool Whether user has permission. + */ + private function verify_admin_permission(): bool { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( __( 'You do not have sufficient permissions to access this page.', 'wp-graphql-headless-webhooks' ) ); + return false; + } + return true; + } + + /** + * Verify nonce. + * + * @param string $nonce_name Nonce name. + * @param string $action Nonce action. + * @return bool Whether nonce is valid. + */ + private function verify_nonce( string $nonce_name, string $action ): bool { + if ( ! isset( $_REQUEST[ $nonce_name ] ) || ! wp_verify_nonce( $_REQUEST[ $nonce_name ], $action ) ) { + wp_die( __( 'Security check failed.', 'wp-graphql-headless-webhooks' ) ); + return false; + } + return true; + } + + /** + * Handle webhook save + */ + public function handle_webhook_save() { + // Verify permissions and nonce + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'webhook_save', 'webhook_nonce' ) ) { + wp_die( __( 'Unauthorized', 'wp-graphql-webhooks' ) ); + } + + $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0; + $data = [ + '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' ), + 'headers' => $this->sanitize_headers( $_POST['webhook_headers'] ?? [] ), + ]; + + // Validate data + $validation = $this->repository->validate_data( $data ); + if ( is_wp_error( $validation ) ) { + wp_die( $validation->get_error_message() ); + } + + // Save webhook + if ( $webhook_id > 0 ) { + $result = $this->repository->update( $webhook_id, $data ); + $redirect_args = $result ? [ 'updated' => 1 ] : [ 'error' => 1 ]; + } else { + $result = $this->repository->create( $data ); + $redirect_args = $result ? [ 'added' => 1 ] : [ 'error' => 1 ]; + } + + // Redirect back to list page + wp_redirect( add_query_arg( $redirect_args, $this->get_admin_url() ) ); + exit; + } + + /** + * Handle webhook delete + */ + public function handle_webhook_delete() { + // This method will be called via bulk actions from WP_List_Table + // Individual deletes are handled through the list table's handle_row_actions + } + + /** + * Handle admin actions + */ + public function handle_admin_actions() { + // Handle bulk actions from WP_List_Table + if ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] || + isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) { + + if ( ! $this->verify_admin_permission() || ! $this->verify_nonce( 'bulk-webhooks', '_wpnonce' ) ) { + return; + } + + $webhook_ids = isset( $_REQUEST['webhook'] ) ? array_map( 'intval', (array) $_REQUEST['webhook'] ) : []; + $deleted = 0; + + foreach ( $webhook_ids as $webhook_id ) { + if ( $this->repository->delete( $webhook_id ) ) { + $deleted++; + } + } + + if ( $deleted > 0 ) { + wp_redirect( add_query_arg( [ 'deleted' => $deleted ], $this->get_admin_url() ) ); + exit; + } + } + } + + /** + * Render the admin page + */ + public function render_admin_page() { + $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : 'list'; + + switch ( $action ) { + case 'add': + case 'edit': + $this->render_form_page( $action ); + break; + default: + $this->render_list_page(); + break; + } + } + + /** + * Render the list page using WP_List_Table + */ + private function render_list_page() { + // Include the custom list table class + require_once __DIR__ . '/WebhooksListTable.php'; + + // Create an instance of our list table + $list_table = new WebhooksListTable( $this->repository ); + + // Include the list view template + include __DIR__ . '/views/webhooks-list.php'; + } + + /** + * Render the form page (add/edit) + * + * @param string $action The action (add or edit). + */ + private function render_form_page( $action ) { + $webhook = null; + $form_title = 'add' === $action ? __( 'Add New Webhook', 'wp-graphql-webhooks' ) : __( 'Edit Webhook', 'wp-graphql-webhooks' ); + $submit_text = 'add' === $action ? __( 'Add Webhook', 'wp-graphql-webhooks' ) : __( 'Update Webhook', 'wp-graphql-webhooks' ); + + // Default values for new webhook + $name = ''; + $event = ''; + $url = ''; + $method = 'POST'; + $headers = []; + + if ( 'edit' === $action ) { + $webhook_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0; + $webhook = $this->repository->get( $webhook_id ); + + if ( ! $webhook ) { + wp_die( __( 'Webhook not found.', 'wp-graphql-webhooks' ) ); + } + + // Extract values from webhook entity + $name = $webhook->name; + $event = $webhook->event; + $url = $webhook->url; + $method = $webhook->method; + $headers = $webhook->headers; + } + + $events = $this->repository->get_allowed_events(); + $methods = $this->repository->get_allowed_methods(); + $admin = $this; // Pass admin instance to template + + include __DIR__ . '/views/webhook-form.php'; + } + + /** + * Sanitize headers + * + * @param array $headers Headers to sanitize. + * @return array Sanitized headers. + */ + private function sanitize_headers( array $headers ): array { + $sanitized_headers = []; + + // Handle the form data structure where headers come as separate arrays + if ( isset( $headers['name'] ) && isset( $headers['value'] ) ) { + $names = (array) $headers['name']; + $values = (array) $headers['value']; + + foreach ( $names as $index => $name ) { + $name = sanitize_text_field( $name ); + $value = sanitize_text_field( $values[ $index ] ?? '' ); + + if ( ! empty( $name ) && ! empty( $value ) ) { + $sanitized_headers[ $name ] = $value; + } + } + } + + return $sanitized_headers; + } + + /** + * Handle AJAX webhook test request. + */ + public function ajax_test_webhook(): void { + // Verify nonce + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'wp_rest' ) ) { + wp_send_json_error( [ + 'message' => __( 'Invalid security token.', 'wp-graphql-headless-webhooks' ), + 'error_code' => 'invalid_nonce' + ] ); + } + + // Check permissions + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ + 'message' => __( 'You do not have permission to test webhooks.', 'wp-graphql-headless-webhooks' ), + 'error_code' => 'insufficient_permissions' + ] ); + } + + // Get webhook ID + $webhook_id = isset( $_POST['webhook_id'] ) ? intval( $_POST['webhook_id'] ) : 0; + if ( ! $webhook_id ) { + wp_send_json_error( [ + 'message' => __( 'Invalid webhook ID.', 'wp-graphql-headless-webhooks' ), + 'error_code' => 'invalid_webhook_id' + ] ); + } + + // Get webhook + $webhook = $this->repository->get( $webhook_id ); + if ( ! $webhook ) { + wp_send_json_error( [ + 'message' => __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ), + 'error_code' => 'webhook_not_found' + ] ); + } + + // Create test payload + $test_payload = [ + 'event' => 'test_webhook', + 'timestamp' => current_time( 'mysql' ), + 'webhook' => [ + 'id' => $webhook->id, + 'name' => $webhook->name, + 'url' => $webhook->url, + ], + 'test_data' => [ + 'message' => 'This is a test webhook dispatch', + 'random' => wp_generate_password( 12, false ), + ], + ]; + + // Log the test attempt + error_log( sprintf( + '[WPGraphQL Webhooks] Testing webhook #%d (%s) to %s', + $webhook->id, + $webhook->name, + $webhook->url + ) ); + + // Prepare request args + $args = [ + 'method' => $webhook->method, + 'timeout' => 15, + 'redirection' => 5, + 'httpversion' => '1.1', + 'blocking' => true, // We want to wait for the response + 'headers' => array_merge( + [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'WPGraphQL-Webhooks/' . WPGRAPHQL_HEADLESS_WEBHOOKS_VERSION, + ], + $webhook->headers + ), + 'body' => wp_json_encode( $test_payload ), + 'sslverify' => apply_filters( 'graphql_webhooks_sslverify', true ), + ]; + + // Add webhook metadata to headers + $args['headers']['X-WPGraphQL-Webhook-Event'] = 'test_webhook'; + $args['headers']['X-WPGraphQL-Webhook-ID'] = (string) $webhook->id; + + // Start timing + $start_time = microtime( true ); + + // Make the request + $response = wp_remote_request( $webhook->url, $args ); + + // Calculate duration + $duration_ms = round( ( microtime( true ) - $start_time ) * 1000, 2 ); + + // Check for errors + if ( is_wp_error( $response ) ) { + error_log( sprintf( + '[WPGraphQL Webhooks] Test failed for webhook #%d: %s', + $webhook->id, + $response->get_error_message() + ) ); + + wp_send_json_error( [ + 'message' => sprintf( + __( 'Failed to send test webhook: %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 ); + + // Log the response + error_log( sprintf( + '[WPGraphQL Webhooks] Test response for webhook #%d: HTTP %d in %sms', + $webhook->id, + $response_code, + $duration_ms + ) ); + + // Determine if successful (2xx status codes) + $is_success = $response_code >= 200 && $response_code < 300; + + // Prepare response data + $response_data = [ + 'success' => $is_success, + 'message' => $is_success + ? sprintf( __( 'Test webhook sent successfully to %s', 'wp-graphql-headless-webhooks' ), $webhook->url ) + : sprintf( __( 'Webhook returned HTTP %d', 'wp-graphql-headless-webhooks' ), $response_code ), + 'webhook_id' => $webhook->id, + 'webhook_name' => $webhook->name, + 'target_url' => $webhook->url, + 'method' => $webhook->method, + 'response_code' => $response_code, + 'duration_ms' => $duration_ms, + 'timestamp' => current_time( 'c' ), + 'test_payload' => $test_payload, + ]; + + // Add response body if available (limit to 1000 chars for UI) + if ( ! empty( $response_body ) ) { + $response_data['response_body'] = strlen( $response_body ) > 1000 + ? substr( $response_body, 0, 1000 ) . '...' + : $response_body; + } + + // Send success response (even if webhook returned non-2xx, the test itself succeeded) + wp_send_json_success( $response_data ); + } +} diff --git a/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksListTable.php b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksListTable.php new file mode 100644 index 00000000..1e1e9aea --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksListTable.php @@ -0,0 +1,264 @@ +repository = $repository; + + parent::__construct( [ + 'singular' => __( 'Webhook', 'wp-graphql-webhooks' ), + 'plural' => __( 'Webhooks', 'wp-graphql-webhooks' ), + 'ajax' => false, + ] ); + } + + /** + * Get columns + * + * @return array + */ + public function get_columns() { + return [ + 'cb' => '', + 'name' => __( 'Name', 'wp-graphql-webhooks' ), + 'event' => __( 'Event', 'wp-graphql-webhooks' ), + 'method' => __( 'Method', 'wp-graphql-webhooks' ), + 'url' => __( 'URL', 'wp-graphql-webhooks' ), + 'headers' => __( 'Headers', 'wp-graphql-webhooks' ), + ]; + } + + /** + * Get sortable columns + * + * @return array + */ + public function get_sortable_columns() { + return [ + 'name' => [ 'name', false ], + 'event' => [ 'event', false ], + 'method' => [ 'method', false ], + ]; + } + + /** + * Get bulk actions + * + * @return array + */ + public function get_bulk_actions() { + return [ + 'bulk-delete' => __( 'Delete', 'wp-graphql-webhooks' ), + ]; + } + + /** + * Process bulk actions + */ + public function process_bulk_action() { + // Handle bulk delete + if ( 'bulk-delete' === $this->current_action() ) { + $webhook_ids = isset( $_POST['webhook'] ) ? array_map( 'intval', $_POST['webhook'] ) : []; + + if ( ! empty( $webhook_ids ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'bulk-' . $this->_args['plural'] ) ) { + foreach ( $webhook_ids as $id ) { + $this->repository->delete( $id ); + } + + wp_redirect( add_query_arg( 'deleted', count( $webhook_ids ), remove_query_arg( [ 'action', 'webhook', '_wpnonce' ] ) ) ); + exit; + } + } + + // Handle single delete + if ( 'delete' === $this->current_action() ) { + $webhook_id = isset( $_GET['webhook'] ) ? intval( $_GET['webhook'] ) : 0; + $nonce = isset( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : ''; + + if ( $webhook_id && wp_verify_nonce( $nonce, 'delete-webhook-' . $webhook_id ) ) { + $this->repository->delete( $webhook_id ); + wp_redirect( add_query_arg( 'deleted', 1, remove_query_arg( [ 'action', 'webhook', '_wpnonce' ] ) ) ); + exit; + } + } + } + + /** + * Prepare items for display + */ + public function prepare_items() { + $this->process_bulk_action(); + + $per_page = $this->get_items_per_page( 'webhooks_per_page', 20 ); + $current_page = $this->get_pagenum(); + + // Get all webhooks + $webhooks = $this->repository->get_all(); + $total_items = count( $webhooks ); + + // Handle sorting + $orderby = ! empty( $_GET['orderby'] ) ? $_GET['orderby'] : 'name'; + $order = ! empty( $_GET['order'] ) ? $_GET['order'] : 'asc'; + + usort( $webhooks, function( $a, $b ) use ( $orderby, $order ) { + $result = 0; + + switch ( $orderby ) { + case 'name': + $result = strcmp( $a->name, $b->name ); + break; + case 'event': + $result = strcmp( $a->event, $b->event ); + break; + case 'method': + $result = strcmp( $a->method, $b->method ); + break; + } + + return ( 'asc' === $order ) ? $result : -$result; + } ); + + // Pagination + $this->items = array_slice( $webhooks, ( $current_page - 1 ) * $per_page, $per_page ); + + $this->set_pagination_args( [ + 'total_items' => $total_items, + 'per_page' => $per_page, + 'total_pages' => ceil( $total_items / $per_page ), + ] ); + + $columns = $this->get_columns(); + $hidden = []; + $sortable = $this->get_sortable_columns(); + + $this->_column_headers = [ $columns, $hidden, $sortable ]; + } + + /** + * Default column renderer + * + * @param object $item Webhook item. + * @param string $column_name Column name. + * @return string + */ + public function column_default( $item, $column_name ) { + switch ( $column_name ) { + case 'name': + return esc_html( $item->name ); + case 'event': + $events = $this->repository->get_allowed_events(); + return esc_html( $events[ $item->event ] ?? $item->event ); + case 'url': + return '' . esc_html( $item->url ) . ''; + case 'method': + return '' . esc_html( $item->method ) . ''; + case 'headers': + $count = is_array( $item->headers ) ? count( $item->headers ) : 0; + return $count > 0 ? sprintf( __( '%d headers', 'wp-graphql-webhooks' ), $count ) : '—'; + default: + return ''; + } + } + + /** + * Checkbox column + * + * @param object $item Webhook item. + * @return string + */ + public function column_cb( $item ) { + return sprintf( + '', + $item->id + ); + } + + /** + * Name column with row actions + * + * @param object $item Webhook item. + * @return string + */ + public function column_name( $item ) { + $edit_url = add_query_arg( [ + 'page' => 'graphql-webhooks', + 'action' => 'edit', + 'id' => $item->id, + ], admin_url( 'admin.php' ) ); + + $delete_url = wp_nonce_url( + add_query_arg( [ + 'page' => 'graphql-webhooks', + 'action' => 'delete', + 'webhook' => $item->id, + ], admin_url( 'admin.php' ) ), + 'delete-webhook-' . $item->id + ); + + $actions = [ + 'edit' => sprintf( '%s', esc_url( $edit_url ), __( 'Edit', 'wp-graphql-webhooks' ) ), + 'test' => sprintf( '%s', $item->id, __( 'Test', 'wp-graphql-webhooks' ) ), + 'delete' => sprintf( '%s', esc_url( $delete_url ), __( 'Delete', 'wp-graphql-webhooks' ) ), + ]; + + return sprintf( + '%s%s', + esc_url( $edit_url ), + esc_html( $item->name ), + $this->row_actions( $actions ) + ); + } + + /** + * Display when no items + */ + public function no_items() { + _e( 'No webhooks found.', 'wp-graphql-webhooks' ); + } + + /** + * Extra controls to be displayed between bulk actions and pagination + * + * @param string $which Top or bottom. + */ + protected function extra_tablenav( $which ) { + if ( 'top' === $which ) { + ?> +
+ +
+ + +
+ + + +
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..cc9e7abd --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhook-form.php @@ -0,0 +1,124 @@ + + +
+

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

+ + + + +

+
+
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..9af6f985 --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Admin/views/webhooks-list.php @@ -0,0 +1,64 @@ + + +
+

+ + + +
+ + +
+

+ +

+
+ +
+

+
+ +
+

+
+ + +
+ prepare_items(); + $list_table->display(); + ?> +
+
diff --git a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php index 3776fae7..207696a9 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php +++ b/plugins/wp-graphql-headless-webhooks/src/Events/WebhookEventManager.php @@ -47,6 +47,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 ); + + // Register test webhook event handler + add_action( 'graphql_webhooks_test_event', [ $this, 'on_test_webhook' ], 10, 2 ); } /** @@ -177,4 +180,22 @@ public function on_comment_status( $new_status, $old_status, $comment ) { 'new_status' => $new_status, ] ); } + + /** + * Handle test webhook event. + * + * @param \WPGraphQL\Webhooks\Entity\Webhook $webhook The webhook being tested. + * @param array $payload The test payload. + */ + public function on_test_webhook( $webhook, $payload = [] ) { + // For test webhooks, we want to send directly to the specific webhook + // Enable test mode for blocking requests during tests + add_filter( 'graphql_webhooks_test_mode', '__return_true' ); + + // Send the webhook with the test payload + $this->handler->handle( $webhook, $payload ); + + // Remove the test mode filter + remove_filter( 'graphql_webhooks_test_mode', '__return_true' ); + } } \ No newline at end of file diff --git a/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php b/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php index d546baca..eecd7d12 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php +++ b/plugins/wp-graphql-headless-webhooks/src/Handlers/WebhookHandler.php @@ -20,25 +20,90 @@ class WebhookHandler implements Handler { * @return void */ public function handle( Webhook $webhook, array $payload ): void { + // Log webhook dispatch initiation + $dispatch_timestamp = current_time( 'mysql' ); + error_log( "\n========== WEBHOOK DISPATCH ==========" ); + error_log( "Timestamp: {$dispatch_timestamp}" ); + error_log( "Webhook: {$webhook->name} (ID: {$webhook->id})" ); + error_log( "Event: {$webhook->event}" ); + error_log( "Target URL: {$webhook->url}" ); + error_log( "Method: {$webhook->method}" ); + $args = [ 'headers' => $webhook->headers ?: [ 'Content-Type' => 'application/json' ], - 'timeout' => 5, + 'timeout' => apply_filters( 'graphql_webhooks_timeout', 15 ), // Configurable timeout with a default of 15 seconds 'blocking' => false, + 'sslverify' => apply_filters( 'graphql_webhooks_sslverify', true, $webhook ), + 'user-agent' => 'WPGraphQL-Webhooks/' . ( defined( 'WPGRAPHQL_WEBHOOKS_VERSION' ) ? WPGRAPHQL_WEBHOOKS_VERSION : '1.0.0' ), + ]; + + // Apply payload filter + $payload = apply_filters( 'graphql_webhooks_payload', $payload, $webhook ); + + // Add webhook metadata to payload + $payload['_webhook_meta'] = [ + 'sent_at' => $dispatch_timestamp, + 'webhook_id' => $webhook->id, + 'webhook_name' => $webhook->name, + 'event_type' => $webhook->event, ]; - $payload = apply_filters( 'graphql_webhooks_payload', $payload, $webhook ); + // Handle different HTTP methods if ( strtoupper( $webhook->method ) === 'GET' ) { $url = add_query_arg( $payload, $webhook->url ); $args['method'] = 'GET'; + error_log( "Payload (GET query params): " . wp_json_encode( $payload ) ); } else { $url = $webhook->url; - $args['method'] = 'POST'; + $args['method'] = strtoupper( $webhook->method ); $args['body'] = wp_json_encode( $payload ); + + // Ensure Content-Type header is set for non-GET requests if ( empty( $args['headers']['Content-Type'] ) ) { $args['headers']['Content-Type'] = 'application/json'; } + + error_log( "Payload ({$args['method']} body): " . $args['body'] ); + error_log( "Payload size: " . strlen( $args['body'] ) . " bytes" ); + } + + // Log headers + error_log( "Headers: " . wp_json_encode( $args['headers'] ) ); + + // For test mode or debugging, optionally use blocking mode + if ( apply_filters( 'graphql_webhooks_test_mode', false, $webhook ) ) { + $args['blocking'] = true; + error_log( "Test mode enabled - using blocking request" ); + } + + error_log( "====================================\n" ); + + // Send the webhook + $start_time = microtime( true ); + $response = wp_remote_request( $url, $args ); + $end_time = microtime( true ); + $duration = round( ( $end_time - $start_time ) * 1000, 2 ); + + // Log response if in blocking mode + if ( $args['blocking'] ) { + if ( is_wp_error( $response ) ) { + error_log( "\n========== WEBHOOK ERROR ==========" ); + error_log( "❌ ERROR: " . $response->get_error_message() ); + error_log( "Duration: {$duration}ms" ); + error_log( "==================================\n" ); + } else { + $response_code = wp_remote_retrieve_response_code( $response ); + $response_body = wp_remote_retrieve_body( $response ); + + error_log( "\n========== WEBHOOK RESPONSE ==========" ); + error_log( ( $response_code >= 200 && $response_code < 300 ? "✅" : "⚠️" ) . " Response Code: {$response_code}" ); + error_log( "Duration: {$duration}ms" ); + error_log( "Response Body: " . ( strlen( $response_body ) > 500 ? substr( $response_body, 0, 500 ) . '...' : $response_body ) ); + error_log( "====================================\n" ); + } } - wp_remote_request( $url, $args ); - do_action( 'graphql_webhooks_sent', $webhook, $payload ); + + // Trigger action after webhook is sent + do_action( 'graphql_webhooks_sent', $webhook, $payload, $response ); } } \ No newline at end of file diff --git a/plugins/wp-graphql-headless-webhooks/src/Plugin.php b/plugins/wp-graphql-headless-webhooks/src/Plugin.php index 2f1a2ee6..51d49a1a 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Plugin.php +++ b/plugins/wp-graphql-headless-webhooks/src/Plugin.php @@ -89,6 +89,26 @@ private function setup(): void { // Initialize event manager and register hooks $eventManager = $this->services->get( 'event_manager' ); $eventManager->register_hooks(); + + // Initialize admin UI + if ( is_admin() ) { + $repository = $this->services->get( 'repository' ); + + if ( class_exists( 'WPGraphQL\Webhooks\Admin\WebhooksAdmin' ) ) { + $admin = new \WPGraphQL\Webhooks\Admin\WebhooksAdmin( $repository ); + $admin->init(); + } + } + + // Initialize REST endpoints + add_action( 'rest_api_init', function () { + $repository = $this->services->get( 'repository' ); + + if ( class_exists( 'WPGraphQL\Webhooks\Rest\WebhookTestEndpoint' ) ) { + $testEndpoint = new \WPGraphQL\Webhooks\Rest\WebhookTestEndpoint( $repository ); + $testEndpoint->register_routes(); + } + } ); } /** diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php b/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php index 422c1f06..885eeb9e 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php +++ b/plugins/wp-graphql-headless-webhooks/src/Repository/Interfaces/WebhookRepositoryInterface.php @@ -29,29 +29,31 @@ public function get(int $id): ?Webhook; /** * Creates a new webhook. * - * @param string $name The name (title) of the webhook. - * @param string $event The event identifier the webhook listens to. - * @param string $url The target URL the webhook will send data to. - * @param string $method The HTTP method to use when sending the webhook (e.g., 'POST'). - * @param array $headers Optional associative array of headers to send with the request. + * @param array $data The webhook data containing: + * - name (string): The name (title) of the webhook. + * - event (string): The event identifier the webhook listens to. + * - url (string): The target URL the webhook will send data to. + * - method (string): The HTTP method to use when sending the webhook (e.g., 'POST'). + * - headers (array): Optional associative array of headers to send with the request. * * @return int|\WP_Error The new webhook's post ID on success, or WP_Error on failure. */ - public function create(string $name, string $event, string $url, string $method, array $headers); + public function create(array $data); /** * Updates an existing webhook. * - * @param int $id The webhook post ID. - * @param string $name The updated name (title) of the webhook. - * @param string $event The updated event identifier. - * @param string $url The updated target URL. - * @param string $method The updated HTTP method. - * @param array $headers The updated array of headers. + * @param int $id The webhook post ID. + * @param array $data The webhook data containing: + * - name (string): The updated name (title) of the webhook. + * - event (string): The updated event identifier. + * - url (string): The updated target URL. + * - method (string): The updated HTTP method. + * - headers (array): The updated array of headers. * * @return bool|\WP_Error True on success, or WP_Error on failure. */ - public function update(int $id, string $name, string $event, string $url, string $method, array $headers); + public function update(int $id, array $data); /** * Deletes a webhook by its ID. diff --git a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php index 12eaf433..2f48a191 100644 --- a/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php +++ b/plugins/wp-graphql-headless-webhooks/src/Repository/WebhookRepository.php @@ -58,6 +58,16 @@ public function get_allowed_events(): array { return apply_filters( 'graphql_webhooks_allowed_events', $filtered_events ); } + /** + * Get the list of allowed HTTP methods. + * + * @return array Array of allowed HTTP methods. + */ + public function get_allowed_methods(): array { + $methods = [ 'POST', 'GET' ]; + return apply_filters( 'graphql_webhooks_allowed_methods', $methods ); + } + /** * Retrieve all published webhook entities. * @@ -96,15 +106,25 @@ public function get( $id ): ?Webhook { /** * Create a new webhook entity. * - * @param string $name Name/title of the webhook. - * @param string $event Event key the webhook listens to. - * @param string $url Target URL for the webhook request. - * @param string $method HTTP method (GET or POST). - * @param array $headers Associative array of HTTP headers. + * @param array $data { + * Webhook data. + * + * @type string $name Name/title of the webhook. + * @type string $event Event key the webhook listens to. + * @type string $url Target URL for the webhook request. + * @type string $method HTTP method (GET, POST, etc). + * @type array $headers Associative array of HTTP headers. + * } * * @return int|WP_Error Post ID on success, or WP_Error on failure. */ - public function create( $name, $event, $url, $method, $headers ) { + public function create( $data ) { + $name = $data['name'] ?? ''; + $event = $data['event'] ?? ''; + $url = $data['url'] ?? ''; + $method = $data['method'] ?? 'POST'; + $headers = $data['headers'] ?? []; + $validation = $this->validate_data( $event, $url, $method ); if ( is_wp_error( $validation ) ) { return $validation; @@ -131,21 +151,31 @@ public function create( $name, $event, $url, $method, $headers ) { /** * Update an existing webhook entity. * - * @param int $id Post ID of the webhook to update. - * @param string $name New name/title of the webhook. - * @param string $event New event key. - * @param string $url New target URL. - * @param string $method New HTTP method. - * @param array $headers New HTTP headers. + * @param int $id Post ID of the webhook to update. + * @param array $data { + * Webhook data. + * + * @type string $name New name/title of the webhook. + * @type string $event New event key. + * @type string $url New target URL. + * @type string $method New HTTP method. + * @type array $headers New HTTP headers. + * } * * @return bool|WP_Error True on success, or WP_Error on failure. */ - public function update( $id, $name, $event, $url, $method, $headers ) { + public function update( $id, $data ) { $post = get_post( $id ); if ( ! $post || $post->post_type !== 'graphql_webhook' ) { return new WP_Error( 'invalid_webhook', __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ) ); } + $name = $data['name'] ?? ''; + $event = $data['event'] ?? ''; + $url = $data['url'] ?? ''; + $method = $data['method'] ?? 'POST'; + $headers = $data['headers'] ?? []; + $validation = $this->validate_data( $event, $url, $method ); if ( is_wp_error( $validation ) ) { return $validation; @@ -202,7 +232,7 @@ 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 ) ) { + if ( ! in_array( strtoupper( $method ), $this->get_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/src/Rest/WebhookTestEndpoint.php b/plugins/wp-graphql-headless-webhooks/src/Rest/WebhookTestEndpoint.php new file mode 100644 index 00000000..4fc4481a --- /dev/null +++ b/plugins/wp-graphql-headless-webhooks/src/Rest/WebhookTestEndpoint.php @@ -0,0 +1,166 @@ +repository = $repository; + } + + /** + * Register REST routes. + */ + public function register_routes(): void { + register_rest_route( + 'graphql-webhooks/v1', + '/test', + [ + 'methods' => 'POST', + 'callback' => [ $this, 'test_webhook' ], + 'permission_callback' => [ $this, 'permission_callback' ], + 'args' => [ + 'webhook_id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ], + ], + ] + ); + } + + /** + * Permission callback. + * + * @return bool Whether user has permission. + */ + public function permission_callback(): bool { + return current_user_can( 'manage_options' ); + } + + /** + * Test a webhook. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Response. + */ + public function test_webhook( WP_REST_Request $request ): WP_REST_Response|WP_Error { + $webhook_id = $request->get_param( 'webhook_id' ); + $webhook = $this->repository->get( $webhook_id ); + + if ( ! $webhook ) { + return new WP_Error( + 'webhook_not_found', + __( 'Webhook not found.', 'wp-graphql-headless-webhooks' ), + [ 'status' => 404 ] + ); + } + + // Log test initiation + $test_timestamp = current_time( 'mysql' ); + error_log( "\n========== WEBHOOK TEST INITIATED ==========" ); + error_log( "Timestamp: {$test_timestamp}" ); + error_log( "Webhook ID: {$webhook_id}" ); + error_log( "Webhook Name: {$webhook->name}" ); + error_log( "Target URL: {$webhook->url}" ); + error_log( "HTTP Method: {$webhook->method}" ); + error_log( "Event: {$webhook->event}" ); + error_log( "Headers: " . wp_json_encode( $webhook->headers ) ); + error_log( "==========================================\n" ); + + // Create test payload + $test_payload = [ + 'event' => 'test', + 'timestamp' => $test_timestamp, + 'webhook' => [ + 'id' => $webhook->id, + 'name' => $webhook->name, + 'event' => $webhook->event, + ], + 'test_data' => [ + 'message' => 'This is a test webhook payload', + 'triggered_by' => wp_get_current_user()->user_login, + 'site_url' => get_site_url(), + ], + ]; + + // Allow filtering of test payload + $test_payload = apply_filters( 'graphql_webhooks_test_payload', $test_payload, $webhook ); + + // Trigger test event with enhanced logging + $start_time = microtime( true ); + + try { + do_action( 'graphql_webhooks_test_event', $webhook, $test_payload ); + + $end_time = microtime( true ); + $duration = round( ( $end_time - $start_time ) * 1000, 2 ); // Convert to milliseconds + + error_log( "\n========== WEBHOOK TEST COMPLETED ==========" ); + error_log( "✅ SUCCESS: Test webhook dispatched" ); + error_log( "Duration: {$duration}ms" ); + error_log( "Completed at: " . current_time( 'mysql' ) ); + error_log( "==========================================\n" ); + + return new WP_REST_Response( + [ + 'success' => true, + 'message' => __( 'Test webhook dispatched successfully.', 'wp-graphql-headless-webhooks' ), + 'details' => [ + 'webhook_id' => $webhook_id, + 'webhook_name' => $webhook->name, + 'target_url' => $webhook->url, + 'method' => $webhook->method, + 'duration_ms' => $duration, + 'timestamp' => $test_timestamp, + 'payload_size' => strlen( wp_json_encode( $test_payload ) ) . ' bytes', + ], + ], + 200 + ); + } catch ( \Exception $e ) { + error_log( "\n========== WEBHOOK TEST ERROR ==========" ); + error_log( "❌ ERROR: " . $e->getMessage() ); + error_log( "Stack trace: " . $e->getTraceAsString() ); + error_log( "========================================\n" ); + + return new WP_Error( + 'webhook_test_failed', + sprintf( + /* translators: %s: error message */ + __( 'Failed to dispatch test webhook: %s', 'wp-graphql-headless-webhooks' ), + $e->getMessage() + ), + [ 'status' => 500 ] + ); + } + } +}