11 APIv5 PageExtension API
Aurora Lahtela edited this page 2023-03-04 11:10:13 +02:00

Plan Header

Plan API version 5 - Page Extension API

This page is about API that is available in version 5.1 and above. API can be called on all platforms.

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.
  • pageExtensionApi - Javascript api for extending the React website

Table of contents


ResolverService

Requires Capability PAGE_EXTENSION_RESOLVERS

ResolverService can be obtained with

ResolverService svc = ResolverService.getInstance();

Creating a Resolver

First interface to understand is the Resolver. (Javadoc has been truncated)

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

Here are the methods of Request:

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.

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.

💡 Tips

  • headers that can have multiple values (Like Cookie) are split with ; character
Example of URIPath usage inside Resolver#resolve-method
@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();
    }
}
Example of URIQuery usage inside Resolver#resolve-method
@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
}

canAccess-method

This method should check if the user viewing the URL is allowed to do so.

Here is an example that only checks the user's permission regardless of the accessed URIPath or URIQuery.

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");
        }
    }
}

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

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 Requests to appropriate Response objects.

public class YourResolver implements Resolver {
    @Override
    Optional<Response> resolve(Request request) {
        return Optional.of(
            newResponseBuilder()
                // calls to builder methods
                .build()
        );
    }
}

You can use newResponseBuilder() to begin building a new Response. You can learn more about the builder below.

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.

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.

Examples

// 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();

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.

💡 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.

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

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).

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:

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

public interface WebResource {
    byte[] asBytes();
    String asString(); // Assumed UTF-8
    InputStream asStream();
}

Adding javascript to .html resource

This can be done with the following call:

ResourceService svc = ResourceService.getInstance();
svc.addScriptsToResource("PluginName", "customized.html", Position.PRE_CONTENT, "./some.js", "another.js");

<script src="./js"></script> snippets can be added to 3 Positions in a .html file:

  • PRE_CONTENT - to the <head>, loaded before content, recommended for stylesheets
  • PRE_MAIN_SCRIPT - Before React script runs, but after pageExtensionApi.js has run, load libraries or call pageExtensionApi

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

This can be done with the following call:

ResourceService svc = ResourceService.getInstance();
svc.addStylesToResource("PluginName", "customized.html", Position.PRE_MAIN_SCRIPT, "./some.css", "another.css");

<link href="./css" rel="stylesheet"></link> snippets can be added to 3 Positions in a .html file:

  • PRE_CONTENT - to the <head>, loaded before content, recommended for stylesheets
  • PRE_MAIN_SCRIPT - Before React script runs, but after pageExtensionApi.js has run

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.

Javascript API

There is an example plugin for using the javascript API https://github.com/plan-player-analytics/Example-Plan-API-Plugins/tree/master/PageExtension-API-Example

Javascript API is defined in https://github.com/plan-player-analytics/Plan/blob/master/Plan/react/dashboard/public/pageExtensionApi.js

Methods

pageExtensionApi.registerElement('beforeElement', 'card-body-server-as-numbers', render, unmount);

Parameter Accepted values Description
0 'beforeElement' or 'afterElement' Should the element you're rendering be added above or below the element
1 '{id of an element with 'extendable' class}' Any element on the page with "extendable" class can be used. If your use-case is not covered, please contact on discord and we'll add more extendable items for it - We didn't add it to everything since there was so much content to go through.
2 async function Render function, example: (context) => {return '<p>Hello world</p>'}. More about rendering below
3 async function Unmount function, in case your code is registering listeners or timers, you can unregister them here.

Finding IDs

Open Browser Dev Tools > Inspector and search for elements with .extendable (class)

image

Render function

Render function can use either html elements or html as a String to render elements.

The context contains useful information about the current page, as well as other utilities. console.log(context) to see contents

const render = async (context) => {
  /* Any code that needs to run before the element is added to DOM */
  return '<p>Hello world</p>';
}

const render = () => {
  const para = document.createElement("p");
  para.innerText = "Hello world";
  return para;
}