<?php namespace iThemesSecurity\User_Groups\REST; use iThemesSecurity\User_Groups\Repository\Repository; use iThemesSecurity\User_Groups\User_Group; use iThemesSecurity\User_Groups\Repository\User_Group_Not_Found; use iThemesSecurity\Exception\Invalid_Argument_Exception; use iThemesSecurity\Exception\WP_Error; class User_Groups extends \WP_REST_Controller { const ID_PATTERN = '(?P<id>[\\w_:-]+)'; /** @var \iThemesSecurity\User_Groups\Repository\Repository */ private $repository; /** * REST constructor. * * @param \iThemesSecurity\User_Groups\Repository\Repository $repository */ public function __construct( Repository $repository ) { $this->repository = $repository; $this->namespace = 'ithemes-security/v1'; $this->rest_base = 'user-groups'; } public function register_routes() { register_rest_route( $this->namespace, $this->rest_base, [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_items' ], 'permission_callback' => [ $this, 'get_items_permissions_check' ], 'args' => $this->get_collection_params(), ], [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_item' ], 'permission_callback' => [ $this, 'create_item_permissions_check' ], 'args' => array_merge( $this->get_endpoint_args_for_item_schema(), [ 'ignore_duplicate' => [ 'type' => 'boolean', ], ] ), 'allow_batch' => [ 'v1' => true, ], ], 'schema' => [ $this, 'get_public_item_schema' ], ] ); register_rest_route( $this->namespace, $this->rest_base . '/' . self::ID_PATTERN, [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_item' ], 'permission_callback' => [ $this, 'get_item_permissions_check' ], 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ], ], [ 'methods' => 'PUT', 'callback' => [ $this, 'update_item' ], 'permission_callback' => [ $this, 'update_item_permissions_check' ], 'args' => $this->get_endpoint_args_for_item_schema( 'PUT' ), ], [ 'methods' => 'DELETE', 'callback' => [ $this, 'delete_item' ], 'permission_callback' => [ $this, 'delete_item_permissions_check' ], ], 'schema' => [ $this, 'get_public_item_schema' ], 'args' => [ 'id' => [ 'type' => 'string', ], ], 'allow_batch' => [ 'v1' => true, ], ] ); } public function get_items_permissions_check( $request ) { if ( 'edit' === $request['context'] && ! current_user_can( 'itsec_edit_user_groups' ) ) { return new \WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit user groups.', 'better-wp-security' ), [ 'status' => rest_authorization_required_code() ] ); } if ( ! current_user_can( 'itsec_list_user_groups' ) ) { return new \WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to list user groups.', 'better-wp-security' ), [ 'status' => rest_authorization_required_code() ] ); } return true; } public function get_items( $request ) { $data = []; foreach ( $this->repository->all() as $user_group ) { $data[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $user_group, $request ) ); } return new \WP_REST_Response( $data ); } public function get_item_permissions_check( $request ) { if ( true !== ( $error = $this->check_group_exists( $request ) ) ) { return $error; } if ( 'edit' === $request['context'] && ! current_user_can( 'itsec_edit_user_group', $request['id'] ) ) { return new \WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this user group.', 'better-wp-security' ), [ 'status' => rest_authorization_required_code() ] ); } if ( ! current_user_can( 'itsec_read_user_group', $request['id'] ) ) { return new \WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view this user group.', 'better-wp-security' ), [ 'status' => rest_authorization_required_code() ] ); } return true; } public function get_item( $request ) { try { return $this->prepare_item_for_response( $this->repository->get( $request['id'] ), $request ); } catch ( User_Group_Not_Found $e ) { return new \WP_Error( 'rest_user_group_not_found', $e->getMessage(), [ 'status' => \WP_Http::NOT_FOUND ] ); } } public function create_item_permissions_check( $request ) { if ( ! current_user_can( 'itsec_create_user_groups' ) ) { return new \WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create user groups.', 'better-wp-security' ), [ 'status' => rest_authorization_required_code() ] ); } return true; } public function create_item( $request ) { try { $user_group = $this->prepare_item_for_database( $request ); if ( is_wp_error( $user_group ) ) { return $user_group; } if ( ! $request['ignore_duplicate'] && $response = $this->handle_duplicate_check( $user_group ) ) { return $response; } $this->repository->persist( $user_group, [] ); $request['context'] = 'edit'; $response = $this->prepare_item_for_response( $user_group, $request ); $response->set_status( \WP_Http::CREATED ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $user_group->get_id() ) ) ); return $response; } catch ( \Exception $e ) { return new \WP_Error( 'internal_server_error', __( 'An unexpected error occurred.', 'better-wp-security' ), [ 'status' => \WP_Http::INTERNAL_SERVER_ERROR ] ); } } public function update_item_permissions_check( $request ) { if ( true !== ( $error = $this->check_group_exists( $request ) ) ) { return $error; } if ( ! current_user_can( 'itsec_edit_user_group', $request['id'] ) ) { return new \WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this user group.', 'better-wp-security' ), [ 'status' => rest_authorization_required_code() ] ); } return true; } /** * Handle checking for duplicate user groups. * * @param User_Group $user_group * * @return \WP_REST_Response|null */ private function handle_duplicate_check( User_Group $user_group ) { /** @var User_Group[] $duplicates */ $duplicates = array_filter( $this->repository->all(), static function ( User_Group $maybe_group ) use ( $user_group ) { return $user_group->equals( $maybe_group ); } ); if ( ! $duplicates ) { return null; } \ITSEC_Lib::load( 'rest' ); $error = new \WP_Error( 'rest_duplicate_user_group', __( 'Another user group with this configuration already exists.', 'better-wp-security' ), [ 'status' => \WP_Http::BAD_REQUEST ] ); $response = \ITSEC_Lib_REST::error_to_response( $error ); foreach ( $duplicates as $duplicate ) { $response->add_link( 'duplicate', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $duplicate->get_id() ) ) ); } return $response; } public function update_item( $request ) { try { $user_group = $this->prepare_item_for_database( $request ); if ( is_wp_error( $user_group ) ) { return $user_group; } $this->repository->persist( $user_group, [] ); $request['context'] = 'edit'; return $this->prepare_item_for_response( $user_group, $request ); } catch ( WP_Error $e ) { return $e->get_error(); } catch ( \Exception $e ) { return new \WP_Error( 'internal_server_error', __( 'An unexpected error occurred.', 'better-wp-security' ), [ 'status' => \WP_Http::INTERNAL_SERVER_ERROR ] ); } } public function delete_item_permissions_check( $request ) { if ( true !== ( $error = $this->check_group_exists( $request ) ) ) { return $error; } if ( ! current_user_can( 'itsec_delete_user_group', $request['id'] ) ) { return new \WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this user group.', 'better-wp-security' ), [ 'status' => rest_authorization_required_code() ] ); } return true; } public function delete_item( $request ) { try { $user_group = $this->repository->get( $request['id'] ); $this->repository->delete( $user_group ); return new \WP_REST_Response( null, \WP_Http::NO_CONTENT ); } catch ( User_Group_Not_Found $e ) { return new \WP_Error( 'rest_user_group_not_found', $e->getMessage(), [ 'status' => \WP_Http::NOT_FOUND ] ); } } protected function prepare_item_for_database( $request ) { try { if ( isset( $request->get_url_params()['id'] ) ) { $user_group = $this->repository->get( $request->get_url_params()['id'] ); } elseif ( isset( $request['id'] ) ) { if ( $this->repository->has( $request['id'] ) ) { throw WP_Error::from_code( 'rest_duplicate_user_group_id', sprintf( __( 'A user group already exists with the id \'%s\'.', 'better-wp-security' ), $request['id'] ), [ 'status' => \WP_Http::BAD_REQUEST ] ); } $user_group = new User_Group( $request['id'] ); } else { $user_group = new User_Group( $this->repository->next_id() ); } if ( isset( $request['users'] ) ) { $user_group->set_users( array_map( static function ( $id ) { if ( ! $user = get_userdata( $id ) ) { throw WP_Error::from_code( 'rest_user_not_found', sprintf( __( 'No user found for %d.', 'better-wp-security' ), $id ), [ 'status' => \WP_Http::BAD_REQUEST ] ); } return $user; }, $request['users'] ) ); } if ( isset( $request['roles'] ) ) { $user_group->set_roles( $request['roles'] ); } if ( isset( $request['canonical'] ) ) { $user_group->set_canonical_roles( $request['canonical'] ); } if ( isset( $request['min_role'] ) ) { $user_group->set_min_role( $request['min_role'] ); } if ( isset( $request['label'] ) ) { $user_group->set_label( $request['label'] ); } } catch ( WP_Error $e ) { return $e->get_error(); } catch ( Invalid_Argument_Exception $e ) { return new \WP_Error( 'rest_invalid_param', $e->getMessage(), [ 'status' => \WP_Http::BAD_REQUEST ] ); } catch ( \Exception $e ) { return new \WP_Error( 'internal_server_error', __( 'An unexpected error occurred.', 'better-wp-security' ), [ 'status' => \WP_Http::INTERNAL_SERVER_ERROR ] ); } if ( ! $user_group->is_configured() ) { return new \WP_Error( 'rest_user_group_not_configured', __( 'A user group must have a minimum role, list of roles, or list of users to be created.', 'better-wp-security' ), [ 'status' => \WP_Http::BAD_REQUEST ] ); } return $user_group; } public function prepare_item_for_response( $item, $request ) { if ( ! $item instanceof User_Group ) { return new \WP_REST_Response(); } $fields = $this->get_fields_for_response( $request ); $data = [ 'id' => $item->get_id(), ]; if ( in_array( 'label', $fields, true ) ) { $data['label'] = $item->get_label(); } if ( in_array( 'description', $fields, true ) ) { $data['description'] = $item->get_description(); } if ( in_array( 'users', $fields, true ) ) { $data['users'] = wp_list_pluck( $item->get_users(), 'ID' ); } if ( in_array( 'roles', $fields, true ) ) { $data['roles'] = $item->get_roles(); } if ( in_array( 'canonical', $fields, true ) ) { $data['canonical'] = $item->get_canonical_roles(); } if ( in_array( 'min_role', $fields, true ) ) { $data['min_role'] = $item->get_min_role(); } $response = new \WP_REST_Response( $data ); $response->add_links( $this->prepare_links( $item ) ); return $response; } /** * Check that a user group exists. * * @param \WP_REST_Request $request * * @return bool|\WP_Error */ protected function check_group_exists( \WP_REST_Request $request ) { if ( $this->repository->has( $request['id'] ) ) { return true; } return new \WP_Error( 'rest_not_found', __( 'Sorry, no user group exists with that id.', 'better-wp-security' ), [ 'status' => \WP_Http::NOT_FOUND ] ); } /** * Prepare the links for each user group. * * @param User_Group $user_group * * @return array */ public function prepare_links( User_Group $user_group ) { $links = [ 'self' => [ 'href' => rest_url( "{$this->namespace}/{$this->rest_base}/{$user_group->get_id()}" ), ], \ITSEC_Lib_REST::get_link_relation( 'user-matchable-settings' ) => [ 'href' => rest_url( "{$this->namespace}/user-matchable-settings/{$user_group->get_id()}" ), 'embeddable' => true, ], ]; foreach ( $user_group->get_users() as $user ) { $links[ \ITSEC_Lib_REST::get_link_relation( 'user-group-member' ) ][] = [ 'href' => rest_url( "wp/v2/users/{$user->ID}" ), 'embeddable' => true ]; } return $links; } public function get_item_schema() { if ( ! empty( $this->schema ) && ! \ITSEC_Core::is_test_suite( 'wpunit' ) ) { return $this->schema; } $schema = [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'ithemes-security-user-group', 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string', 'readonly' => true, 'context' => [ 'view', 'edit', 'embed' ], ], 'label' => [ 'type' => 'string', 'minLength' => 1, 'required' => true, 'context' => [ 'view', 'edit', 'embed' ], ], 'description' => [ 'type' => 'string', 'readonly' => true, 'context' => [ 'view', 'edit', 'embed' ], ], 'users' => [ 'type' => 'array', 'items' => [ 'type' => 'integer', 'minimum' => 0, ], 'uniqueItems' => true, 'context' => [ 'view', 'edit' ], ], 'roles' => [ 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => array_keys( wp_roles()->get_names() ), ], 'context' => [ 'view', 'edit' ], ], 'canonical' => [ 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => \ITSEC_Lib_Canonical_Roles::get_canonical_roles( is_multisite() ), ], 'arg_options' => [ 'validate_callback' => static function ( $value ) { return rest_validate_value_from_schema( $value, [ 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => \ITSEC_Lib_Canonical_Roles::get_canonical_roles(), ], ] ); }, ], ], 'min_role' => [ 'type' => 'string', 'enum' => array_merge( [ '' ], array_keys( wp_roles()->get_names() ) ), 'context' => [ 'view', 'edit' ], ], ] ]; if ( isset( $this->schema ) ) { $this->schema = $schema; } return $schema; } public function get_collection_params() { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ]; } }