# Server-side functionality

## Overview

The provided PHP classes can be split into several groups:
1. server-side message publishing
1. token management
1. rest service
1. nchan-related helped classes for nchan hooks (described elsewhere)

### Server-side message publishing

The _server-side message publisher_ provides a flexible message dispatcher intended to publish messages on various media. For that an interface for a message publisher is provided with two implementations:
* `HttpPublisher` that publishes messages with HTTP POST requests, for instance to locations served by Nchan
* `TouchPublisher` that notifies the OS about messages by touching particular files.

Once constructed, publishers are added to a message dispatcher that from now on is used to publish messages.

The `MessageDispatcher` class supports PSR-3 loggers via the `LoggerAwareTrait`. If a logger is provided, the dispatcher will log all publishing errors.

#### Example
```php
use IMATHUZH\Messenger\{MessageDispatcher,HttpPublisher,TouchPublisher};

// Create a dispatcher an add some publishers
$dispatcher = (new MessageDispatcher())
    ->addPublisher(
        // Publishes messages using Nchan
        new HttpPublisher('http://localhost:8080/messenger/mysite', [
            'Content-Type' => 'text/json'
        ])
    )->addPublisher(
        // Touches files watched by an inotify-based script
        new TouchPublisher('/var/messanger', [
            'p' => 'page',
            'c' => 'command',
            't' => ''
        ])
    );

// Dispatch messages: (channels, name, payload=null)
// Note: processing publishing errors are explained later
$dispatcher->publishMessage(['t_Person'],
                            'updated',
                            ['id'=>433, 'name'=>'Joe']);
$dispatcher->publishMessage(['chat','chatadmin'],
                            'control',
                            'User jdoe has joined');
$dispatcher->publishMessage(['chatuserjdoe'],
                            'warning',
                            'Behave or you\'ll be kicked out!');
```


#### Example

```php
$token = AuthorizationToken::createMessengerToken(
    'mygroup',         // the group of channels
    ['pub1','pub2'],   // a list of channels for publishing
    ['sub1']           // a list of channels for listening
);

$storage = new TokenStorage($_SESSION['tokens']);
$tokenId = $storage->saveToken($token);

// Expose the token ID to the requesting client
echo $tokenId;
```


### The rest service

The _rest service_ is implemented by the script `php/Hooks/rest.php` and implements currently the following endpoints.

|Request                               |Description              |
|--------------------------------------|-------------------------|
|`POST token/merge`                    |Merges an array of token IDs (provided as a JSON array in the request body) into a single one that is returned.|
|`GET token/channels/{tokenId}/{group}`|Lists channels for a given token and group.|
|`GET token/groups/{tokenId}`          |Lists groups covered by the token.|
|`GET token/{tokenId}`                 |Decodes all data stored in a token.|
|`GET test/{sip}`                      |A test endpoint for checking the rest service. The `sip` parameter offers to check if the QFQ session is accessible.|

The following HTTP responses are generated by the rest endpoint.

|Situation       |HTTP code|Returned value                                  |
|----------------|------|---------------------------------------------------|
|No error occured|200   |`{"success":true, "data":<value from the handler>}`|
|Rejected request|custom|`{"success":false, "reason":<rejection message>}`  |
|Exception thrown|500   |`{"success":false, "reason":<rejection message>}`  |

The request is rejected in the following scenarios:
1. The request path does not match the base URL (HTTP 405, invalid endpoint)
1. No matching handler has been found (HTTP 404, not found)
1. The request handler rejected the request explicitly (see below)

### Token management

The messenger uses a token-based authorization system to restrict access to channels. A token is an instance of `TokenInterface`, like `AuthorizationToken`, and is provided by a `TokenStorage`.


## Publishing messages

### Message dispatchers

A message dispatcher implements the interface
```php
interface MessageDispatcherInterface
{
    public function addPublisher(MessagePublisherInterface $publisher): self;

    public function publishMessage(array $channels,
                                   string $name,
                                   mixed $payload): array;
}
```
* `addPublisher` - adds a publisher to the internal collection.
* `publishMessage` - encodes a message and calls all publishers to handle it. Returns an array of `MessagePublisherError` objects that wrap errors returned by publishers.

The package provides the class `MessageDispatcher` with a minimal implementation and no configuration. It can be extended or used as is.


### MessagePublisherError
A data class representing an error returned by a publisher. It has three properties:

