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
23 changes: 20 additions & 3 deletions includes/Core/Modules/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ abstract class Module {
*/
private $google_services;

/**
* Memoized datapoint definitions map.
*
* @since n.e.x.t
* @var array|null
*/
private $datapoint_definitions;

/**
* Constructor.
*
Expand Down Expand Up @@ -283,13 +291,22 @@ protected function get_datapoint_definitions() {
* Gets the datapoint definition instance.
*
* @since 1.77.0
* @since n.e.x.t Changed visibility to public so REST permission checks can
* inspect a datapoint (e.g. for Permission_Aware_Datapoint).
*
* @param string $datapoint_id Datapoint ID.
* @param string $datapoint_id Datapoint ID, in the `METHOD:datapoint` form (e.g. `GET:report`).
* @return Datapoint Datapoint instance.
* @throws Invalid_Datapoint_Exception Thrown if no datapoint exists by the given ID.
*/
protected function get_datapoint_definition( $datapoint_id ) {
$definitions = $this->get_datapoint_definitions();
public function get_datapoint_definition( $datapoint_id ) {
// Memoize the definitions map: a single module data request resolves the
// same datapoint twice (once for the permission check, once to execute),
// and rebuilding the full map instantiates every datapoint object.
if ( null === $this->datapoint_definitions ) {
$this->datapoint_definitions = $this->get_datapoint_definitions();
}

$definitions = $this->datapoint_definitions;

// All datapoints must be defined.
if ( empty( $definitions[ $datapoint_id ] ) ) {
Expand Down
36 changes: 36 additions & 0 deletions includes/Core/Modules/Permission_Aware_Datapoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Interface Google\Site_Kit\Core\Modules\Permission_Aware_Datapoint
*
* @package Google\Site_Kit\Core\Modules
* @copyright 2026 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/

namespace Google\Site_Kit\Core\Modules;

/**
* Interface for a datapoint that provides its own REST permission check.
*
* By default every module datapoint inherits the permission check of its HTTP
* method (readable datapoints require view access, editable datapoints require
* `manage_options`). A datapoint implementing this interface overrides that
* default so it can, for example, allow any dashboard-viewing user to persist a
* per-user setting.
*
* @since n.e.x.t
* @access private
* @ignore
*/
interface Permission_Aware_Datapoint {

/**
* Checks whether the current user is allowed to access the datapoint.
*
* @since n.e.x.t
*
* @return bool True if the current user is allowed, false otherwise.
*/
public function permission_callback();
}
40 changes: 38 additions & 2 deletions includes/Core/Modules/REST_Modules_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,38 @@ private function get_rest_routes() {
return current_user_can( Permissions::MANAGE_OPTIONS );
};

// Resolves the permission check for a module data request. A datapoint
// implementing Permission_Aware_Datapoint provides its own check (e.g. to
// allow any dashboard-viewing user to persist a per-user setting);
// otherwise the method's default check is used.
$datapoint_permission_callback = function ( WP_REST_Request $request, callable $default_callback ) {
try {
$method = $request->get_method();
$module = $this->modules->get_module( $request['slug'] );
$datapoint = $module->get_datapoint_definition( "{$method}:{$request['datapoint']}" );
} catch ( Exception $e ) {
// The module or datapoint could not be resolved; defer to the
// default permission check (the request callback then surfaces
// the actual invalid-module/datapoint error).
return $default_callback( $request );
}

if ( ! $datapoint instanceof Permission_Aware_Datapoint ) {
return $default_callback( $request );
}

// A datapoint that defines its own permission check must never
// silently fall back to the (broader) default if that check fails,
// so any error here denies access (`\Throwable`, not just
// `\Exception`, so a `\Error`/`\TypeError` is also fail-closed)
// rather than reverting to the default permission.
try {
return $datapoint->permission_callback();
} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
return false;
}
};

$get_module_schema = function () {
return $this->get_module_schema();
};
Expand Down Expand Up @@ -558,7 +590,9 @@ private function get_rest_routes() {
}
return new WP_REST_Response( $data );
},
'permission_callback' => $can_view_insights,
'permission_callback' => function ( WP_REST_Request $request ) use ( $datapoint_permission_callback, $can_view_insights ) {
return $datapoint_permission_callback( $request, $can_view_insights );
},
),
array(
'methods' => WP_REST_Server::EDITABLE,
Expand All @@ -581,7 +615,9 @@ private function get_rest_routes() {
}
return new WP_REST_Response( $data );
},
'permission_callback' => $can_manage_options,
'permission_callback' => function ( WP_REST_Request $request ) use ( $datapoint_permission_callback, $can_manage_options ) {
return $datapoint_permission_callback( $request, $can_manage_options );
},
'args' => array(
'data' => array(
'type' => 'object',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@

namespace Google\Site_Kit\Modules\Analytics_4\Datapoints;

use Google\Site_Kit\Core\Modules\Executable_Datapoint;
use Google\Site_Kit\Core\Modules\Shareable_Datapoint;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Modules\Analytics_4\Site_Goals_Settings;

/**
* Class for the Site Goals settings retrieval datapoint.
Expand All @@ -22,27 +19,7 @@
* @access private
* @ignore
*/
class Get_Site_Goals_Settings extends Shareable_Datapoint implements Executable_Datapoint {

/**
* Site Goals settings instance.
*
* @since n.e.x.t
* @var Site_Goals_Settings
*/
private $site_goals_settings;

/**
* Constructor.
*
* @since n.e.x.t
*
* @param array $definition Definition fields.
*/
public function __construct( array $definition ) {
parent::__construct( $definition );
$this->site_goals_settings = $definition['site_goals_settings'];
}
class Get_Site_Goals_Settings extends Site_Goals_Settings_Datapoint {

/**
* Creates a request object.
Expand All @@ -59,17 +36,4 @@ public function create_request( Data_Request $data_request ) {
return $site_goals_settings->get();
};
}

/**
* Parses a response.
*
* @since n.e.x.t
*
* @param mixed $response Request response.
* @param Data_Request $data Data request object.
* @return mixed The response without any modifications.
*/
public function parse_response( $response, Data_Request $data ) {
return $response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@

namespace Google\Site_Kit\Modules\Analytics_4\Datapoints;

use Google\Site_Kit\Core\Modules\Datapoint;
use Google\Site_Kit\Core\Modules\Executable_Datapoint;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Modules\Analytics_4\Site_Goals_Settings;

/**
* Class for the Site Goals settings save datapoint.
Expand All @@ -22,27 +19,7 @@
* @access private
* @ignore
*/
class Save_Site_Goals_Settings extends Datapoint implements Executable_Datapoint {

/**
* Site Goals settings instance.
*
* @since n.e.x.t
* @var Site_Goals_Settings
*/
private $site_goals_settings;

/**
* Constructor.
*
* @since n.e.x.t
*
* @param array $definition Definition fields.
*/
public function __construct( array $definition ) {
parent::__construct( $definition );
$this->site_goals_settings = $definition['site_goals_settings'];
}
class Save_Site_Goals_Settings extends Site_Goals_Settings_Datapoint {

/**
* Creates a request object.
Expand All @@ -62,17 +39,4 @@ public function create_request( Data_Request $data_request ) {
return $site_goals_settings->get();
};
}

/**
* Parses a response.
*
* @since n.e.x.t
*
* @param mixed $response Request response.
* @param Data_Request $data Data request object.
* @return mixed The response without any modifications.
*/
public function parse_response( $response, Data_Request $data ) {
return $response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* Class Google\Site_Kit\Modules\Analytics_4\Datapoints\Site_Goals_Settings_Datapoint
*
* @package Google\Site_Kit\Modules\Analytics_4\Datapoints
* @copyright 2026 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/

namespace Google\Site_Kit\Modules\Analytics_4\Datapoints;

use Google\Site_Kit\Core\Modules\Executable_Datapoint;
use Google\Site_Kit\Core\Modules\Permission_Aware_Datapoint;
use Google\Site_Kit\Core\Modules\Shareable_Datapoint;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Modules\Analytics_4\Site_Goals_Settings;

/**
* Base class for the per-user Site Goals settings datapoints.
*
* @since n.e.x.t
* @access private
* @ignore
*/
abstract class Site_Goals_Settings_Datapoint extends Shareable_Datapoint implements Executable_Datapoint, Permission_Aware_Datapoint {

/**
* Site Goals settings instance.
*
* @since n.e.x.t
* @var Site_Goals_Settings
*/
protected $site_goals_settings;

/**
* Constructor.
*
* @since n.e.x.t
*
* @param array $definition Definition fields.
*/
public function __construct( array $definition ) {
parent::__construct( $definition );
$this->site_goals_settings = $definition['site_goals_settings'];
}

/**
* Parses a response.
*
* @since n.e.x.t
*
* @param mixed $response Request response.
* @param Data_Request $data Data request object.
* @return mixed The response without any modifications.
*/
public function parse_response( $response, Data_Request $data ) {
return $response;
}

/**
* Checks whether the current user is allowed to access the datapoint.
*
* @since n.e.x.t
*
* @return bool True if the current user can view the dashboard, false otherwise.
*/
public function permission_callback() {
return current_user_can( Permissions::VIEW_DASHBOARD );
}
}
Loading