Skip to content
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
36 changes: 23 additions & 13 deletions modules/notifications/notifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -554,8 +554,9 @@ function ( $user_id ) use ( $post_id ) {
* @since 0.8
*/
public function handle_user_post_subscription() {
// Require a valid nonce for this AJAX request.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
if ( ! empty( $_GET['_wpnonce'] ) && ! wp_verify_nonce( $_GET['_wpnonce'], 'ef_notifications_user_post_subscription' ) ) {
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'ef_notifications_user_post_subscription' ) ) {
$this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
}

Expand Down Expand Up @@ -599,21 +600,30 @@ public function save_post_subscriptions( $new_status, $old_status, $post ) {
return;
}

// Only process if Edit Flow's followers form was submitted.
if ( ! isset( $_POST['ef-save_followers'] ) ) {
return;
}

// Check capability.
if ( ! current_user_can( $this->edit_post_subscriptions_cap ) ) {
return;
}

// Verify Edit Flow's own nonce. Return early if missing or invalid - don't die,
// as this hook fires on all post transitions including non-admin contexts.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
if ( ! empty( $_POST['_wpnonce'] ) && ! wp_verify_nonce( $_POST['_wpnonce'], 'update-post_' . $post->ID ) ) {
$this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
if ( ! isset( $_POST['ef_notifications_nonce'] ) || ! wp_verify_nonce( $_POST['ef_notifications_nonce'], 'save_user_usergroups' ) ) {
return;
}

// Only if has edit_post_subscriptions cap.
if ( isset( $_POST['ef-save_followers'] ) && current_user_can( $this->edit_post_subscriptions_cap ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved.
$users = isset( $_POST['ef-selected-users'] ) ? $_POST['ef-selected-users'] : [];
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved.
$usergroups = isset( $_POST['following_usergroups'] ) ? $_POST['following_usergroups'] : [];
$this->save_post_following_users( $post, $users );
if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) {
$this->save_post_following_usergroups( $post, $usergroups );
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved.
$users = isset( $_POST['ef-selected-users'] ) ? $_POST['ef-selected-users'] : [];
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved.
$usergroups = isset( $_POST['following_usergroups'] ) ? $_POST['following_usergroups'] : [];
$this->save_post_following_users( $post, $users );
if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) {
$this->save_post_following_usergroups( $post, $usergroups );
}
}

Expand Down
105 changes: 105 additions & 0 deletions tests/Integration/NotificationsAjaxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,109 @@ public function test_ajax_save_persists_usergroup_subscriptions(): void {
$edit_flow->user_groups->delete_usergroup( $group1->term_id );
$edit_flow->user_groups->delete_usergroup( $group2->term_id );
}

/**
* Test: handle_user_post_subscription requires a nonce.
*
* This tests the fix for the nonce check logic. Previously the check was:
* `if ( ! empty( $_GET['_wpnonce'] ) && ! wp_verify_nonce(...) )`
* which allowed requests without any nonce to pass through.
*
* The fix changes it to:
* `if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce(...) )`
* which requires a valid nonce.
*
* @ticket https://github.com/Automattic/edit-flow/issues/882
* @covers EF_Notifications::handle_user_post_subscription
*/
public function test_handle_user_post_subscription_requires_nonce(): void {
wp_set_current_user( self::$admin_user_id );

$post_id = $this->factory->post->create();

// Make request WITHOUT a nonce - this should fail now.
$_GET['post_id'] = $post_id;
$_GET['method'] = 'follow';
// Intentionally NOT setting $_GET['_wpnonce']

// print_ajax_response outputs JSON before dying, so we get WPAjaxDieContinueException.
try {
$this->_handleAjax( 'ef_notifications_user_post_subscription' );
} catch ( WPAjaxDieContinueException $e ) {
unset( $e );
}

// Verify error response.
$response = json_decode( $this->_last_response, true );
$this->assertIsArray( $response, 'Response should be valid JSON' );
$this->assertEquals( 'error', $response['status'], 'Response should indicate error when nonce is missing' );
}

/**
* Test: handle_user_post_subscription fails with invalid nonce.
*
* @ticket https://github.com/Automattic/edit-flow/issues/882
* @covers EF_Notifications::handle_user_post_subscription
*/
public function test_handle_user_post_subscription_fails_with_invalid_nonce(): void {
wp_set_current_user( self::$admin_user_id );

$post_id = $this->factory->post->create();

$_GET['_wpnonce'] = 'invalid_nonce';
$_GET['post_id'] = $post_id;
$_GET['method'] = 'follow';

// print_ajax_response outputs JSON before dying, so we get WPAjaxDieContinueException.
try {
$this->_handleAjax( 'ef_notifications_user_post_subscription' );
} catch ( WPAjaxDieContinueException $e ) {
unset( $e );
}

// Verify error response.
$response = json_decode( $this->_last_response, true );
$this->assertIsArray( $response, 'Response should be valid JSON' );
$this->assertEquals( 'error', $response['status'], 'Response should indicate error when nonce is invalid' );
}

/**
* Test: handle_user_post_subscription succeeds with valid nonce.
*
* @ticket https://github.com/Automattic/edit-flow/issues/882
* @covers EF_Notifications::handle_user_post_subscription
*/
public function test_handle_user_post_subscription_succeeds_with_valid_nonce(): void {
global $edit_flow;

wp_set_current_user( self::$admin_user_id );

$post_id = $this->factory->post->create(
array(
'post_author' => self::$admin_user_id,
'post_status' => 'draft',
)
);

$_GET['_wpnonce'] = wp_create_nonce( 'ef_notifications_user_post_subscription' );
$_GET['post_id'] = $post_id;
$_GET['method'] = 'follow';

try {
$this->_handleAjax( 'ef_notifications_user_post_subscription' );
} catch ( WPAjaxDieContinueException $e ) {
unset( $e );
}

// Verify we got a success response.
$this->assertNotEmpty( $this->_last_response, 'AJAX should return a response' );

$response = json_decode( $this->_last_response, true );
$this->assertIsArray( $response, 'Response should be valid JSON' );
$this->assertEquals( 'success', $response['status'], 'Response should indicate success' );

// Verify the user is now following the post.
$following_users = $edit_flow->notifications->get_following_users( $post_id, 'id' );
$this->assertContains( self::$admin_user_id, $following_users, 'User should be following the post' );
}
}
Loading