|Property   |Type                       |Description                                               |
|-----------|---------------------------|----------------------------------------------------------|
|`publisher`|`MessagePublisherInterface`| A reference to the publisher                             |
|`key`      |`string`                   | Represents the medium to which the message was published |
|`message`  |`string`                   | The error message                                        |

**Example**
```php
$dispatcher = new MessageDispatcher();
$dispatcher->addPublisher($somePublisher);

// Publish a message
$errors = $dispatcher->publishMessage(['ch1','ch2'], 'test', ['value' => 42]);
// Process the errors
foreach($errors as $error) {
    echo $error->publisher::class . " returned an error " . $error->message
                                  . " on " . $error->key;
}
```

### Message publishers

A message publisher must implement the interface
```php
interface MessagePublisherInterface
{
    public function publishMessage(array $channels, string $msg): Iterator;
}
```
where the only function `publishMessage` is called by a `MessageDispatcher` to publish a message. This message must return in key-value iterator that iterates over potential errors:
* the value is interpreted as the error message
* the key is interpreted as the medium on which the message was published.

The package comes with two message publisher classes.

#### HttpPublisher

```php
class HttpPublisher implements MessagePublisherInterface
{
    public function __construct(string $basepath,
                                array $headers = [],
                                array $options = []);

    protected function pathsFromChannels(array $channels): Iterator;

    protected function isValidResponseCode(int $httpCode): bool;
    ...
}
```
This publisher makes an HTTP POST request with the message as the body to all urls `$basepath/$subpath`, where `$subpath` iterates over `pathsFromChannels()`. By default only one subpath is returned: `pub-$csch`, where `$csch` is a comma-separated list of all channels.

Use `$headers` to specify headers of the request, such as
* `Content-Type: text/json`
* `Authorization: <token>`
* ...

Use `$options` to specify additional options of the request, such as `timeout`.

**Example**
```php
$dispatcher = new MessageDispatcher();
$dispatcher->addPublisher(new HttpPublisher('http://localhost/messenger'));
$dispatcher->addPublisher(new HttpPublisher(
    'http://example.com/subdomain/channels/',
    ['Authorization' => 'secretToken']
));

// This results in HTTP requests to
// - http://localhost/messenger/pub-ch1,ch2
// - https://example.com/subdomain/channels/pub-ch1,ch2
$dispatcher->publishMessage(['ch1','ch2'], 'test');
```

**Errors**

An error represented by `MessagePublisherError` has the request URL as its key. The error message can be one of the following.

| Message                          | Meaning                                                                                                       |
|----------------------------------|---------------------------------------------------------------------------------------------------------------|
|*bad request*                     | No response received. Most likely an invalid URL or connection issues.                                        |
|*bad response*                    | The response was not in an HTTP format.                                                                       |
|*invalid HTTP status &lt;code&gt;*| An HTTP response with status code not accepted by `isValidHttpResponse()` (default: anything other than 2xx). |

#### TouchPublisher

```php
class TouchPublisher implements MessagePublisherInterface
{
    public function __construct(string $rootDir,
                                array $directories);

    protected function filesFromChannels(array $channels): Iterator;
}
```
Publishing a message touches (or creates) files associated with channels as follows.
1. A channel has an associated file only if it is of the form `${type}_${name}`, where `$type` is a key of the array `$directories` provided to the constructor.
1. For such channel the associated file is given as
   ```
   $rootDir/$directories[$type]/$name
   ```
1. Other channels are ignored.

Note that the content of the message is ignored by this publisher.

**Example**
```php
$dispatcher = new MessageDispatcher();
$dispatcher->addPublisher(new TouchPublisher(
    '/var/www/signals', [       // trailing slash is not required
        'p' => 'page',
        'pa' => 'page/admin',   // longer paths are allowed
        'cmd' => 'command',
        'tbl' => ''             // directly in the root folder
    ]
));

// This touches
// - /var/www/signals/page/login
// - /var/www/signals/command/refresh
// Channel 'other' is ignored.
$dispatcher->publishMessage(['p_login', 'cmd_refresh', 'other'], 'test');
```

**Errors**

Unsuccessful touches are represented by errors with a generic message (*failed to touch file*) and the file name as the key.


## Token Management

The messenger uses a token-based authorization system to restrict access to channels. A token is an instance of `TokenInterface` and is provided by a `TokenStorage`.

### AuthorizationToken

This class represents a set of permissions (publisher, listener, controller) for multiple groups of channels.

