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
230 changes: 226 additions & 4 deletions src/php/REST_API/Snippets/Snippets_REST_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use WP_REST_Response;
use WP_REST_Server;
use function Code_Snippets\activate_snippet;
use function Code_Snippets\clean_active_snippets_cache;
use function Code_Snippets\code_snippets;
use function Code_Snippets\deactivate_snippet;
use function Code_Snippets\delete_snippet;
use function Code_Snippets\get_snippet;
Expand Down Expand Up @@ -162,7 +164,7 @@ public function register_routes() {
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'activate_item' ],
'permission_callback' => [ $this, 'update_item_permissions_check' ],
'permission_callback' => [ $this, 'toggle_item_permissions_check' ],
'schema' => [ $this, 'get_item_schema' ],
'args' => $network_args,
]
Expand All @@ -174,7 +176,7 @@ public function register_routes() {
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'deactivate_item' ],
'permission_callback' => [ $this, 'update_item_permissions_check' ],
'permission_callback' => [ $this, 'toggle_item_permissions_check' ],
'schema' => [ $this, 'get_item_schema' ],
'args' => $network_args,
]
Expand Down Expand Up @@ -205,6 +207,165 @@ public function register_routes() {
);
}

/**
* Determine whether a request targets network-scoped snippets.
*
* Only the literal boolean `true` (or its common string/integer equivalents)
* is treated as a network-scoped request. A missing or null `network` param
* means "site-scoped", and must not be escalated to the network capability.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
private function is_network_scoped_request( $request ): bool {
if ( ! is_multisite() ) {
return false;
}

if ( ! $request instanceof WP_REST_Request || ! $request->has_param( 'network' ) ) {
return false;
}

$network = $request->get_param( 'network' );

if ( is_bool( $network ) ) {
return $network;
}

if ( is_string( $network ) ) {
return in_array( strtolower( $network ), [ '1', 'true', 'yes' ], true );
}

return (bool) $network;
}

/**
* Verify the current user has permission for the scope implied by the request.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
private function check_request_capability( $request ): bool {
if ( $this->is_network_scoped_request( $request ) ) {
return code_snippets()->user_can_manage_network_snippets();
}

return code_snippets()->current_user_can();
}

/**
* Determine whether the request targets a shared network snippet.
*
* Shared network snippets are stored network-wide but each site decides whether
* to activate them via the per-site `active_shared_network_snippets` option. The
* `id` route parameter is used to look up the snippet so the result reflects the
* actual stored row rather than a value supplied in the request payload.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
private function is_shared_network_snippet_request( $request ): bool {
if ( ! is_multisite() || ! $request instanceof WP_REST_Request ) {
return false;
}

$snippet_id = absint( $request->get_param( 'id' ) );

if ( ! $snippet_id ) {
return false;
}

$snippet = get_snippet( $snippet_id, true );

return $snippet && $snippet->id && $snippet->shared_network;
}

/**
* Check if a given request has access to get items.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
public function get_items_permissions_check( $request ): bool {
return $this->check_request_capability( $request );
}

/**
* Check if a given request has access to get a specific item.
*
* Shared network snippets are readable by any user who can manage snippets on
* the current site, since the snippet is intentionally exposed to subsites.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
public function get_item_permissions_check( $request ): bool {
if ( $this->is_shared_network_snippet_request( $request ) ) {
return code_snippets()->current_user_can();
}

return $this->check_request_capability( $request );
}

/**
* Check if a given request has access to create items.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
public function create_item_permissions_check( $request ): bool {
return $this->check_request_capability( $request );
}

/**
* Check if a given request has access to update a specific item.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
public function update_item_permissions_check( $request ): bool {
return $this->check_request_capability( $request );
}

/**
* Check if a given request has access to delete a specific item.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
public function delete_item_permissions_check( $request ): bool {
return $this->check_request_capability( $request );
}

/**
* Check if a given request has access to toggle a snippet's activation.
*
* For shared network snippets the activation toggle only writes to the
* per-site `active_shared_network_snippets` option, so the site capability
* is sufficient. For all other snippets we keep the strict capability check
* that prevents a subsite admin from forging `network=true` to operate on
* exclusive network-scoped snippets.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return bool
*/
public function toggle_item_permissions_check( $request ): bool {
if ( $this->is_shared_network_snippet_request( $request ) ) {
return code_snippets()->current_user_can();
}

return $this->check_request_capability( $request );
}

/**
* Retrieves a collection of snippets, with pagination.
*
Expand Down Expand Up @@ -466,7 +627,23 @@ public function restore_item( WP_REST_Request $request ) {
*/
public function activate_item( WP_REST_Request $request ) {
$item = $this->prepare_item_for_database( $request );
$result = activate_snippet( $item->id, $item->network );
$snippet = $item ? get_snippet( $item->id, $item->network ) : null;

if ( ! $snippet || ! $snippet->id ) {
return new WP_Error(
'rest_cannot_activate',
__( 'The snippet could not be found.', 'code-snippets' ),
[ 'status' => 404 ]
);
}

if ( $snippet->shared_network ) {
$this->set_shared_network_active( $snippet->id, true );
$snippet->active = true;
return rest_ensure_response( $snippet );
}

$result = activate_snippet( $snippet->id, $snippet->network );

return $result instanceof Snippet
? rest_ensure_response( $result )
Expand All @@ -486,7 +663,23 @@ public function activate_item( WP_REST_Request $request ) {
*/
public function deactivate_item( WP_REST_Request $request ) {
$item = $this->prepare_item_for_database( $request );
$result = deactivate_snippet( $item->id, $item->network );
$snippet = $item ? get_snippet( $item->id, $item->network ) : null;

if ( ! $snippet || ! $snippet->id ) {
return new WP_Error(
'rest_cannot_activate',
__( 'The snippet could not be found.', 'code-snippets' ),
[ 'status' => 404 ]
);
}

if ( $snippet->shared_network ) {
$this->set_shared_network_active( $snippet->id, false );
$snippet->active = false;
return rest_ensure_response( $snippet );
}

$result = deactivate_snippet( $snippet->id, $snippet->network );

return $result instanceof Snippet
? rest_ensure_response( $result )
Expand All @@ -497,6 +690,35 @@ public function deactivate_item( WP_REST_Request $request ) {
);
}

/**
* Toggle a shared network snippet's active state for the current site only.
*
* @param int $snippet_id Snippet identifier.
* @param bool $active Whether the snippet should be active on the current site.
*
* @return void
*/
private function set_shared_network_active( int $snippet_id, bool $active ): void {
$active_shared_snippets = get_option( 'active_shared_network_snippets', [] );

if ( ! is_array( $active_shared_snippets ) ) {
$active_shared_snippets = [];
}

$already_active = in_array( $snippet_id, $active_shared_snippets, true );

if ( $active === $already_active ) {
return;
}

$active_shared_snippets = $active
? array_merge( $active_shared_snippets, [ $snippet_id ] )
: array_values( array_diff( $active_shared_snippets, [ $snippet_id ] ) );

update_option( 'active_shared_network_snippets', $active_shared_snippets );
clean_active_snippets_cache( code_snippets()->db->ms_table );
}

/**
* Prepare an instance of the Export class from a request.
*
Expand Down
Loading
Loading