Remote Procedure Call (RPC) Plugin

The RPC plugin provides an easy way to implement a Remote Procedure Call API. The plugin uses JSON-RPC protocol version 2. You can read the specification here.

Install

RPC Plugin is dependent on Jimbo.

  1. Check the Jimbo plugin has been installed
  2. Add the plugin:
    git submodule add git@gitlab.varteq.com:FestiPlugins/php_festi_plugin_rpc.git plugins/Rpc
  3. Run dump from install folder
  4. Add area and rule
    INSERT INTO festi_url_areas (ident) VALUES ('rpc');
    INSERT INTO festi_url_rules (plugin, pattern, method) VALUES ('Rpc', '~^/rpc/(.*)$~', 'onJsonCallMethod');
    And add this rule to festi_url_rules2areas for area rpc
  5. Call RPC method syncRpcMethods
  6. Configure permission section
  7. Add column access_token to users table
  8. Configure index.php and .htaccess. Examples can be found in install/rpc/
  9. Add composer package festi-team/festi-framework-serialization

Authentication

Access token should be stored in the users table in the access_token column. You have two ways for authentication:

  1. Put token to GET param token:
    https://RPC_HOST/?token=XXX
  2. Put token in the X-Authorization header:
    X-Authorization: XXX

Request

You can send parameters using an array or object:

{"jsonrpc":"2.0", "method":"getScraperActions", "params":["linkedin"], "id":1}
{"jsonrpc":"2.0", "method":"getScraperActions", "params":{"ident":"linkedin"}, "id":1}

If you use an object, please ensure that the key name matches the parameter name in the RPC method annotation.

The RPC plugin supports batch requests:

[
  {"jsonrpc":"2.0","method":"getScraperActions","params":["linkedin"],"id":1},
  {"jsonrpc":"2.0","method":"getScraperActions","params":{"ident":"google"},"id":2}
]

For easy debugging, you can use the GET parameter rawRequest:

https://RPC_HOST/?rawRequest={"jsonrpc":"2.0","method":"syncRpcMethods"}

RPC Method Implementation

Use @rpc annotation to define RPC method:

class ScraperPlugin extends DisplayPlugin
{
    /**
     * @rpc getScraperActions
     * @param string $ident
     * @section scraper
     * @return array
     */
    public function getActionsByIdent(string $ident): array
    {
        ...
    }
}

If @rpc doesn't have a name, the real method name will be used:

/**
 * @rpc
 * @param bool $idUser
 * @return mixed
 * @throws PermissionsException
 */
public function getMentors($idUser = false)
{
    ...
}

You can use on* methods and methods with DGS as well:

/**
 * @rpc getProjects
 * @urlRule ~^/projects/$~
 * @area backend
 * @param Response $response
 * @return bool
 * @throws SystemException
 * @section manage_projects
 */
public function onDisplayList(Response &$response)
{
    $store = $this->createStoreInstance("projects");

    $store->onRequest($response);

    return true;
}

Parameters for the result will be retrieved from the Response object

Events

The RPC plugin dispatches two events during sync (when building the list of RPC methods from plugin annotations). Other plugins can listen to extend or customize method discovery and stored values.

BeforePluginAnnotationsParseEvent::TYPE

This event is triggered once per plugin, before PluginAnnotations::parse() is called for that plugin. It can be used to add extra annotation names so they are parsed from docblocks and appear in $methodInfo (e.g. mcpReadOnly, mcpDestructive for MCP tools).

Event data: * getPluginContext() – plugin being scanned * getPluginAnnotations() – the PluginAnnotations instance * getAnnotationNames() / setAnnotationNames(array) – list of annotation names to parse (listeners can add more)

init.php:

$this->core->addEventListener(
    BeforePluginAnnotationsParseEvent::TYPE,
    function (BeforePluginAnnotationsParseEvent $event): void {
        $names = $event->getAnnotationNames();
        $names[] = 'mcpReadOnly';
        $names[] = 'mcpDestructive';
        $event->setAnnotationNames($names);
    }
);

PrepareRpcMethodValuesEvent::TYPE

This event is triggered once per RPC method, after the method item (plugin, method, rpc_method, params, description, etc.) is built and before it is written to the rpc_methods table. It can be used to add or change keys in the method values (e.g. is_read_only) so they are stored and available later.

Event data: * getValues() / setValues(array) – the method row to store (mutate and set back) * getPluginContext() – plugin * getMethodName() – method name * getMethodInfo() – parsed annotations for this method (including any added via BeforePluginAnnotationsParseEvent)

init.php:

$this->core->addEventListener(
    PrepareRpcMethodValuesEvent::TYPE,
    function (PrepareRpcMethodValuesEvent $event): void {
        $values = $event->getValues();
        $methodInfo = $event->getMethodInfo();
        $values['is_read_only'] = array_key_exists('mcpDestructive', $methodInfo) ? 0 : 1;
        $event->setValues($values);
    }
);

Update RPC Methods

All RPC methods are stored in the rpc_methods table. To update them, you can call the RPC method syncRpcMethods or call the method from code:

Core::getInstance()->getPluginInstance('Rpc')->syncRpcMethods();
or
https://RPC_HOST/?rawRequest={"jsonrpc":"2.0","method":"syncRpcMethods"}

Client

  • jQuery - https://github.com/Textalk/jquery.jsonrpcclient.js
let rpc = new jQuery.JsonRpcClient({ ajaxUrl: 'https://RPC_HOST/' });

rpc.call(
    'getScraperActions', ['linkedin'],
    function (response) {
        console.log("RESULT:", response);
    },
    function (error) {
        console.error(error);
    }
);