```php
use IMATHUZH\Messenger\AuthorizationToken;

// Create a token for a group with specific channels
$token = AuthorizationToken::createMessengerToken('mygroup', ['pub1'], ['sub1']);

// Wildcards are supported
$token = AuthorizationToken::createPublisherToken('admin', ['chat.*', 'system.status']);

// Controller tokens have only subscription channels
$token = AuthorizationToken::createControllerToken('mygroup', ['sub1','pub1']);

// Merge multiple tokens into one
$merged = AuthorizationToken::mergeTokens($token1, $token2);
```

### TokenStorage

This class manages the storage and retrieval of tokens, typically using the PHP `$_SESSION` array.

```php
use IMATHUZH\Messenger\TokenStorage;

// Use the arrays 'user-tokens' and 'system-tokens' from PHP session for storage.
// Created tokens are saved in the first array.
$tokenStorage = new SessionToken($_SESSION['user-tokens'], $_SESSION['system-tokens']);

// Save a token and get its ID
$tokenId = $tokenStorage->saveToken($token);

// Load a token by ID
$token = $tokenStorage->loadToken($tokenId);
```
A token is stored internally as a query string obtained from the array with keys `<group>/<channel-type>` and comma-separated lists of channels as values. For instance, the token represented as
```
group1:
  pub: ch1, ch2
  sub: ch3, ch4
group2:
  pub: ch*
```
is stored as the string
```
group1/pub=ch1,ch2&group1/sub=ch3,ch4&group2/pub=ch*
```
The reason for picking `/` instead of `.` is that dots are replaced with underscores by `http_build_query`.


## REST service

The rest service in implemented by classes from `php/src/Rest`:
* `RestProcessor` is the main class that reads the request, finds the corresponding handler and executes it in order to create a response.
* `RestRequest` encapsulates the HTTP request into an object that is exposed to request handlers as `RequestInterface` (a request handler is allowed to use only the functionality exposed by this interface).

The main part of `rest.php` are the lines
```
$app = new RestProcessor(REST_URL_BASE, [
    '/endpoint/one/' => [
        'GET' => function(RequestInterface $request): mixed { ... },
        'POST' => function(RequestInterface $request): mixed { ... },
        ..
    ],
    '/endpoint/two/' => [
        'GET' => function(RequestInterface $request): mixed { ... },
        'POST' => function(RequestInterface $request): mixed { ... },
        ..
    ],
    ...
]);
$app->process(RestRequest::fromServerRequest());
```
where `REST_URL_BASE` is the constant from `common.php` that contains the base URL for the rest endpoint (`/messenger/rest/` with the provided nginx configuration) and the array of request handlers has rest endpoints and top level keys and HTTP request methods (GET, POST, PUT, DELETE, PATCH) as second-level keys. Each handler is a function that takes the request object as its parameter and can return any value. It is encourange to add own handlers as needed in custom installations.

### Handlers and RequestInterface

The object `$request` passed to a request handler provides the following functionality.

|Method|Description|
|---|---|
|`getPath()`|The absolute path of the request.|
|`getMethod()`|The method of the request.|
|`getBody($patterns)`|The both of the request as an JSON object if no pattern is provided. Otherwise, the array obtained from `$patterns` by replacing values with the matching values in the request body, assuming they match the patterns. If the patterns are not satisfied or some values are missing then the request is rejected.|
|`getQuery($patterns)`|Like above but for the query parameters.|
|`getParams($patterns)`|Like above but for the list of parameters passed in the URL, i.e. the values on the path below the handled endpoint (empty values are ignored). For instance, if the request handler under `/endpoint/one/` is used for `/endpoints/one/foo//bar` then the parameters are `['foo', 'bar']`.|
|`assertMatches($value, $pattern)`|A helper method that rejects the request if the provided value does not match the pattern.|
|`$request->reject($msg, $code=403)`|Rejects the request with the provided message and HTTP code (403 by default).|

### Helper functions

The file `common.php` included by `rest.php` offers the following helper methods:

1. `openSession(bool $readonly=false)` opens the default PHP session
1. `loadTokenStorage(bool $readonly=false)` loads the token storage from the PHP session. A session is opened if not active already.

The sessions are govered by constants
* SESSION_COOKIE - the name of the cookie with the session id
* SESSION_KEY - the name of the key in the `$_SESSION` array that contains tokens.

The provided implementation is aimed at a standard QFQ installation.
