Skip to content

fix: inability to delete webhooks via UI #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/wp-graphql-headless-webhooks/assets/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'click',
function () {
var headerRow = $( wpGraphQLWebhooks.headerTemplate || wpGraphQLWebhooks.headerRowTemplate );
$( '#webhook-headers-container, #webhook-headers' ).append( headerRow );
$( '#webhook-headers-container' ).append( headerRow );
}
);

Expand Down
108 changes: 51 additions & 57 deletions plugins/wp-graphql-headless-webhooks/src/Admin/WebhooksAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,38 +41,34 @@ class WebhooksAdmin {
*/
public function __construct( WebhookRepositoryInterface $repository ) {
$this->repository = $repository;

add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'admin_post_graphql_webhook_save', [ $this, 'handle_webhook_save' ] );
add_action( 'admin_post_graphql_webhook_delete', [ $this, 'handle_webhook_delete' ] );
add_action( 'admin_init', [ $this, 'handle_admin_actions' ] );
add_action( 'wp_ajax_test_webhook', [ $this, 'ajax_test_webhook' ] );
}

/**
* Optionally initialize additional admin hooks.
* Initialize the admin functionality.
*
* @return void
*/
public function init(): void {
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'admin_init', [ $this, 'handle_actions' ] );
add_action( 'wp_ajax_test_webhook', [ $this, 'ajax_test_webhook' ] );
}

/**
* Registers the top-level "Webhooks" admin menu.
* Registers the webhooks submenu under the GraphQL admin menu.
*
* @return void
*/
public function add_admin_menu(): void {
add_menu_page(
__( 'Webhooks', 'wp-graphql-headless-webhooks' ),
// Add submenu under GraphQL menu using the correct parent slug
add_submenu_page(
'graphiql-ide',
__( 'GraphQL Webhooks', 'wp-graphql-headless-webhooks' ),
__( 'Webhooks', 'wp-graphql-headless-webhooks' ),
'manage_options',
self::ADMIN_PAGE_SLUG,
[ $this, 'render_admin_page' ],
'dashicons-rss',
25
[ $this, 'render_admin_page' ]
);
}

Expand All @@ -97,6 +93,11 @@ public function get_admin_url( array $args = [] ): string {
* @return void
*/
public function enqueue_assets( string $hook ): void {
// Only enqueue on our admin page
if ( false === strpos( $hook, self::ADMIN_PAGE_SLUG ) ) {
return;
}

wp_enqueue_style(
'graphql-webhooks-admin',
WPGRAPHQL_HEADLESS_WEBHOOKS_PLUGIN_URL . 'assets/css/admin.css',
Expand Down Expand Up @@ -136,24 +137,6 @@ private function get_header_row_template(): string {
return ob_get_clean();
}

/**
* Handles admin actions from the webhooks page.
*
* @return void
*/
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();
}
}

/**
* Checks if the current user has permission to manage options.
Expand Down Expand Up @@ -227,44 +210,55 @@ public function handle_webhook_save() {
}

/**
* Handles deleting a webhook.
* Handles admin actions from the webhooks page.
*
* @return void
*/
public function handle_webhook_delete() {
// To be implemented: Individual deletes are handled through the list table's handle_row_actions.
public function handle_actions(): void {
if ( ! isset( $_REQUEST['page'] ) || self::ADMIN_PAGE_SLUG !== $_REQUEST['page'] ) {
return;
}

// Handle save action
if ( isset( $_POST['action'] ) && 'save_webhook' === $_POST['action'] ) {
$this->handle_webhook_save();
}

// Handle delete action
if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] && isset( $_GET['webhook'] ) ) {
$this->handle_webhook_delete();
}
}

/**
* Handles bulk admin actions (such as bulk delete).
* Handles single webhook deletion.
*
* @return void
*/
public function handle_admin_actions() {
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++;
}
}
private function handle_webhook_delete(): void {
// Verify permissions
if ( ! $this->verify_admin_permission() ) {
return;
}

if ( $deleted > 0 ) {
wp_redirect( add_query_arg( [ 'deleted' => $deleted ], $this->get_admin_url() ) );
exit;
}
// Get webhook ID
$webhook_id = intval( $_GET['webhook'] );
$nonce = isset( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : '';

// Verify nonce
if ( ! wp_verify_nonce( $nonce, 'delete-webhook-' . $webhook_id ) ) {
wp_die( __( 'Security check failed.', 'wp-graphql-headless-webhooks' ) );
}

// Delete webhook
$deleted = $this->repository->delete( $webhook_id ) ? 1 : 0;

// Redirect with result
wp_redirect( add_query_arg( [ 'deleted' => $deleted ], remove_query_arg( [ 'action', 'webhook', '_wpnonce' ], $this->get_admin_url() ) ) );
exit;
}


/**
* Renders the webhooks admin page.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,45 +77,56 @@ public function get_sortable_columns() {
*/
public function get_bulk_actions() {
return [
'bulk-delete' => __( 'Delete', 'wp-graphql-webhooks' ),
'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;
}
// Only handle delete action
if ( 'delete' !== $this->current_action() ) {
return;
}

// 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;

// Verify nonce
if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-' . $this->_args['plural'] ) ) {
wp_die( __( 'Security check failed.', 'wp-graphql-webhooks' ) );
}

// Check permissions
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'You do not have sufficient permissions to access this page.', 'wp-graphql-headless-webhooks' ) );
}

// Get selected webhooks
$webhook_ids = isset( $_REQUEST['webhook'] ) ? array_map( 'intval', (array) $_REQUEST['webhook'] ) : [];
if ( empty( $webhook_ids ) ) {
return;
}

// Delete webhooks
$deleted = 0;
foreach ( $webhook_ids as $webhook_id ) {
if ( $this->repository->delete( $webhook_id ) ) {
$deleted++;
}
}

// Redirect with success message
if ( $deleted > 0 ) {
wp_redirect( add_query_arg( [ 'deleted' => $deleted ], remove_query_arg( [ 'action', 'action2', 'webhook', '_wpnonce' ] ) ) );
exit;
}
}

/**
* Prepare items for display
*/
public function prepare_items() {
// Process bulk actions first
$this->process_bulk_action();

$per_page = $this->get_items_per_page( 'webhooks_per_page', 20 );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
<div class="wrap">
<h1><?php echo esc_html( $form_title ); ?></h1>

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<form method="post">
<?php wp_nonce_field( 'webhook_save', 'webhook_nonce' ); ?>
<input type="hidden" name="action" value="graphql_webhook_save">
<input type="hidden" name="action" value="save_webhook">
<?php if ( $webhook ) : ?>
<input type="hidden" name="webhook_id" value="<?php echo esc_attr( $webhook->id ); ?>">
<?php endif; ?>
Expand Down