. namespace core_user\route\api; use core\exception\coding_exception; use core\exception\invalid_parameter_exception; use core\param; use core\router\route; use core\router\schema\objects\scalar_type; use core\router\schema\response\payload_response; use core\router\schema\response\content\payload_response_type; use core\router\schema\response\response_type; use core\user; use core_user\route\responses\user_preferences_response; use stdClass; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; /** * User preference API handler. * * @package core_user * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[route( path: '/{user}/preferences', pathtypes: [ new \core\router\parameters\path_user(), ], )] class preferences { /** * Fetch all user preferences, or a specific user preference. * * @param ResponseInterface $response * @param ServerRequestInterface $request * @param stdClass $user * @param null|string $preference * @return payload_response */ #[route( path: '[/{preference}]', title: 'Fetch user preferences', description: 'Fetch one user preference, or all user preferences', pathtypes: [ new \core\router\schema\parameters\path_parameter( name: 'preference', type: param::RAW, ), ], responses: [ new user_preferences_response(), ], )] public function get_preferences( ResponseInterface $response, ServerRequestInterface $request, stdClass $user, ?string $preference, ): payload_response { $this->check_user($user); $result = get_user_preferences( name: $preference, user: $user, ); if (!is_array($result)) { // Check if we received just one preference. $result = [$preference => $result]; } return new payload_response($result, $request, $response); } /** * Set a set of user preferences. * * @param ResponseInterface $response * @param stdClass $user * @return payload_response */ #[route( method: ['POST'], title: 'Set or update multiple user preferences', requestbody: new \core\router\schema\request_body( content: new payload_response_type( schema: new \core\router\schema\objects\schema_object( content: [ 'preferences' => new \core\router\schema\objects\array_of_strings( keyparamtype: param::TEXT, valueparamtype: param::RAW, ), ], ), ), ), responses: [ new user_preferences_response(), ], )] public function set_preferences( ResponseInterface $response, ServerRequestInterface $request, stdClass $user, ): payload_response { $this->check_user($user); $values = $request->getParsedBody(); $preferences = $values['preferences'] ?? []; foreach ($preferences as $preference => $value) { $this->set_single_preference($user, $preference, $value); } $result = array_filter( get_user_preferences( user: $user, ), fn ($preference) => array_key_exists($preference, $preferences), ARRAY_FILTER_USE_KEY, ); return new payload_response($result, $request, $response); } /** * Set a single user preference. * * @param ResponseInterface $response * @param string $themename * @param string $component * @param null|string $identifier * @return response_type */ #[route( path: '/{preference}', method: ['POST'], title: 'Set a single user preference', description: 'Set a single user preference', pathtypes: [ new \core\router\schema\parameters\path_parameter( name: 'preference', type: param::RAW, ), ], requestbody: new \core\router\schema\request_body( content: new payload_response_type( schema: new \core\router\schema\objects\schema_object( content: [ 'value' => new scalar_type(param::RAW), ], ), ), ), responses: [ new \core\router\schema\response\response( statuscode: 200, description: 'OK', content: [ new \core\router\schema\response\content\json_media_type( schema: new \core\router\schema\objects\array_of_strings( keyparamtype: param::TEXT, valueparamtype: param::RAW, ), examples: [ new \core\router\schema\example( name: 'A single preference value', summary: 'A json response containing a single preference', value: [ "drawers-open-index" => "1", ], ), ] ), ], ), ], )] public function set_preference( ResponseInterface $response, ServerRequestInterface $request, stdClass $user, ?string $preference, ): response_type { $this->check_user($user); $values = $request->getParsedBody(); $value = $values['value'] ?? null; $this->set_single_preference($user, $preference, $value); return $this->get_preferences($response, $request, $user, $preference); } /** * Set a single user preference. * * @param \stdClass $user * @param string $preference * @param mixed $value * @throws \core\exception\access_denied_exception * @throws \invalid_parameter_exception */ protected function set_single_preference( stdClass $user, string $preference, mixed $value, ): void { try { $definition = user::get_preference_definition($preference); } catch (coding_exception $e) { throw new invalid_parameter_exception("Invalid preference '$preference'"); } if (!user::can_edit_preference($preference, $user)) { throw new \core\exception\access_denied_exception('You do not have permission to edit this preference.'); } if (isset($definition['type'])) { $type = param::from_type($definition['type']); $value = $this->standardise_value($type, $value); } $cleanvalue = user::clean_preference($value, $preference); if ($cleanvalue !== $value) { throw new \invalid_parameter_exception("Invalid value for preference '$preference': '{$value}'"); } $value = $cleanvalue; set_user_preference($preference, $value, $user->id); } /** * Ensure that the requested user meets the requirements. * * @param stdClass $user * @throws invalid_parameter_exception */ protected function check_user(stdClass $user): void { global $USER; if ($user->id !== $USER->id) { throw new \core\exception\access_denied_exception( 'You do not have permission to view or edit preferences for other users.', ); } } /** * Standardise value based on type. * * Note: We cannot use \core\param here because we only want to cast some types. * Requests do not have an inherent understanding of anything but strings. We need to be strict on typing of integers and bools. * * @param string param $type * @param mixed $value * @return mixed */ protected function standardise_value(param $type, mixed $value): mixed { if (is_numeric($value) || is_bool($value)) { switch ($type) { case param::INT: case param::BOOL: $value = (int) $value; } } return $value; } }