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
77 changes: 69 additions & 8 deletions includes/content-gate/class-ip-access-rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,34 @@ public static function add_rewrite_rule() {
}

/**
* Register the REST API route for IP checking.
* Register the REST API routes for IP checking.
*/
public static function register_rest_route() {
\register_rest_route(
NEWSPACK_API_NAMESPACE,
self::REST_ROUTE,
[
'methods' => 'GET',
'callback' => [ __CLASS__, 'check_ip_rest' ],
'permission_callback' => '__return_true',
'args' => [
'institution_id' => [
'type' => 'integer',
'sanitize_callback' => 'absint',
[
'methods' => 'GET',
'callback' => [ __CLASS__, 'check_ip_rest' ],
'permission_callback' => '__return_true',
'args' => [
'institution_id' => [
'type' => 'integer',
'sanitize_callback' => 'absint',
],
],
],
[
'methods' => 'POST',
'callback' => [ __CLASS__, 'check_external_ip_rest' ],
'permission_callback' => [ __CLASS__, 'check_external_ip_permission' ],
'args' => [
Comment thread
rbcorrales marked this conversation as resolved.
'ip' => [
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
],
]
Expand Down Expand Up @@ -136,6 +150,53 @@ public static function check_ip_rest( $request ) {
return new \WP_REST_Response( $data );
}

/**
* REST API callback for external IP queries via POST.
*
* Accepts a JSON body with an `ip` field and checks it against all
* institutional IP ranges. Designed for server-to-server calls from
* external platforms.
*
* Example request:
*
* POST /wp-json/newspack/v1/institutional-access/check
* Content-Type: application/json
*
* {"ip": "127.0.0.1"}
*
* @param \WP_REST_Request $request The REST request.
* @return \WP_REST_Response|\WP_Error
*/
public static function check_external_ip_rest( $request ) {
$ip = $request->get_param( 'ip' );
if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
return new \WP_Error(
'rest_invalid_param',
'Only IPv4 addresses are supported.',
[ 'status' => 400 ]
);
}

$override = fn() => $ip;
add_filter( 'newspack_visitor_ip', $override );

/** This filter is documented in self::handle_redirect(). */
$result = apply_filters( 'newspack_content_gate_check_ip', false );

remove_filter( 'newspack_visitor_ip', $override );
Comment thread
rbcorrales marked this conversation as resolved.

return new \WP_REST_Response( [ 'show_paywall' => ! (bool) $result ] );
}

/**
* Permission check for the external IP query endpoint.
*
* @return bool
*/
public static function check_external_ip_permission() {
return current_user_can( 'manage_options' );
}

/**
* Handle the institutional access check.
*
Expand Down
91 changes: 91 additions & 0 deletions tests/unit-tests/content-gate/class-ip-access-rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,95 @@ public function test_rest_endpoint_institution_id_invalid() {
$this->assertFalse( $data['valid'] );
$this->assertArrayNotHasKey( 'institution', $data );
}

/**
* Test the POST route is registered.
*/
public function test_post_route_registered() {
do_action( 'rest_api_init' );

$routes = rest_get_server()->get_routes( NEWSPACK_API_NAMESPACE );
$expected_route = '/' . NEWSPACK_API_NAMESPACE . IP_Access_Rule::REST_ROUTE;
$endpoint = $routes[ $expected_route ][1];
$this->assertArrayHasKey( 'POST', $endpoint['methods'], 'The route should accept POST requests.' );
Comment thread
rbcorrales marked this conversation as resolved.
}

/**
* Test POST requires manage_options capability.
*/
public function test_post_requires_authentication() {
$route = '/' . NEWSPACK_API_NAMESPACE . IP_Access_Rule::REST_ROUTE;

// Unauthenticated request should be forbidden.
$request = new WP_REST_Request( 'POST', $route );
$request->set_param( 'ip', '10.0.0.1' );
$response = rest_do_request( $request );
$this->assertSame( 401, $response->get_status() );

// Non-admin user should be forbidden.
wp_set_current_user( self::factory()->user->create( [ 'role' => 'subscriber' ] ) );
$request = new WP_REST_Request( 'POST', $route );
$request->set_param( 'ip', '10.0.0.1' );
$response = rest_do_request( $request );
$this->assertSame( 403, $response->get_status() );
}

/**
* Test POST returns 400 for missing or invalid ip param.
*/
public function test_post_requires_valid_ip_param() {
wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) );

$route = '/' . NEWSPACK_API_NAMESPACE . IP_Access_Rule::REST_ROUTE;

// Missing param (handled by WP REST required arg validation).
$request = new WP_REST_Request( 'POST', $route );
$response = rest_do_request( $request );
$this->assertSame( 400, $response->get_status() );

// Invalid IP.
$request = new WP_REST_Request( 'POST', $route );
$request->set_param( 'ip', 'not-an-ip' );
$response = rest_do_request( $request );
$this->assertSame( 400, $response->get_status() );

// IPv6 not supported.
$request = new WP_REST_Request( 'POST', $route );
$request->set_param( 'ip', '2001:db8::1' );
$response = rest_do_request( $request );
$this->assertSame( 400, $response->get_status() );
}

/**
* Test POST returns correct show_paywall value.
*/
public function test_post_show_paywall_response() {
wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) );

$route = '/' . NEWSPACK_API_NAMESPACE . IP_Access_Rule::REST_ROUTE;

// No match: show paywall.
$request = new WP_REST_Request( 'POST', $route );
$request->set_param( 'ip', '203.0.113.50' );
$response = rest_do_request( $request );
$data = $response->get_data();
$this->assertSame( 200, $response->get_status() );
$this->assertTrue( $data['show_paywall'] );

// Match: hide paywall.
add_filter(
'newspack_content_gate_check_ip',
function () {
return 123;
}
);
Comment thread
rbcorrales marked this conversation as resolved.
$request = new WP_REST_Request( 'POST', $route );
$request->set_param( 'ip', '10.0.0.1' );
$response = rest_do_request( $request );
$data = $response->get_data();
$this->assertSame( 200, $response->get_status() );
$this->assertFalse( $data['show_paywall'] );

remove_all_filters( 'newspack_content_gate_check_ip' );
Comment thread
rbcorrales marked this conversation as resolved.
}
}
Loading