modules = $modules;
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.92.0
*/
public function register() {
add_filter(
'googlesitekit_rest_routes',
function ( $routes ) {
return array_merge( $routes, $this->get_rest_routes() );
}
);
add_filter(
'googlesitekit_apifetch_preload_paths',
function ( $paths ) {
$modules_routes = array(
'/' . REST_Routes::REST_ROOT . '/core/modules/data/list',
);
$settings_routes = array_map(
function ( Module $module ) {
if ( $module instanceof Module_With_Settings ) {
return '/' . REST_Routes::REST_ROOT . "/modules/{$module->slug}/data/settings";
}
return null;
},
$this->modules->get_active_modules()
);
return array_merge( $paths, $modules_routes, array_filter( $settings_routes ) );
}
);
}
/**
* Gets the REST schema for a module.
*
* @since 1.92.0
*
* @return array Module REST schema.
*/
private function get_module_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'module',
'type' => 'object',
'properties' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Name of the module.', 'google-site-kit' ),
'readonly' => true,
),
'description' => array(
'type' => 'string',
'description' => __( 'Description of the module.', 'google-site-kit' ),
'readonly' => true,
),
'homepage' => array(
'type' => 'string',
'description' => __( 'The module homepage.', 'google-site-kit' ),
'format' => 'uri',
'readonly' => true,
),
'internal' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is internal, thus without any UI.', 'google-site-kit' ),
'readonly' => true,
),
'active' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is active.', 'google-site-kit' ),
),
'connected' => array(
'type' => 'boolean',
'description' => __( 'Whether the module setup has been completed.', 'google-site-kit' ),
'readonly' => true,
),
'dependencies' => array(
'type' => 'array',
'description' => __( 'List of slugs of other modules that the module depends on.', 'google-site-kit' ),
'items' => array(
'type' => 'string',
),
'readonly' => true,
),
'dependants' => array(
'type' => 'array',
'description' => __( 'List of slugs of other modules depending on the module.', 'google-site-kit' ),
'items' => array(
'type' => 'string',
),
'readonly' => true,
),
'shareable' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is shareable.', 'google-site-kit' ),
),
'recoverable' => array(
'type' => 'boolean',
'description' => __( 'Whether the module is recoverable.', 'google-site-kit' ),
),
'owner' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'integer',
'description' => __( 'Owner ID.', 'google-site-kit' ),
'readonly' => true,
),
'login' => array(
'type' => 'string',
'description' => __( 'Owner login.', 'google-site-kit' ),
'readonly' => true,
),
),
),
),
);
}
/**
* Gets related REST routes.
*
* @since 1.92.0
*
* @return array List of REST_Route objects.
*/
private function get_rest_routes() {
$can_setup = function () {
return current_user_can( Permissions::SETUP );
};
$can_authenticate = function () {
return current_user_can( Permissions::AUTHENTICATE );
};
$can_list_data = function () {
return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD );
};
$can_view_insights = function () {
// This accounts for routes that need to be called before user has completed setup flow.
if ( current_user_can( Permissions::SETUP ) ) {
return true;
}
return current_user_can( Permissions::VIEW_POSTS_INSIGHTS );
};
$can_manage_options = function () {
// This accounts for routes that need to be called before user has completed setup flow.
if ( current_user_can( Permissions::SETUP ) ) {
return true;
}
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();
};
return array(
new REST_Route(
'core/modules/data/list',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
$modules = array_map(
array( $this, 'prepare_module_data_for_response' ),
$this->modules->get_available_modules()
);
return new WP_REST_Response( array_values( $modules ) );
},
'permission_callback' => $can_list_data,
),
),
array(
'schema' => $get_module_schema,
)
),
new REST_Route(
'core/modules/data/activation',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
$slug = isset( $data['slug'] ) ? $data['slug'] : '';
try {
$this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', $e->getMessage() );
}
$modules = $this->modules->get_available_modules();
if ( ! empty( $data['active'] ) ) {
// Prevent activation if one of the dependencies is not active.
$dependency_slugs = $this->modules->get_module_dependencies( $slug );
foreach ( $dependency_slugs as $dependency_slug ) {
if ( ! $this->modules->is_module_active( $dependency_slug ) ) {
/* translators: %s: module name */
return new WP_Error( 'inactive_dependencies', sprintf( __( 'Module cannot be activated because of inactive dependency %s.', 'google-site-kit' ), $modules[ $dependency_slug ]->name ), array( 'status' => 500 ) );
}
}
if ( ! $this->modules->activate_module( $slug ) ) {
return new WP_Error( 'cannot_activate_module', __( 'An internal error occurred while trying to activate the module.', 'google-site-kit' ), array( 'status' => 500 ) );
}
} else {
// Automatically deactivate dependants.
$dependant_slugs = $this->modules->get_module_dependants( $slug );
foreach ( $dependant_slugs as $dependant_slug ) {
if ( $this->modules->is_module_active( $dependant_slug ) ) {
if ( ! $this->modules->deactivate_module( $dependant_slug ) ) {
/* translators: %s: module name */
return new WP_Error( 'cannot_deactivate_dependant', sprintf( __( 'Module cannot be deactivated because deactivation of dependant %s failed.', 'google-site-kit' ), $modules[ $dependant_slug ]->name ), array( 'status' => 500 ) );
}
}
}
if ( ! $this->modules->deactivate_module( $slug ) ) {
return new WP_Error( 'cannot_deactivate_module', __( 'An internal error occurred while trying to deactivate the module.', 'google-site-kit' ), array( 'status' => 500 ) );
}
}
return new WP_REST_Response( array( 'success' => true ) );
},
'permission_callback' => $can_manage_options,
'args' => array(
'data' => array(
'type' => 'object',
'required' => true,
),
),
),
),
array(
'schema' => $get_module_schema,
)
),
new REST_Route(
'core/modules/data/info',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $request ) {
try {
$module = $this->modules->get_module( $request['slug'] );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', $e->getMessage() );
}
return new WP_REST_Response( $this->prepare_module_data_for_response( $module ) );
},
'permission_callback' => $can_authenticate,
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
),
),
array(
'schema' => $get_module_schema,
)
),
new REST_Route(
self::REST_ROUTE_CHECK_ACCESS,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function ( WP_REST_Request $request ) {
$data = $request['data'];
$slug = isset( $data['slug'] ) ? $data['slug'] : '';
try {
$module = $this->modules->get_module( $slug );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) );
}
if ( ! $module->is_connected() ) {
return new WP_Error( 'module_not_connected', __( 'Module is not connected.', 'google-site-kit' ), array( 'status' => 500 ) );
}
if ( ! $module instanceof Module_With_Service_Entity ) {
if ( $module->is_shareable() ) {
return new WP_REST_Response(
array(
'access' => true,
)
);
}
return new WP_Error( 'invalid_module', __( 'Module access cannot be checked.', 'google-site-kit' ), array( 'status' => 500 ) );
}
$access = $module->check_service_entity_access();
if ( is_wp_error( $access ) ) {
return $access;
}
return new WP_REST_Response(
array(
'access' => $access,
)
);
},
'permission_callback' => $can_setup,
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Identifier for the module.', 'google-site-kit' ),
'sanitize_callback' => 'sanitize_key',
),
),
),
)
),
new REST_Route(
'modules/(?P