mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-11-25 03:55:37 +01:00
Created APIv5 PageExtension API (markdown)
parent
4fff66e2ca
commit
d58e7041ca
390
APIv5-PageExtension-API.md
Normal file
390
APIv5-PageExtension-API.md
Normal file
@ -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<Response> 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<WebUser> getUser() // Plan WebUser if present
|
||||||
|
public Optional<String> 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
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example of URIPath usage inside Resolver#resolve-method</summary>
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
Optional<Response> 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 <plan-address>/help
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Example of URIQuery usage inside Resolver#resolve-method</summary>
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
Optional<Response> resolve(Request request) {
|
||||||
|
URIQuery query = request.getQuery();
|
||||||
|
String serverUUID = query.get("serverUUID")
|
||||||
|
.orElseThrow(() -> new BadRequestException("'serverUUID' parameter required");
|
||||||
|
// return response for the specific server
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
### `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<Response> 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
|
||||||
|
|
||||||
|
`<script src="./js"></script>` snippets can be added to 3 `Position`s in a .html file:
|
||||||
|
|
||||||
|
- `PRE_CONTENT` - to the `<head>`, loaded before content
|
||||||
|
- `PRE_MAIN_SCRIPT` - before `<script id="mainScript">` (Recommended for libraries and page stucture modifications)
|
||||||
|
- `AFTER_MAIN_SCRIPT` - at the end of the page before `</body>` tag (Recommended for data loading)
|
||||||
|
|
||||||
|
This can be done with the following call:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ResourceService svc = ResourceService.getInstance();
|
||||||
|
svc.addScriptsToResource("PluginName", "customized.html", Position.PRE_CONTENT, "./some.js", "another.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
You can add a query to the URL if your javascript needs parameters, eg `"./some.js?value=some"` and access them with `URIQuery` in `Resolver#resolve`.
|
||||||
|
Some Plan resources use `${placeholder}` replacement - in these cases you can use `"./some.js?player=${playerUUID}"` for example.
|
||||||
|
|
||||||
|
## Adding stylesheet to .html resource
|
||||||
|
|
||||||
|
`<link href="./css" rel="stylesheet"></link>` snippets can be added to 3 `Position`s in a .html file:
|
||||||
|
|
||||||
|
- `PRE_CONTENT` - to the `<head>`, loaded before content
|
||||||
|
- `PRE_MAIN_SCRIPT` - before `<script id="mainScript">` (Recommended for libraries)
|
||||||
|
- `AFTER_MAIN_SCRIPT` - at the end of the page before `</body>` tag
|
||||||
|
|
||||||
|
This can be done with the following call:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ResourceService svc = ResourceService.getInstance();
|
||||||
|
svc.addStylesToResource("PluginName", "customized.html", Position.PRE_MAIN_SCRIPT, "./some.css", "another.css");
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
The PageExtension API will be expanded with javascript methods that allow easier modification of the site and interaction with the Plan webserver. At the moment only following method is quaranteed at position `PRE_MAIN_SCRIPT`:
|
||||||
|
|
||||||
|
`jsonRequest(address, callback)`:
|
||||||
|
- `address` : String
|
||||||
|
- `callback` : Function (json : Object, error : String)
|
||||||
|
- Makes an xmlhttprequest to the Plan webserver or elsewhere
|
||||||
|
|
||||||
|
Library methods are also available after `PRE_MAIN_SCRIPT` (For example HighCharts APIs) - You might need to check each file you're adding snippets to to be sure.
|
Loading…
Reference in New Issue
Block a user