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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
* Fixed the version list refresh reporting success when the versions could not be fetched.
* Fixed safe mode failing on sites running a plugin that builds URLs while WordPress is identifying the current user,
and breaking REST API URLs. Safe mode is now carried on admin links only.
* Fixed the snippet import routes allowing a subsite administrator on multisite to import network snippets without the
network capability. Thanks to Artus KG for responsibly disclosing this issue.
* Fixed the snippet source shortcodes revealing snippet code in posts written by users who cannot manage snippets.
Thanks to Het Kalariya for responsibly disclosing this issue.

## [4.0.0] (2026-10-12)

Expand Down
35 changes: 33 additions & 2 deletions src/php/Integration/Shortcodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ protected function invalid_id_warning( int $id ): string {
return current_user_can( 'edit_posts' ) ? sprintf( $text, $id ) : '';
}

/**
* Determine whether the author of the current post may reveal the given snippet.
*
* The author is checked rather than the viewer, as published posts are viewed anonymously.
*
* @param Snippet $snippet Snippet object.
* @param bool $network Whether the snippet was requested from the network table.
*
* @return bool
*/
protected function can_render_snippet_source( Snippet $snippet, bool $network ): bool {
$post = get_post();
$allowed = $post && code_snippets()->user_can_manage_snippets(
(int) $post->post_author,
validate_network_param( $network )
);

return (bool) apply_filters( 'code_snippets/shortcode_can_render_source', $allowed, $snippet, $post );
}

/**
* Allow boolean attributes to be provided without a value, similar to how React works.
*
Expand Down Expand Up @@ -277,12 +297,18 @@ public function render_content_shortcode( array $atts ): string {

// Render the source code if this is not a shortcode snippet.
if ( 'content' !== $snippet->scope ) {
return $snippet->id ? $this->render_snippet_source( $snippet ) : $this->invalid_id_warning( $snippet->id );
if ( ! $snippet->id ) {
return $this->invalid_id_warning( $snippet->id );
}

return $this->can_render_snippet_source( $snippet, (bool) $atts['network'] )
? $this->render_snippet_source( $snippet )
: '';
}

// If the snippet is inactive, either display a message or render nothing.
if ( ! $snippet->active ) {
if ( $atts['debug'] ) {
if ( $atts['debug'] && $this->can_render_snippet_source( $snippet, (bool) $atts['network'] ) ) {
/* translators: 1: snippet name, 2: snippet edit link */
$text = __( '%1$s is currently inactive. You can <a href="%2$s">edit this snippet</a> to activate it and make it visible. This message will not appear in the published post.', 'code-snippets' );
$snippet_name = '<strong>' . $snippet->name . '</strong>';
Expand Down Expand Up @@ -418,6 +444,11 @@ public function render_source_shortcode( array $atts ): string {
}

$snippet = $this->get_snippet( $id, (bool) $atts['network'] );

if ( ! $this->can_render_snippet_source( $snippet, (bool) $atts['network'] ) ) {
return '';
}

return $this->render_snippet_source( $snippet, $atts );
}
}
16 changes: 16 additions & 0 deletions src/php/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,22 @@ public function user_can_manage_network_snippets(): bool {
return is_super_admin() || current_user_can( $this->get_network_cap_name() );
}

/**
* Determine if a given user has the capability to manage snippets at the given scope.
*
* @param int $user_id User ID.
* @param bool $network Whether to check for network snippets.
*
* @return bool
*/
public function user_can_manage_snippets( int $user_id, bool $network = false ): bool {
if ( $network || ( is_multisite() && ! $this->is_subsite_menu_enabled() ) ) {
return is_super_admin( $user_id ) || user_can( $user_id, $this->get_network_cap_name() );
}

return user_can( $user_id, $this->get_cap_name() );
}

