diff --git a/SETUP/API.md b/SETUP/API.md index ab5b2c445..6f00a3d1a 100644 --- a/SETUP/API.md +++ b/SETUP/API.md @@ -66,3 +66,24 @@ Three settings in `configuration.sh` control limiting: allowed per given window. * `_API_RATE_LIMIT_SECONDS_IN_WINDOW` - the number of seconds within a given window. + +## Client Storage + +To facilitate javascript UI clients persisting data across browsers and devices, +the API includes an optional endpoint for clients to store and fetch JSON blobs. +To enable this feature, add a string for the client to the +`_API_CLIENT_STORAGE_KEYS` configuration setting and have the client use that +string with the endpoint as the `clientid`. + +Some important notes about this feature: +* Client storage is one blob per user per client. Said another way: API users are + only able to store one blob per `clientid` and that blob is only for the user + authenticated with the API. +* Beyond validating these are valid JSON objects, they are treated as opaque + blobs server-side. It is up to the client to manage the object, including + the schema and the possibility that the object will not match an expected + schema. +* The `clientid` is not a secret to the browser. Nothing prevents users with + API keys (or valid PHP session keys) from using this endpoint with a valid + client ID to change the contents of this blob for their user. Clients + should treat this blob as unvalidated user input and act accordingly. diff --git a/SETUP/tests/unittests/ApiTest.php b/SETUP/tests/unittests/ApiTest.php index bcf0d9416..32e3e5101 100644 --- a/SETUP/tests/unittests/ApiTest.php +++ b/SETUP/tests/unittests/ApiTest.php @@ -787,6 +787,9 @@ public function test_pickersets(): void $this->assertEquals(["¿", "INVERTED QUESTION MARK"], $pickerset["subsets"][3]["rows"][1][1]); } + //--------------------------------------------------------------------------- + // tests for documents + public function test_available_italian_documents(): void { $path = "v1/documents"; @@ -823,6 +826,48 @@ public function test_unavailable_document(): void $_SERVER["REQUEST_METHOD"] = "GET"; $router->route($path, ['language_code' => 'de']); } + + //--------------------------------------------------------------------------- + // tests for storage + + public function test_client_storage_valid(): void + { + global $pguser; + global $api_client_storage_keys; + global $request_body; + + $pguser = $this->TEST_USERNAME_PM; + array_push($api_client_storage_keys, "valid"); + + $path = "v1/storage/clients/valid"; + $query_params = []; + $request_body = ["key" => 1]; + $router = ApiRouter::get_router(); + + $_SERVER["REQUEST_METHOD"] = "PUT"; + $response = $router->route($path, $query_params); + $this->assertEquals($request_body, (array)$response); + + $_SERVER["REQUEST_METHOD"] = "GET"; + $response = $router->route($path, $query_params); + $this->assertEquals($request_body, (array)$response); + + $_SERVER["REQUEST_METHOD"] = "DELETE"; + $response = $router->route($path, $query_params); + $this->assertEquals(null, $response); + } + + public function test_client_storage_invalid(): void + { + $this->expectExceptionCode(4); + + $query_params = []; + + $path = "v1/storage/clients/invalid"; + $_SERVER["REQUEST_METHOD"] = "GET"; + $router = ApiRouter::get_router(); + $router->route($path, $query_params); + } } // this mocks the function in index.php diff --git a/api/dp-openapi.yaml b/api/dp-openapi.yaml index c69ae9873..b70ce147a 100644 --- a/api/dp-openapi.yaml +++ b/api/dp-openapi.yaml @@ -1338,6 +1338,58 @@ paths: 404: $ref: '#/components/responses/NotFound' + /storage/clients/{clientid}: + get: + tags: + - storage + description: Get JSON blob stored by the client + parameters: + - name: clientid + in: path + description: Client ID + required: true + schema: + type: string + responses: + 200: + description: JSON blob + content: + application/json: + schema: + type: object + put: + tags: + - storage + description: Save JSON blob for the client + parameters: + - name: clientid + in: path + description: Client ID + required: true + schema: + type: string + responses: + 200: + description: JSON blob that was persisted + content: + application/json: + schema: + type: object + delete: + tags: + - storage + description: Delete JSON blob for the client + parameters: + - name: clientid + in: path + description: Client ID + required: true + schema: + type: string + responses: + 200: + description: JSON blob was deleted + components: securitySchemes: ApiKeyAuth: diff --git a/api/v1.inc b/api/v1.inc index 8607632ab..875c81f12 100644 --- a/api/v1.inc +++ b/api/v1.inc @@ -7,6 +7,7 @@ include_once("v1_projects.inc"); include_once("v1_queues.inc"); include_once("v1_stats.inc"); include_once("v1_docs.inc"); +include_once("v1_storage.inc"); $router = ApiRouter::get_router(); @@ -18,6 +19,7 @@ $router->add_validator(":pagename", "validate_page_name"); $router->add_validator(":pageroundid", "validate_page_round"); $router->add_validator(":queueid", "validate_release_queue"); $router->add_validator(":document", "validate_document"); +$router->add_validator(":clientid", "validate_client_id"); // Add routes $router->add_route("GET", "v1/documents", "api_v1_documents"); @@ -62,3 +64,7 @@ $router->add_route("GET", "v1/stats/site/projects/stages", "api_v1_stats_site_pr $router->add_route("GET", "v1/stats/site/projects/states", "api_v1_stats_site_projects_states"); $router->add_route("GET", "v1/stats/site/rounds", "api_v1_stats_site_rounds"); $router->add_route("GET", "v1/stats/site/rounds/:roundid", "api_v1_stats_site_round"); + +$router->add_route("GET", "v1/storage/clients/:clientid", "api_v1_storage_clients"); +$router->add_route("PUT", "v1/storage/clients/:clientid", "api_v1_storage_clients"); +$router->add_route("DELETE", "v1/storage/clients/:clientid", "api_v1_storage_clients_delete"); diff --git a/api/v1_storage.inc b/api/v1_storage.inc new file mode 100644 index 000000000..4c5dd76de --- /dev/null +++ b/api/v1_storage.inc @@ -0,0 +1,32 @@ +get()); + } elseif ($method == "PUT") { + $storage->set(json_encode(api_get_request_body())); + return json_decode($storage->get()); + } +} + +function api_v1_storage_clients_delete(string $method, array $data, array $query_params): void +{ + global $pguser; + + $clientid = $data[":clientid"]; + + $storage = new ApiClientStorage($clientid, $pguser); + $storage->delete(); +} diff --git a/api/v1_validators.inc b/api/v1_validators.inc index 2615f2531..39763d0c6 100644 --- a/api/v1_validators.inc +++ b/api/v1_validators.inc @@ -77,3 +77,13 @@ function validate_document(string $document): string } return $document; } + +function validate_client_id(string $clientid, array $data): string +{ + global $api_client_storage_keys; + + if (!in_array($clientid, $api_client_storage_keys)) { + throw new NotFoundError("$clientid is not a valid client id"); + } + return $clientid; +}