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
1 change: 1 addition & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@
add_action( 'init', 'rest_api_init' );
add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 );
add_action( 'rest_api_init', 'register_initial_settings', 10 );
add_filter( 'rest_pre_update_setting', 'rest_restrict_privacy_policy_page_setting_update', 10, 2 );
add_action( 'rest_api_init', 'create_initial_rest_routes', 99 );
add_action( 'parse_request', 'rest_api_loaded' );

Expand Down
13 changes: 13 additions & 0 deletions src/wp-includes/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -2739,6 +2739,7 @@ function set_site_transient( $transient, $value, $expiration = 0 ) {
*
* @since 4.7.0
* @since 6.0.1 The `show_on_front`, `page_on_front`, and `page_for_posts` options were added.
* @since 7.2.0 The `wp_page_for_privacy_policy` option was registered, exposed as `page_for_privacy_policy`.
*/
function register_initial_settings() {
register_setting(
Expand Down Expand Up @@ -2931,6 +2932,18 @@ function register_initial_settings() {
)
);

register_setting(
'reading',
'wp_page_for_privacy_policy',
array(
'show_in_rest' => array(
'name' => 'page_for_privacy_policy',
),
'type' => 'integer',
'description' => __( 'The ID of the page that should be displayed as the privacy policy page' ),
)
);

register_setting(
'discussion',
'default_ping_status',
Expand Down
21 changes: 21 additions & 0 deletions src/wp-includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -3447,6 +3447,27 @@ function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::C
return $endpoint_args;
}

/**
* Prevents users without the `manage_privacy_options` capability from
* changing the privacy policy page through the REST API.
*
* The settings endpoint only checks `manage_options`. On multisite the
* `manage_privacy_options` capability maps to `manage_network`, so a site
* administrator can read the setting but must not change it, matching the
* Settings > Privacy screen.
*
* @since 7.2.0
*
* @param bool $updated Whether the setting update has already been handled.
* @param string $name Setting name (as shown in REST API responses).
* @return bool Whether to short-circuit the update.
*/
function rest_restrict_privacy_policy_page_setting_update( $updated, $name ) {
if ( 'page_for_privacy_policy' === $name && ! current_user_can( 'manage_privacy_options' ) ) {
return true;
}
return $updated;
}

/**
* Converts an error to a response object.
Expand Down
49 changes: 49 additions & 0 deletions tests/phpunit/tests/rest-api/rest-settings-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,26 @@ public function tear_down() {
}
}

remove_filter( 'map_meta_cap', array( $this, 'deny_manage_privacy_options' ), 10 );

parent::tear_down();
}

/**
* Maps `manage_privacy_options` to `do_not_allow`, as happens for a site
* administrator on multisite.
*
* @param string[] $caps Primitive capabilities required.
* @param string $cap Capability being checked.
* @return string[] Primitive capabilities required.
*/
public function deny_manage_privacy_options( $caps, $cap ) {
if ( 'manage_privacy_options' === $cap ) {
return array( 'do_not_allow' );
}
return $caps;
}

public function test_register_routes() {
$routes = rest_get_server()->get_routes();
$this->assertArrayHasKey( '/wp/v2/settings', $routes );
Expand Down Expand Up @@ -116,6 +133,7 @@ public function test_get_items() {
'show_on_front',
'page_on_front',
'page_for_posts',
'page_for_privacy_policy',
'default_ping_status',
'default_comment_status',
'site_icon', // Registered in wp-includes/blocks/site-logo.php
Expand Down Expand Up @@ -403,6 +421,37 @@ public function test_update_item() {
$this->assertSame( get_option( 'blogname' ), $data['title'] );
}

public function test_update_item_privacy_policy_page() {
wp_set_current_user( self::$administrator );
$page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );

$request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
$request->set_param( 'page_for_privacy_policy', $page_id );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();

$this->assertSame( 200, $response->get_status() );
$this->assertSame( $page_id, $data['page_for_privacy_policy'] );
$this->assertSame( $page_id, (int) get_option( 'wp_page_for_privacy_policy' ) );
}

public function test_update_item_privacy_policy_page_without_capability() {
wp_set_current_user( self::$administrator );
$page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
update_option( 'wp_page_for_privacy_policy', $page_id );
$other_page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
add_filter( 'map_meta_cap', array( $this, 'deny_manage_privacy_options' ), 10, 2 );

$request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
$request->set_param( 'page_for_privacy_policy', $other_page_id );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();

$this->assertSame( 200, $response->get_status() );
$this->assertSame( $page_id, $data['page_for_privacy_policy'], 'The response should still report the previous page.' );
$this->assertSame( $page_id, (int) get_option( 'wp_page_for_privacy_policy' ), 'The option should not change.' );
}

public function update_setting_custom_callback( $result, $name, $value, $args ) {
if ( 'title' === $name && 'The new title!' === $value ) {
// Do not allow changing the title in this case.
Expand Down
7 changes: 7 additions & 0 deletions tests/qunit/fixtures/wp-api-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -11237,6 +11237,12 @@ mockedApiResponse.Schema = {
"type": "integer",
"required": false
},
"page_for_privacy_policy": {
"title": "",
"description": "The ID of the page that should be displayed as the privacy policy page",
"type": "integer",
"required": false
},
"default_ping_status": {
"title": "",
"description": "Allow link notifications from other blogs (pingbacks and trackbacks) on new articles.",
Expand Down Expand Up @@ -14932,6 +14938,7 @@ mockedApiResponse.settings = {
"show_on_front": "posts",
"page_on_front": 0,
"page_for_posts": 0,
"page_for_privacy_policy": 0,
"default_ping_status": "open",
"default_comment_status": "open",
"site_logo": null,
Expand Down
Loading