rpc.batch(
    function (batch) {
        batch.call('getScraperActions', ['linkedin'], function (response) {
            console.log("1", response);
        }, function (error) {
            console.error(error);
        });
        batch.call('getScraperActions', { "ident": "linkedin" }, function (response) {
            console.log("2", response);
        }, function (error) {
            console.error(error);
        });
    },
    function (all_result_array) { alert('All done.'); },
    function (error_data) { alert('Error in batch response.'); }
);

Override Authorization Logic

init.php:

assert($this instanceof Core);

$this->addEventListener(Core::EVENT_ON_AFTER_INIT, function () {
    $this->addEventListener(IRpc::EVENT_ON_TOKEN_LOGIN, function (FestiEvent &$event) {
        Core::getInstance()->getPluginInstance('YourNewPlugin')->onInterceptLoginByToken($event);
    });
});

public function onInterceptLoginByToken(FestiEvent &$event): void
{
    $isAuth = &$event->getTargetValueByKey('is_auth');
    $token = &$event->getTargetValueByKey('token');

    if (mb_strlen($token) == 32) { // system access token
        $isAuth = $this->core->getSystemPlugin()->signinByToken($token);
    } else {
        $isAuth = $this->_signInByGoogleTokenID($token);
    }

     if (!$isAuth) {
         throw new PermissionsException("Undefined access token.");
     }
}

Reusable API

The RPC plugin provides reusable type handling utilities that can be used by other plugins:

ParameterProcessor

The ParameterProcessor class provides public methods for type checking and casting:

  • isSimpleType(string $typeName): bool - Checks if a type is a simple/primitive type
  • castToType(mixed $value, string $typeName): mixed - Casts a value to the specified type

These methods are used internally by RPC for parameter processing and are also available for use by dependent plugins (e.g., MCPServer) to maintain consistency in type handling across the codebase.

Error Handling

The RPC plugin follows the JSON-RPC 2.0 specification for error responses. Errors are returned in the following format:

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Server error",
    "data": {}
  },
  "id": 1
}

Common error codes: - -32700 - Parse error - -32600 - Invalid Request - -32601 - Method not found - -32602 - Invalid params - -32603 - Internal error - -32000 to -32099 - Server error (custom errors)

Permissions

RPC methods can be protected using the @section annotation. The section name should match a permission section defined in your permissions system. If a user doesn't have access to the required section, a PermissionsException will be thrown.

For detailed information about the permissions system, see the Permissions documentation.

How Permission Checking Works

The permission system uses the following database tables:

  • festi_sections - Defines permission sections (identified by the @section annotation)
  • festi_section_actions - Maps plugin methods to sections and required permission masks
  • festi_sections_user_permission - User-specific permissions (overrides user type permissions)
  • festi_sections_user_types_permission - User type/role-based permissions

When an RPC method is called:

  1. The system looks up the method in festi_section_actions to find:
  2. The associated section (id_section)
  3. The required permission mask (2 = Read, 4 = Write, 6 = Execute)

  4. The system then checks the user's permissions by querying:

  5. festi_sections_user_permission for user-specific permissions (takes priority)
  6. festi_sections_user_types_permission for role-based permissions

  7. The user's granted mask must be greater than or equal to the required mask for the action.

Example

/**
 * @rpc getProjects
 * @section manage_projects
 * @return array
 */
public function getProjects(): array
{
    // Only users with 'manage_projects' permission can call this
}

To set up permissions for this method, you need to:

  1. Create the section in festi_sections (if it doesn't exist):

    INSERT INTO festi_sections (caption, ident, mask)
    VALUES ('Project Management', 'manage_projects', '6');

  2. Register the action in festi_section_actions:

    INSERT INTO festi_section_actions (id_section, plugin, method, mask, comment)
    VALUES (
        (SELECT id FROM festi_sections WHERE ident = 'manage_projects'),
        'MyPlugin',
        'getProjects',
        '2',  -- Read permission required
        'Get projects list'
    );

  3. Assign permissions to users or user types as needed.

Built-in RPC Methods

getDataGridStoreModel

Returns the DataGrid Store (DGS) model schema for a given plugin and store.

Parameters: - pluginName (string) or [0] - Name of the plugin - storeName (string) or [1] - Name of the store

Example:

{"jsonrpc":"2.0", "method":"getDataGridStoreModel", "params":["MyPlugin", "users"], "id":1}

Based on: RpcPlugin::onJsonDataGridStore

syncRpcMethods

Scans the project and updates the RPC methods registry in the database. This method should be called after adding or modifying RPC methods.

Parameters: None

Example:

{"jsonrpc":"2.0", "method":"syncRpcMethods", "params":[], "id":1}

Based on: RpcPlugin::syncRpcMethods

getStructureMenu

Returns the menu structure for a given area.

Parameters: - area (string) or [0] - Area identifier

Example:

{"jsonrpc":"2.0", "method":"getStructureMenu", "params":["backend"], "id":1}

Based on: Jimbo::getStructureMenu

getSchemeByUrl

Returns all schemes (URL rules and handlers) for a given URL and area.

Parameters: - url (string) or [0] - URL to analyze - area (string|null) or [1] - Optional area identifier

Example:

{"jsonrpc":"2.0", "method":"getSchemeByUrl", "params":["/projects/", "backend"], "id":1}

Based on: RpcPlugin::getSchemeByUrl

execUrl

Executes a request for a given URL and area, returning the response.

Parameters: - url (string) or [0] - URL to execute - area (string|null) or [1] - Optional area identifier

Example:

{"jsonrpc":"2.0", "method":"execUrl", "params":["/projects/list", "backend"], "id":1}

Based on: RpcPlugin::execUrl