/**
* Get the required capability to perform a certain action on snippets.
* Does not check if the user has this capability or not.
Expand Down
30 changes: 29 additions & 1 deletion src/php/REST_API/Import/File_Import_REST_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Code_Snippets\REST_API\Import;

use Code_Snippets\Model\Snippet;
use Code_Snippets\REST_API\Network_Scope;
use Code_Snippets\REST_API\REST_Controller;
use DOMDocument;
use WP_Error;
Expand All @@ -17,6 +18,8 @@
* Manages the import of code snippets from uploaded files via REST API.
*/
class File_Import_REST_Controller extends REST_Controller {
use Network_Scope;


/**
* Current API version.
Expand Down Expand Up @@ -79,7 +82,32 @@ public function register_routes() {
* @return bool
*/
public function permission_callback( WP_REST_Request $request ): bool {
return code_snippets()->current_user_can();
return $this->is_network_scoped_request( $request ) || $this->has_shared_network_snippets( $request )
? code_snippets()->user_can_manage_network_snippets()
: code_snippets()->current_user_can();
}

/**
* Determine whether any snippet in the request is marked as a shared network snippet.
*
* @param WP_REST_Request $request Incoming HTTP request.
*
* @return bool
*/
private function has_shared_network_snippets( WP_REST_Request $request ): bool {
$snippets = $request->get_param( 'snippets' );

if ( ! is_multisite() || ! is_array( $snippets ) ) {
return false;
}

foreach ( $snippets as $snippet ) {
if ( is_array( $snippet ) && ! empty( $snippet['shared_network'] ) ) {
return true;
}
}

return false;
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/php/REST_API/Import/Plugins_Import_REST_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Code_Snippets\REST_API\Import\Plugins\Insert_Headers_And_Footers_Plugin_Importer;
use Code_Snippets\REST_API\Import\Plugins\Insert_PHP_Code_Snippet_Plugin_Importer;
use Code_Snippets\REST_API\Import\Plugins\Plugin_Importer;
use Code_Snippets\REST_API\Network_Scope;
use Code_Snippets\REST_API\REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
Expand All @@ -18,6 +19,8 @@
* Handles registering REST API routes and providing information about available plugin importers.
*/
class Plugins_Import_REST_Controller extends REST_Controller {
use Network_Scope;


/**
* Current API version.
Expand Down Expand Up @@ -119,6 +122,8 @@ public function register_routes() {
* @return bool
*/
public function permission_callback( WP_REST_Request $request ): bool {
return code_snippets()->current_user_can();
return $this->is_network_scoped_request( $request )
? code_snippets()->user_can_manage_network_snippets()
: code_snippets()->current_user_can();
}
}
41 changes: 41 additions & 0 deletions src/php/REST_API/Network_Scope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Code_Snippets\REST_API;

use WP_Error;
use WP_REST_Request;

/**
* Shared handling of the `network` request parameter for REST controllers.
*/
trait Network_Scope {

/**
* 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|WP_Error $request Full data about the request.
*
* @return bool
*/
protected function is_network_scoped_request( $request ): bool {
if ( ! is_multisite() || ! $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 ), [ '0', 'false', 'no', '' ], true );

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.

We could use filter_var( $network, FILTER_VALIDATE_BOOLEAN ) here for something a little more idiomatic.

}

return (bool) $network;
}
}
32 changes: 3 additions & 29 deletions src/php/REST_API/Snippets/Snippets_REST_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Code_Snippets\Migration\Export\Export_Code;
use Code_Snippets\Migration\Export\Export_JSON;
use Code_Snippets\Model\Snippet;
use Code_Snippets\REST_API\Network_Scope;
use Code_Snippets\REST_API\REST_Collection_Controller;
use WP_Error;
use WP_REST_Request;
Expand All @@ -30,6 +31,8 @@
* @package Code_Snippets
*/
final class Snippets_REST_Controller extends REST_Collection_Controller {
use Network_Scope;


/**
* Current API version.
Expand Down Expand Up @@ -219,35 +222,6 @@ public function permission_callback( WP_REST_Request $request ): bool {
return code_snippets()->current_user_can();
}

/**
* 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|WP_Error $request Full data about the request.
*
* @return bool
*/
private function is_network_scoped_request( $request ): bool {
if ( ! is_multisite() || ! $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 ), [ '0', 'false', 'no', '' ], true );
}

return (bool) $network;
}

/**
* Verify the current user has permission for the scope implied by the request.
*
Expand Down
Loading
Loading