Skip to content
Open
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
78 changes: 78 additions & 0 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -3163,6 +3163,84 @@ function generic_ping( $post_id = 0 ) {
return $post_id;
}

/**
* Determines whether pings should be disabled for the current environment.
*
* By default, all pings (outgoing pingbacks, trackbacks, and ping service
* notifications, as well as incoming pingbacks and trackbacks) are disabled
* for non-production environments ('local', 'development', 'staging').
*
* @since 7.1.0
*
* @return bool True if pings should be disabled, false otherwise.
*/
function wp_should_disable_pings_for_environment() {
$environment_type = wp_get_environment_type();
$should_disable = 'production' !== $environment_type;

/**
* Filters whether pings should be disabled for the current environment.
*
* Returning false re-enables pings in non-production environments.
* Returning true disables pings even in production.
*
* @since 7.1.0
*
* @param bool $should_disable Whether pings should be disabled. Default true
* for non-production environments, false for production.
* @param string $environment_type The current environment type as returned by
* wp_get_environment_type().
*/
return apply_filters( 'wp_should_disable_pings_for_environment', $should_disable, $environment_type );
}

/**
* Removes outgoing ping callbacks in non-production environments.
*
* Hooked to `do_all_pings` at priority 1 so it runs before the default
* priority 10 callbacks. Does not remove `do_all_enclosures`.
*
* @since 7.1.0
*/
function wp_maybe_disable_outgoing_pings_for_environment() {
if ( wp_should_disable_pings_for_environment() ) {
remove_action( 'do_all_pings', 'do_all_pingbacks' );
remove_action( 'do_all_pings', 'do_all_trackbacks' );
remove_action( 'do_all_pings', 'generic_ping' );
}
}

/**
* Rejects incoming trackbacks in non-production environments.
*
* Hooked to `pre_trackback_post` which fires in `wp-trackback.php` before the
* trackback is processed. Calls `trackback_response()` which sends an XML error
* response and terminates the request.
*
* @since 7.1.0
*/
function wp_maybe_disable_trackback_for_environment() {
if ( wp_should_disable_pings_for_environment() ) {
trackback_response( 1, __( 'Trackbacks are disabled in non-production environments.' ) );
}
}

/**
* Removes the pingback XML-RPC method in non-production environments.
*
* @since 7.1.0
*
* @param string[] $methods An array of XML-RPC methods, keyed by their methodName.
* @return string[] Modified array of XML-RPC methods.
*/
function wp_maybe_disable_xmlrpc_pingback_for_environment( $methods ) {
if ( wp_should_disable_pings_for_environment() ) {
unset( $methods['pingback.ping'] );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we intentionally leaving the readonly pingback.extensions.getPingbacks() intact for non-production environments?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly it was a bit of an oversight initially, but thinking about it more I'm leaning toward leaving it alone. It's a read-only SELECT against the comments table, so it feels like it sits outside what this PR is really targeting (staging pinging prod, or accepting incoming pings).

The other thing that stopped me was that existing pingbacks are still visible in the admin, REST API, get_comments(), etc. on non-prod. Stripping just this one read method while leaving all those alone felt inconsistent. I am open on this though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe we should leave it alone. Maybe the site owner disabled pingbacks after having them enabled for a while before.

}

return $methods;
}

