[ 'P', 'C', 'R' ], 'single_event_pages' => [ 'on', 'off' ], ]; /** * Register the routes. */ public function register_routes(): void { register_rest_route( $this->namespace, '/users', [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_users' ], 'permission_callback' => [ $this, 'get_users_permissions_check' ], ], ] ); // Build PUT /preferences args from the ALLOWED_PREFS allowlist so // the schema stays authoritative: adding a new preference key to // the constant automatically extends the accepted args without // duplicating the valid-values list. Each key is optional (partial // update) and has NO default — if the client omits it the schema // must not inject a value, or every PUT would silently overwrite // every preference with its default (WPORG-7.10). $preferences_args = []; foreach ( self::ALLOWED_PREFS as $key => $valid_values ) { $preferences_args[ $key ] = [ 'required' => false, 'type' => 'string', 'enum' => $valid_values, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => static fn( $v ): bool => is_string( $v ) && in_array( $v, $valid_values, true ), ]; } register_rest_route( $this->namespace, '/preferences', [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_preferences' ], 'permission_callback' => [ $this, 'preferences_permissions_check' ], ], [ 'methods' => 'PUT', 'callback' => [ $this, 'update_preferences' ], 'permission_callback' => [ $this, 'preferences_permissions_check' ], 'args' => $preferences_args, ], ] ); } /** * Check permissions for getting users. */ public function get_users_permissions_check( WP_REST_Request $request ): bool|\WP_Error { $rate_limit = $this->check_rate_limit(); if ( is_wp_error( $rate_limit ) ) { return $rate_limit; } return $this->check_permission( 'webcal_can_view' ); } /** * Get users for participant selection. */ public function get_users( WP_REST_Request $request ): WP_REST_Response { $search = $this->str( $request->get_param( 'search' ) ); $query = new WP_User_Query( [ 'search' => $search ? '*' . $search . '*' : '', 'search_columns' => UserSearch::columns(), 'number' => 20, 'fields' => [ 'ID', 'user_login', 'display_name' ], 'capability' => 'webcal_can_view', ] ); $users = []; /** @var \WP_User $user */ foreach ( $query->get_results() as $user ) { $users[] = [ 'id' => $user->ID, 'login' => $user->user_login, 'name' => $user->display_name, ]; } // Also fetch remote calendars (resources) try { $container = \WebCalendar\WP\Container::get_instance(); /** @var \WebCalendar\Core\Domain\Repository\ResourceRepositoryInterface */ $resource_repo = $container->get( 'resource_repository' ); $resources = $resource_repo->findAll(); foreach ( $resources as $resource ) { // Simple client-side filtering if search is provided if ( $search !== '' && stripos( $resource->login(), $search ) === false && stripos( $resource->name(), $search ) === false ) { continue; } $users[] = [ 'id' => 'remote_' . $resource->login(), 'login' => $resource->login(), 'name' => $resource->name() . ' (' . __( 'Remote', 'agenticdaisy-calendar' ) . ')', 'type' => 'remote', ]; } // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- remote calendar users are optional } catch ( \Exception $e ) { // Remote calendars unavailable; return local users only. } return new WP_REST_Response( $users, 200 ); } /** * Check permissions for preferences endpoints. */ public function preferences_permissions_check( WP_REST_Request $request ): bool|\WP_Error { $rate_limit = $this->check_rate_limit(); if ( is_wp_error( $rate_limit ) ) { return $rate_limit; } if ( ! is_user_logged_in() ) { return new \WP_Error( 'rest_forbidden', __( 'You must be logged in to access preferences.', 'agenticdaisy-calendar' ), [ 'status' => 401 ] ); } return true; } /** * Get the current user's preferences. */ public function get_preferences( WP_REST_Request $request ): WP_REST_Response { $user_repo = $this->container->get_user_repository(); $login = wp_get_current_user()->user_login; $prefs = $user_repo->getPreferences( $login ); $result = []; foreach ( self::ALLOWED_PREFS as $key => $valid_values ) { $result[ $key ] = $valid_values[0]; // default to first valid value } foreach ( $prefs as $pref ) { if ( isset( self::ALLOWED_PREFS[ $pref->key() ] ) ) { $result[ $pref->key() ] = $pref->value(); } } return new WP_REST_Response( $result, 200 ); } /** * Update the current user's preferences. */ public function update_preferences( WP_REST_Request $request ): WP_REST_Response|\WP_Error { $user_repo = $this->container->get_user_repository(); $login = wp_get_current_user()->user_login; $params = $this->safe_params( $request ); foreach ( $params as $key => $value ) { if ( ! isset( self::ALLOWED_PREFS[ $key ] ) ) { continue; } // Schema (see register_routes(), WPORG-7.10) has already // sanitize_text_field'd and enum-validated the value; // $this->str() stays for the PHPStan Level 9 mixed→string // coercion. The in_array below is defence-in-depth for unit // tests that bypass the schema. $str_value = $this->str( $value ); if ( ! in_array( $str_value, self::ALLOWED_PREFS[ $key ], true ) ) { return new \WP_Error( 'webcal_invalid_preference', /* translators: 1: preference key, 2: allowed values */ sprintf( __( 'Invalid value for %1$s. Allowed: %2$s', 'agenticdaisy-calendar' ), $key, implode( ', ', self::ALLOWED_PREFS[ $key ] ) ), [ 'status' => 400 ] ); } $user_repo->savePreference( $login, new UserPreference( $key, $str_value ) ); } return $this->get_preferences( $request ); } }