# Architecture This file contains description of the software architecture. Before starting any development, consult this file. The plugin is made out of a Gutenberg widget that communicates with the backend using endpoints handled by PHP. The endpoints are defined in the *controllers*, that handle everything between the request and creating a application specific models to pass down to the application. It hides all the details about network communication. ## Forms The main component of the plugin are the *forms*. The administrator defines information that the user submits to them. There are two objects, the *definition* and *submission*. The administrator makes the definition: list of fields & elements the form is made out of. The user fills in the information and submits the form. ### Submitting forms Use POST endpoint `forms/{form_id}` to submit form. The `RsvFormProcessor` will validate the form and call handler for each element in the definition. ## JavaScript The JavaScript is mostly used for frontend components and slightly for the administration editor of the Wordpress widget. Each JS file and symbol in global scope must have the prefix `Rsv` or `rsv`. Each component should be encapsulated within it's own `const` object in separate file. The root directory is `assets/js/components/`. The other JavaScript files are split between user-space and admin-space. Example of admin-space component is Wordpress-looking datagrid. Example of user-space component is calendar. The JavaScript code should always use a resource object for communicating with the backend REST API. See **Data Layer** below. ### Error handling ## Data Layer All REST API communication goes through the data layer in `assets/js/datasource/`. ### RsvDataSource `RsvDataSource` is a factory object with a single method: ```js RsvDataSource.create_rsv_resource(base_url, { nonce }) ``` It returns a resource object that wraps `fetch` and provides a consistent CRUD interface: | Method | Signature | HTTP | |---|---|---| | `get_page` | `(skip = 0, limit = 20, params = {})` | `GET base_url?skip=…&limit=…` | | `get` | `(id)` | `GET base_url/{id}` | | `post` | `(data)` | `POST base_url` | | `put` | `(id, data)` | `PUT base_url/{id}` | | `delete` | `(id)` | `DELETE base_url/{id}` | Every method returns a `Promise`. Non-2xx responses reject with an `Error`. A `204 No Content` response resolves to `null`. When a `nonce` is supplied, it is sent as the `X-WP-Nonce` header, which satisfies WordPress REST API authentication. ### Resource files Each REST endpoint has its own factory function in `assets/js/datasource/`. The factory calls `RsvDataSource.create_rsv_resource` and fills in the endpoint path using the `ReservairServiceAPI.restUrl` global (injected by PHP via `wp_localize_script`). | File | Factory | Endpoint | |---|---|---| | `RsvReservationResource.js` | `RsvReservationResource()` | `/reservation` | | `RsvFormDefinitionResource.js` | `RsvFormDefinitionResource()` | `/form-definition` | | `RsvTimetableResource.js` | `RsvTimetableResource()` | `/timetable` | | `RsvTimetableCapacityResource.js` | `RsvTimetableCapacityResource(id)` | `/timetable/{id}/capacity` | To add a new endpoint, create a new file following the same pattern and register it in `includes/RsvAssetsDefinition.php`. ## Admin REST Forms Admin pages that write to the REST API follow a standard pattern to keep the code consistent and easy to extend. ### HTML Use a `
` element with three data attributes: ```html
``` - `action` — the full REST endpoint URL (use `get_rest_url` + `esc_url`). - `data-method` — HTTP verb (`POST`, `PATCH`, `PUT`, `DELETE`). Standard HTML forms only support GET/POST; this attribute carries the real verb for the JS layer. - `data-success-msg` — human-readable message passed to `show_notice` on success. Fields that should not appear as literal HTML inputs (e.g. a ``) runs independently of the submit listener and writes its results into named `` elements so `FormData` picks them up transparently. --- ## Forms The plugin allows administrators to define & display forms on pages as a widget. The definitions is an ordered list of element definitions in a JSON format. The required attributes for each element are `name`, `label` and `is_required`. The optional are `description`. To define new element, create a PHP class in `includes/Services/Forms/` like `RsvFormHelloElementHandler` that derives from `RsvFormElementHandler` and implement all the abstract methods. The `RsvFormHandler` validates and commits the forms. The form values are saved as JSON with keys for the input elements. ## PHP ### Controllers Builds the REST API. Should not assume anything about the application and mainly just pass built domain object to the application layer. Each controller manages a single resource and defines operations that the user can do with the object. --- ## Permissions & Capabilities All administrative functionality — both the REST API and the admin pages — is gated by a **single custom capability**, and every REST route declares its intended audience through a small policy class. There are two source-of-truth symbols; nothing else should appear in authorization code: | Symbol | File | Purpose | |---|---|---| | `RsvCapabilities::MANAGE` (`'manage_reservations'`) | `includes/RsvCapabilities.php` | The capability that authorises managing reservation data. | | `RsvRestPolicy::admin()` / `RsvRestPolicy::open()` | `includes/Controllers/RsvRestPolicy.php` | The `permission_callback` tiers every route uses. | > **Rule:** never write `'manage_options'` or `'__return_true'` in this plugin. Routes go through `RsvRestPolicy`; menus and page-level checks use `RsvCapabilities::MANAGE`. This keeps the whole authorization surface greppable and consistent. ### The capability `manage_reservations` is a **custom** capability (not WordPress's built-in `manage_options`) so that reservation management can later be granted to a non-admin role without also handing over the whole site. By default it is granted only to the `administrator` role. ### Lifecycle WordPress only runs the activation hook on *activate*, never on a plugin *update* — so granting the cap solely on activation would silently lock admins out after an update. `RsvCapabilities` handles all three moments: | Moment | Call | Effect | |---|---|---| | Activation | `RsvInstaller::install()` → `RsvCapabilities::ensure()` | Grants the cap to the default roles. | | Every request | `rsv_bootstrap()` → `RsvCapabilities::ensure()` | Self-heals after an update. No-op once the stored version matches. | | Uninstall | `uninstall.php` → `RsvCapabilities::revoke()` | Removes the cap from every role and clears the marker. | `ensure()` is idempotent and guarded by the `rsv_caps_version` option compared against `RsvCapabilities::VERSION`. **Bump `VERSION` whenever the capability set changes** so existing installs re-grant on their next request. Because `ensure()` runs on every load, admins never need to manually reactivate the plugin. ### Authorization tiers Each route's `permission_callback` is one of: | Tier | Callback | Means | |---|---|---| | **admin** | `[RsvRestPolicy::class, 'admin']` | Caller must have `manage_reservations`. Returns a `WP_Error` (401 logged-out / 403 under-privileged) otherwise. | | **open** | `[RsvRestPolicy::class, 'open']` | Either genuinely public (form submission, availability lookups) **or** a *capability URL* whose secret is validated inside the handler. | A **capability URL** is a public endpoint authorised by an unguessable secret in the request rather than by session auth — e.g. the maintainer accept/refuse links (`/timetable-reservation/accept|refuse/{code}`), the Google webhook (secret channel id in headers), and the OAuth callback. Each is marked `open()` with a comment naming where its secret is checked. > **Rule:** any `open()` route that is not fully public **must** authorise its caller from the request inside the handler. `open()` performs no check itself. ### Using a capability **In a REST route** (`register_rest_route`): ```php register_rest_route($this->namespace, '/' . $this->resource_name, [ 'methods' => 'POST', 'callback' => [$this, 'create'], 'permission_callback' => [RsvRestPolicy::class, 'admin'], // or 'open' ]); ``` **In the admin menu** (`add_menu_page` / `add_submenu_page`) — pass the constant as the capability argument: ```php add_menu_page('Reservations', 'Reservations', RsvCapabilities::MANAGE, 'reservations-settings', 'rsv_reservations_page'); ``` **In a page-level / inline check:** ```php if (!current_user_can(RsvCapabilities::MANAGE)) { return; } ``` ### Adding a new capability The current design hardcodes the single `MANAGE` capability for the `administrator` role. To introduce a second (e.g. a read-only `view_reservations` for an `editor`): 1. **Declare it** as a constant on `RsvCapabilities` (`public const VIEW = 'view_reservations';`). 2. **Grant it** in `RsvCapabilities::ensure()` and **remove it** in `revoke()`. Once there is more than one capability, replace the single-cap logic with a `capability => [roles]` map and iterate it in both methods, so the two stay in sync. 3. **Bump `RsvCapabilities::VERSION`** so `ensure()` re-applies the new grant on existing installs at their next request. 4. **Expose a tier** for it: add a method to `RsvRestPolicy` (e.g. `viewer()` checking `current_user_can(RsvCapabilities::VIEW)`), following the `admin()` pattern (return `true` or a `WP_Error` with `rest_authorization_required_code()`). 5. **Reference it** from the relevant routes, menu entries, and page checks — never the literal string.