/**
* Pings back the links found in a post.
*
Expand Down
6 changes: 6 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,12 @@
add_action( 'do_all_pings', 'do_all_enclosures', 10, 0 );
add_action( 'do_all_pings', 'do_all_trackbacks', 10, 0 );
add_action( 'do_all_pings', 'generic_ping', 10, 0 );

// Disable pings (pingbacks, trackbacks, and ping service notifications) in non-production environments.
add_action( 'do_all_pings', 'wp_maybe_disable_outgoing_pings_for_environment', 1, 0 );
add_action( 'pre_trackback_post', 'wp_maybe_disable_trackback_for_environment', 10, 0 );
add_filter( 'xmlrpc_methods', 'wp_maybe_disable_xmlrpc_pingback_for_environment' );

add_action( 'do_robots', 'do_robots' );
add_action( 'do_favicon', 'do_favicon' );
add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer', 1000 ); // Late priority to let `wp_template_enhancement_output_buffer` filters and `wp_finalized_template_enhancement_output_buffer` actions be registered.
Expand Down
254 changes: 254 additions & 0 deletions tests/phpunit/tests/comment/disablePingsForEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
<?php

/**
* Tests for disabling pings in non-production environments.
*
* @group comment
* @covers ::wp_should_disable_pings_for_environment
* @covers ::wp_maybe_disable_outgoing_pings_for_environment
* @covers ::wp_maybe_disable_trackback_for_environment
* @covers ::wp_maybe_disable_xmlrpc_pingback_for_environment
*
* @ticket 64837
*/
class Tests_Comment_DisablePingsForEnvironment extends WP_UnitTestCase {

/**
* Stores the original WP_ENVIRONMENT_TYPE env value for cleanup.
*
* @var string|false
*/
private $original_env;

public function set_up() {
parent::set_up();
$this->original_env = getenv( 'WP_ENVIRONMENT_TYPE' );
}

public function tear_down() {
if ( false === $this->original_env ) {
putenv( 'WP_ENVIRONMENT_TYPE' );
} else {
putenv( 'WP_ENVIRONMENT_TYPE=' . $this->original_env );
}
parent::tear_down();
}

/**
* @ticket 64837
*/
public function test_should_disable_returns_true_for_local() {
putenv( 'WP_ENVIRONMENT_TYPE=local' );
$this->assertTrue( wp_should_disable_pings_for_environment() );
}

/**
* @ticket 64837
*/
public function test_should_disable_returns_true_for_development() {
putenv( 'WP_ENVIRONMENT_TYPE=development' );
$this->assertTrue( wp_should_disable_pings_for_environment() );
}

/**
* @ticket 64837
*/
public function test_should_disable_returns_true_for_staging() {
putenv( 'WP_ENVIRONMENT_TYPE=staging' );
$this->assertTrue( wp_should_disable_pings_for_environment() );
}

/**
* @ticket 64837
*/
public function test_should_disable_returns_false_for_production() {
putenv( 'WP_ENVIRONMENT_TYPE=production' );
$this->assertFalse( wp_should_disable_pings_for_environment() );
}

/**
* @ticket 64837
*/
public function test_filter_can_enable_pings_in_non_production() {
putenv( 'WP_ENVIRONMENT_TYPE=local' );
add_filter( 'wp_should_disable_pings_for_environment', '__return_false' );

$this->assertFalse( wp_should_disable_pings_for_environment() );
}

/**
* @ticket 64837
*/
public function test_filter_can_disable_pings_in_production() {
putenv( 'WP_ENVIRONMENT_TYPE=production' );
add_filter( 'wp_should_disable_pings_for_environment', '__return_true' );

$this->assertTrue( wp_should_disable_pings_for_environment() );
}

/**
* @ticket 64837
*/
public function test_filter_receives_environment_type() {
putenv( 'WP_ENVIRONMENT_TYPE=staging' );

$received_type = null;
add_filter(
'wp_should_disable_pings_for_environment',
function ( $should_disable, $environment_type ) use ( &$received_type ) {
$received_type = $environment_type;
return $should_disable;
},
10,
2
);

wp_should_disable_pings_for_environment();

$this->assertSame( 'staging', $received_type );
}

/**
* @ticket 64837
*/
public function test_outgoing_pingbacks_removed_in_non_production() {
putenv( 'WP_ENVIRONMENT_TYPE=development' );

// Re-register the defaults to ensure a clean state.
add_action( 'do_all_pings', 'do_all_pingbacks', 10, 0 );

// Fire the priority-1 callback.
wp_maybe_disable_outgoing_pings_for_environment();

$this->assertFalse( has_action( 'do_all_pings', 'do_all_pingbacks' ) );
}

/**
* @ticket 64837
*/
public function test_outgoing_trackbacks_removed_in_non_production() {
putenv( 'WP_ENVIRONMENT_TYPE=development' );

add_action( 'do_all_pings', 'do_all_trackbacks', 10, 0 );

wp_maybe_disable_outgoing_pings_for_environment();

$this->assertFalse( has_action( 'do_all_pings', 'do_all_trackbacks' ) );
}

/**
* @ticket 64837
*/
public function test_outgoing_generic_ping_removed_in_non_production() {
putenv( 'WP_ENVIRONMENT_TYPE=development' );

add_action( 'do_all_pings', 'generic_ping', 10, 0 );

wp_maybe_disable_outgoing_pings_for_environment();

$this->assertFalse( has_action( 'do_all_pings', 'generic_ping' ) );
}

/**
* @ticket 64837
*/
public function test_enclosures_not_removed_in_non_production() {
putenv( 'WP_ENVIRONMENT_TYPE=development' );

add_action( 'do_all_pings', 'do_all_enclosures', 10, 0 );

wp_maybe_disable_outgoing_pings_for_environment();

$this->assertTrue( has_action( 'do_all_pings', 'do_all_enclosures', 10 ) );
}

/**
* @ticket 64837
*/
public function test_outgoing_pings_preserved_in_production() {
putenv( 'WP_ENVIRONMENT_TYPE=production' );

add_action( 'do_all_pings', 'do_all_pingbacks', 10, 0 );
add_action( 'do_all_pings', 'do_all_trackbacks', 10, 0 );
add_action( 'do_all_pings', 'generic_ping', 10, 0 );

wp_maybe_disable_outgoing_pings_for_environment();

$this->assertTrue( has_action( 'do_all_pings', 'do_all_pingbacks', 10 ), 'do_all_pingbacks should still be hooked at priority 10.' );
$this->assertTrue( has_action( 'do_all_pings', 'do_all_trackbacks', 10 ), 'do_all_trackbacks should still be hooked at priority 10.' );
$this->assertTrue( has_action( 'do_all_pings', 'generic_ping', 10 ), 'generic_ping should still be hooked at priority 10.' );
}

/**
* @ticket 64837
*/
public function test_trackback_hook_is_registered() {
$this->assertTrue( has_action( 'pre_trackback_post', 'wp_maybe_disable_trackback_for_environment', 10 ) );
}

/**
* @ticket 64837
*/
public function test_pings_open_unaffected_by_environment() {
putenv( 'WP_ENVIRONMENT_TYPE=local' );

$post = self::factory()->post->create_and_get(
array( 'ping_status' => 'open' )
);

$this->assertTrue( pings_open( $post ) );
}

/**
* @ticket 64837
*/
public function test_xmlrpc_pingback_removed_in_non_production() {
putenv( 'WP_ENVIRONMENT_TYPE=development' );

$methods = array(
'pingback.ping' => 'this:pingback_ping',
'pingback.extensions.getPingbacks' => 'this:pingback_extensions_getPingbacks',
'wp.getUsersBlogs' => 'this:wp_getUsersBlogs',
);

$filtered = wp_maybe_disable_xmlrpc_pingback_for_environment( $methods );

$this->assertArrayNotHasKey( 'pingback.ping', $filtered );
}

/**
* @ticket 64837
*/
public function test_xmlrpc_pingback_preserved_in_production() {
putenv( 'WP_ENVIRONMENT_TYPE=production' );

$methods = array(
'pingback.ping' => 'this:pingback_ping',
'wp.getUsersBlogs' => 'this:wp_getUsersBlogs',
);

$filtered = wp_maybe_disable_xmlrpc_pingback_for_environment( $methods );

$this->assertArrayHasKey( 'pingback.ping', $filtered );
}

/**
* @ticket 64837
*/
public function test_xmlrpc_other_methods_preserved_in_non_production() {
putenv( 'WP_ENVIRONMENT_TYPE=development' );

$methods = array(
'pingback.ping' => 'this:pingback_ping',
'pingback.extensions.getPingbacks' => 'this:pingback_extensions_getPingbacks',
'wp.getUsersBlogs' => 'this:wp_getUsersBlogs',
'wp.getPost' => 'this:wp_getPost',
);

$filtered = wp_maybe_disable_xmlrpc_pingback_for_environment( $methods );

$this->assertArrayHasKey( 'pingback.extensions.getPingbacks', $filtered );
$this->assertArrayHasKey( 'wp.getUsersBlogs', $filtered );
$this->assertArrayHasKey( 'wp.getPost', $filtered );
}
}
Loading