From d58e7041ca386875c638b860f8bec9bf01685ff9 Mon Sep 17 00:00:00 2001 From: Risto Lahtela <24460436+Rsl1122@users.noreply.github.com> Date: Fri, 20 Mar 2020 13:26:13 +0200 Subject: [PATCH] Created APIv5 PageExtension API (markdown) --- APIv5-PageExtension-API.md | 390 +++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 APIv5-PageExtension-API.md diff --git a/APIv5-PageExtension-API.md b/APIv5-PageExtension-API.md new file mode 100644 index 0000000..b4b3bc2 --- /dev/null +++ b/APIv5-PageExtension-API.md @@ -0,0 +1,390 @@ +![Plan Header](https://puu.sh/AXSg7/5f2f78c06c.jpg) +# Plan API version 5 - Page Extension API + +> All links on this page have not been set yet + +Page Extension API is for adding content to the Plan website. + +It consists of these parts: + +- `ResolverService` - For adding HTTP request responses +- `ResourceService` - For customizing files and making them customizable by user. + +## Table of contents + +- ResolverService + - Creating a Resolver + - ResponseBuilder + - Registering your Resolver +- ResourceService + - Making resources customizable + - Adding javascript to .html resource + - Adding stylesheet to .html resource + +---- + +# ResolverService + +Requires Capability `PAGE_EXTENSION_RESOLVERS` + +ResolverService can be obtained with +```java +ResolverService svc = ResolverService.getInstance(); +``` + +## Creating a Resolver + +First interface to understand is the `Resolver`. *(Javadoc has been truncated)* + +```java +package com.djrapitops.plan.delivery.web.resolver; + +public interface Resolver { + + /** + * @return true if allowed or invalid target, false if response should be 403 (forbidden) + */ + boolean canAccess(Request request); + + /** + * @return Response or empty if the response should be 404 (not found). + */ + Optional resolve(Request request); + +} +``` + +Lets implement a new Resolver one method at a time. + +---- + +### `Request`-object + +`Request` object contains everything the HTTP request has, like Request headers, Request method, URI path (`/some/path/somewhere`), URI query (`?some=1&query=example`). In addition it contains Plan specific user information if present. + +Here are the methods of Request: +```java +public final class Request { + public String getMethod() // HTTP method like GET, PUT, PUSH etc + public URIPath getPath() // '/some/path/somewhere' + public URIQuery getQuery() // '?some=1&query=example' + public Optional getUser() // Plan WebUser if present + public Optional getHeader(String key) // Request headers + public Request omitFirstInPath() // Removes first '/part' of path +} +``` + +`URIPath`, `URIQuery` and `WebUser` have methods for reading different parts of the request. + +> 💡 **Tips** +> - headers that can have multiple values (Like `Cookie`) are split with `;` character + +
+Example of URIPath usage inside Resolver#resolve-method + +```java +@Override +Optional resolve(Request request) { + URIPath path = request.getPath(); + String part = path.getPart(0) + .orElseThrow(() -> new BadRequestException("Path needs at least one part! " + path.asString())); + if (part.equals("help")) { + // return response for /help + } else { + return Optional.empty(); + } +} +``` + +
+
+Example of URIQuery usage inside Resolver#resolve-method + +```java +@Override +Optional resolve(Request request) { + URIQuery query = request.getQuery(); + String serverUUID = query.get("serverUUID") + .orElseThrow(() -> new BadRequestException("'serverUUID' parameter required"); + // return response for the specific server +} +``` + +
+ +---- + +### `canAccess`-method + +This method should check if the user viewing the URL is allowed to do so. + +> The WebUser permissions **are not Minecraft user permissions**. They are an upcoming feature of Plan and will be managed by the admins via Plan. +> +> You can check if this feature is available with Capability `PAGE_EXTENSION_USER_PERMISSIONS` +> +> Before the feature arrives, consider using existing page permissions: +> +> Permission level - permission +> - 0 - page.server / page.network +> - 1 - page.players +> - 2 - page.player.self + +Here is an example that only checks the user's permission regardless of the accessed URIPath or URIQuery. + +```java +public class YourResolver implements Resolver { + @Override + public boolean canAccess(Request request) { + WebUser user = request.getUser().orElse(new WebUser("")); + if (CapabilityService.getInstance().hasCapability("PAGE_EXTENSION_USER_PERMISSIONS")) { + return user.hasPermission("custom.permission"); + } else { + return user.hasPermission("page.server"); + } + } +} +``` + +Rest of the `Request` methods can be used for more granular access control. + +> 📢 **Don't return `false` unless a 403 Forbidden should be shown** + +> 💡 **Tips** +> - This method is not called if authentication is not enabled +> - Implement `NoAuthResolver` if response should always be given + +---- + +### `resolve`-method + +This method links `Request`s to appropriate `Response` objects. + +You can use `newResponseBuilder()` to begin building a new `Response`. You can learn more about the builder below. + +```java +public class YourResolver implements Resolver { + @Override + Optional resolve(Request request) { + return Optional.of( + newResponseBuilder() + // calls to builder methods + .build() + ); + } +} +``` + +Following Exceptions can be thrown for automated responses: +- `NotFoundException` for a 404 not found page +- `BadRequestException` for a 400 bad request `{"status": 400, "error": "message"}` response + +---- + +## `ResponseBuilder` + +`Response` objects can be created using this builder. + +Here are the methods of `ResponseBuilder`, consult [Javadoc]() for specifics of each method if necessary. + +```java +public class ResponseBuilder { + + public ResponseBuilder setMimeType(String mimeType) + public ResponseBuilder setStatus(int code) + public ResponseBuilder setHeader(String header, Object value) + public ResponseBuilder redirectTo(String url) + public ResponseBuilder setContent(WebResource resource) + public ResponseBuilder setContent(byte[] bytes) + public ResponseBuilder setContent(String utf8String) + public ResponseBuilder setContent(String content, Charset charset) + public ResponseBuilder setJSONContent(Object objectToSerialize) + public ResponseBuilder setJSONContent(String json) + public Response build() throws InvalidResponseException + +} +``` + +- Response status code is `200` (OK) by default +- It is recommended to `setMimeType` before setting content. + +---- + +### Building + +Following qualities are **required**: + +- Response status code must be between 100 (exclusive) and 600 (inclusive) +- Content must be defined with one of `setContent` or `setJSONContent` methods. +- MIME Type must be set with `setMimeType` if Content is not 0 bytes + +If these qualities are not met, an `InvalidResponseException` is thrown on `ResponseBuilder#build`. + +Some methods of the builder set mimetype and content automatically: +- `redirectTo` +- `setJSONContent` +- `setContent(String)` adds `"; charset=utf-8"` to the MimeType if mimetype was set before the call. + +---- + +### Examples + +```java +// a html page (utf-8 string) +String html; +Response.builder() + .setMimeType(MimeType.HTML) + .setContent(html) + .build(); + +// javascript (utf-8 string) +String javascript; +Response.builder() + .setMimeType(MimeType.JS) + .setContent(javascript) + .setStatus(200) + .build(); + +// Image +byte[] bytes; +Response.builder() + .setMimeType(MimeType.IMAGE) + .setContent(bytes) + .setStatus(200) + .build(); + +// Image, custom mimetype +byte[] bytesOfSvg; +Response.builder() + .setMimeType("image/svg+xml") + .setContent(bytesOfSvg) + .setStatus(200) + .build(); + +// JSON, serialized using Gson +Response.builder() + .setMimeType(MimeType.JSON) + .setJSONContent(Collections.singletonMap("example", "value")) + .build(); + +// Redirection to another address +Response.builder().redirectTo(location).build(); +``` + +> 💡 **Tips** +> - Many Mime types are already defined in [`MimeType`-class]() +> - You can find more example uses in [`ResponseFactory`-class]() +> - `setHeader(String, Object)` assumes headers that can have multiple values are split with `;` character +> - `setJSONContent(String)` assumes the String is UTF-8 +> - You can use `WebResource` of `ResourceService` directly with `setContent(WebResource)` + +---- + +## Registering your Resolvers + +There are two methods to register the resolver, one for Start of the path and one for regex. The path always starts with a `/` character. + +```java +ResolverService svc = ResolverService.getInstance(); +Resolver resolver = new YourResolver(); +if (!svc.getResolver("/start/of/path").isPresent()) { + svc.registerResolver("PluginName", "/start/of/path", resolver); +} + +Resolver resolver = new YourResolver(); +if (!svc.getResolver("/matchingRegex").isPresent()) { + svc.registerResolver("PluginName", Pattern.compile("^/regex"), resolver); +} +``` + +`getResolver("/path")` is used here to ensure that no other plugin already has the path taken. + +Be aware that Plan defines some Resolvers like `/v1/`, `^/` and `/server` to perform its functionality. It is recommended to register all non-page resources with paths like `/pluginname/js/...` etc. + +# ResourceService + +Requires Capability `PAGE_EXTENSION_RESOURCES` + +ResourceService can be obtained with +```java +ResourceService svc = ResourceService.getInstance(); +``` + +## Making resources customizable + +Making resources customizable is done by calling `getResource` method whenever you need a resource (when building a `Response` for example). + +```java +ResourceService svc = ResourceService.getInstance(); +svc.getResource("PluginName", "example.html", () -> WebResource.create(content)) +``` + +- If the resource name ends with .html, it is possible for other API users to add javascript or stylesheet snippets to it. +- After this call Plan will add a config option so that user can customize the file if they wish to do so. + +---- + +### `WebResource`-class + +This class represents the resource, and can be created with following static methods: +```java +public interface WebResource { + static WebResource create(byte[] content) + static WebResource create(String utf8String) + // Closes the inputstream + static WebResource create(InputStream in) throws IOException +} +``` + +You can also implement your own if necessary following the interface + +```java +public interface WebResource { + byte[] asBytes(); + String asString(); // Assumed UTF-8 + InputStream asStream(); +} +``` + +## Adding javascript to .html resource + +`` snippets can be added to 3 `Position`s in a .html file: + +- `PRE_CONTENT` - to the ``, loaded before content +- `PRE_MAIN_SCRIPT` - before `