Skip to content

Commit 408e206

Browse files
Merge pull request #896 from pfrest/next_minor
v2.8.0 Features & Fixes
2 parents 92e692d + 092802b commit 408e206

219 files changed

Lines changed: 8734 additions & 263 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,31 @@ updates:
1111
target-branch: "master"
1212
schedule:
1313
interval: "monthly"
14+
groups:
15+
composer:
16+
patterns:
17+
- "*"
18+
1419
- package-ecosystem: "pip"
1520
directory: "/"
1621
target-branch: "master"
1722
schedule:
1823
interval: "monthly"
24+
groups:
25+
pip:
26+
patterns:
27+
- "*"
28+
1929
- package-ecosystem: "github-actions"
2030
directory: "/"
2131
target-branch: "master"
2232
schedule:
2333
interval: "monthly"
34+
groups:
35+
github-actions:
36+
patterns:
37+
- "*"
38+
2439
- package-ecosystem: "npm"
2540
directory: "/"
2641
target-branch: "master"
@@ -29,20 +44,7 @@ updates:
2944
interval: "monthly"
3045
commit-message:
3146
prefix: "chore"
32-
33-
# LEGACY BRANCH (v1)
34-
- package-ecosystem: "composer"
35-
directory: "/"
36-
target-branch: "legacy"
37-
schedule:
38-
interval: "monthly"
39-
- package-ecosystem: "pip"
40-
directory: "/"
41-
target-branch: "legacy"
42-
schedule:
43-
interval: "monthly"
44-
- package-ecosystem: "github-actions"
45-
directory: "/"
46-
target-branch: "legacy"
47-
schedule:
48-
interval: "monthly"
47+
groups:
48+
npm:
49+
patterns:
50+
- "*"

.github/instructions/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
applyTo: ".github/instructions/**/*.md"
3+
---
4+
5+
# About `.github/instructions/`
6+
7+
This directory contains **GitHub Copilot path-scoped instructions**. Each `*.instructions.md` file declares an `applyTo` glob in its YAML frontmatter; Copilot automatically loads the matching file when you edit a file under that path.
8+
9+
## Files
10+
11+
| File | `applyTo` glob | Purpose |
12+
| ----------------------------- | ---------------------------------------------------------- | --------------------------------------------------------- |
13+
| `php.instructions.md` | `pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/**/*.inc` | Global PHP rules for all package source files. |
14+
| `models.instructions.md` | `.../RESTAPI/Models/**/*.inc` | Model class authoring rules. |
15+
| `endpoints.instructions.md` | `.../RESTAPI/Endpoints/**/*.inc` | Endpoint class authoring rules. |
16+
| `validators.instructions.md` | `.../RESTAPI/Validators/**/*.inc` | Reusable Validator authoring rules. |
17+
| `dispatchers.instructions.md` | `.../RESTAPI/Dispatchers/**/*.inc` | Background Dispatcher authoring rules. |
18+
| `tests.instructions.md` | `.../RESTAPI/Tests/**/*.inc` | TestCase authoring rules (custom framework, not PHPUnit). |
19+
20+
## How to extend
21+
22+
1. Copy an existing `*.instructions.md` and edit the `applyTo` glob.
23+
2. Keep each file focused on one path scope. Don't duplicate `AGENTS.md` — link to it.
24+
3. Keep the rules short and prescriptive. Long-form examples belong in `.github/skills/` or `docs/`.
25+
4. After editing, verify the `applyTo` glob actually matches the file you expect — Copilot only loads the file when the glob matches.
26+
27+
## Authoritative sources
28+
29+
These files are guard-rails. The full picture is:
30+
31+
- `AGENTS.md` (root) — architecture and rules.
32+
- `.github/skills/` — task playbooks with concrete examples.
33+
- `docs/CONTRIBUTING.md` and `docs/BUILDING_CUSTOM_*.md` — human reference docs.
34+
- `https://pfrest.org/php-docs/` — generated PHPDoc.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
applyTo: "pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/**/*.inc"
3+
---
4+
5+
# Dispatchers — `RESTAPI\Dispatchers\*`
6+
7+
Dispatchers run work in the background so HTTP requests stay responsive. Use a Dispatcher for any operation that can take more than ~1 second: filter reloads, service restarts, certificate issuance, HA sync, etc.
8+
9+
For full guidance and worked examples see:
10+
11+
- `.github/skills/validation-command-dispatcher.md`
12+
- `docs/BUILDING_CUSTOM_DISPATCHER_CLASSES.md`
13+
14+
## Required shape
15+
16+
```php
17+
<?php
18+
19+
namespace RESTAPI\Dispatchers;
20+
21+
use RESTAPI\Core\Dispatcher;
22+
23+
/**
24+
* Defines a Dispatcher that <does the long-running thing>.
25+
*/
26+
class <Area>ApplyDispatcher extends Dispatcher {
27+
public int $timeout = 300; # optional override
28+
public int $max_queue = 10; # optional override
29+
public string $schedule = ''; # 5-field cron string; set to register a CronJob
30+
protected array $required_packages = [];
31+
protected array $package_includes = [];
32+
33+
protected function _process(mixed ...$arguments): void {
34+
# Do the actual work here. This runs in a forked PHP process.
35+
}
36+
}
37+
```
38+
39+
## Conventions
40+
41+
- Class name ends in `Dispatcher` (e.g. `FirewallApplyDispatcher`, `DNSResolverApplyDispatcher`, `RESTAPISettingsSyncDispatcher`).
42+
- Implement `_process(mixed ...$arguments): void`. Never expose work via a public method other than the inherited `process()`/`spawn_process()`.
43+
- Honor `$this->async` when the underlying pfSense function offers both sync and async forms (e.g. `filter_configure()` vs `filter_configure_sync()`).
44+
- For Dispatchers that depend on a pfSense package, list it in `$required_packages` (full `pfSense-pkg-...` name) and any required PHP includes in `$package_includes`. Missing dependencies throw `FailedDependencyError`.
45+
- Use `$this->schedule = '*/5 * * * *';` to register a `CronJob` automatically (handled by `manage.php` at install time).
46+
47+
## Triggering from a Model
48+
49+
```php
50+
public function apply(): void {
51+
(new <Area>ApplyDispatcher(async: $this->async))->spawn_process();
52+
}
53+
```
54+
55+
`spawn_process()` returns a PID, writes a PID file under `/tmp/`, and respects `$max_queue` (overflow throws `ServiceUnavailableError` with `DISPATCHER_TOO_MANY_QUEUED_PROCESSES`).
56+
57+
## Hard rules
58+
59+
- **Never** call `filter_configure*()`, `services_*_configure()`, or other long-running pfSense functions directly from a Model — always wrap them in a Dispatcher.
60+
- **Never** use bare `exec()` / `shell_exec()` / `passthru()`. Use `RESTAPI\Core\Command`.
61+
- **Never** rely on `_process()` running synchronously in tests unless you instantiate with `async: false`.
62+
- The PID file lifecycle is owned by `Core/Dispatcher.inc`. Do not write your own locking.
63+
64+
## Manual invocation (debugging)
65+
66+
On the pfSense host:
67+
68+
```bash
69+
pfsense-restapi notifydispatcher <DispatcherClassShortName>
70+
```
71+
72+
This runs the dispatcher synchronously, queues behind any existing instance, and writes `/tmp/<DispatcherName>.lock` while running.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
applyTo: "pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/**/*.inc"
3+
---
4+
5+
# Endpoints — `RESTAPI\Endpoints\*`
6+
7+
Endpoints are **thin URL → Model adapters**. Most are 10–25 lines.
8+
9+
For full guidance and worked examples see:
10+
11+
- `.github/skills/endpoint-model-field.md`
12+
- `docs/BUILDING_CUSTOM_ENDPOINT_CLASSES.md`
13+
14+
## Required shape
15+
16+
```php
17+
<?php
18+
19+
namespace RESTAPI\Endpoints;
20+
21+
require_once 'RESTAPI/autoloader.inc';
22+
23+
use RESTAPI\Core\Endpoint;
24+
25+
class <Area><Resource>Endpoint extends Endpoint {
26+
public function __construct() {
27+
$this->url = '/api/v2/<area>/<resource>';
28+
$this->model_name = '<ModelClassName>';
29+
$this->request_method_options = [/* GET, POST, PATCH, PUT, DELETE */];
30+
# Optional: $this->many, $this->*_help_text, $this->auth_methods, ...
31+
parent::__construct();
32+
}
33+
}
34+
```
35+
36+
## Naming and URLs
37+
38+
- Singular endpoint: `<Area><Resource>Endpoint``/api/v2/<area>/<resource>`.
39+
- Collection endpoint (`many: true`): pluralize, e.g. `FirewallAliasesEndpoint``/api/v2/firewall/aliases`.
40+
- Apply endpoint: `<Area>ApplyEndpoint``/api/v2/<area>/apply`.
41+
- File name = class name + `.inc`.
42+
43+
## Method semantics (enforced in `Core/Endpoint.inc::check_construct`)
44+
45+
| `many` | Method | Model method called |
46+
| ------- | -------- | ------------------- |
47+
| `false` | `GET` | `read()` |
48+
| `false` | `POST` | `create()` |
49+
| `false` | `PATCH` | `update()` |
50+
| `false` | `DELETE` | `delete()` |
51+
| `true` | `GET` | `read_all()` |
52+
| `true` | `PUT` | `replace_all()` |
53+
| `true` | `DELETE` | `delete_many()` |
54+
55+
Setting `$this->many = true;` while `$model->many` is `false` throws `ENDPOINT_MANY_WITHOUT_MANY_MODEL`.
56+
57+
## Hard rules
58+
59+
- **Never** override `get()`, `post()`, `patch()`, `put()`, or `delete()`. There are zero such overrides in `Endpoints/` today.
60+
- **Never** put business logic in the Endpoint. All behavior belongs in the Model (or in a Dispatcher invoked from the Model).
61+
- **Never** hand-roll auth/ACL/privilege handling. Use `requires_auth`, `auth_methods`, `ignore_*` properties.
62+
- **Never** hard-code privilege names. They are derived from `$url` + method automatically.
63+
- **Never** edit the generated PHP files in the pfSense webroot. They are produced by `manage.php buildendpoints`.
64+
65+
## Optional Endpoint properties
66+
67+
Set when needed, leave default otherwise: `requires_auth`, `auth_methods`, `ignore_read_only`, `ignore_interfaces`, `ignore_enabled`, `ignore_acl`, `tag` (overrides default OpenAPI tag derived from URL), `*_help_text`, `deprecated`, `limit`, `offset`, `sort_by`, `sort_order`, `sort_flags`, `encode_content_handlers`, `decode_content_handlers`, `resource_link_set`. See `Core/Endpoint.inc` PHPDoc.
68+
69+
## Help text
70+
71+
Set `$this->get_help_text`, `$this->post_help_text`, `$this->patch_help_text`, `$this->put_help_text`, `$this->delete_help_text` only when the auto-generated default (built from the Model's `verbose_name`/`verbose_name_plural`) is misleading.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
applyTo: "pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/**/*.inc"
3+
---
4+
5+
# Models — `RESTAPI\Models\*`
6+
7+
Models hold the actual feature behavior of the package. Endpoints are thin adapters; everything that _does_ something belongs here.
8+
9+
For full guidance and worked examples see:
10+
11+
- `.github/skills/endpoint-model-field.md`
12+
- `.github/skills/anatomy-of-a-feature.md`
13+
- `docs/BUILDING_CUSTOM_MODEL_CLASSES.md`
14+
15+
## Constructor signature is fixed
16+
17+
```php
18+
public function __construct(
19+
mixed $id = null,
20+
mixed $parent_id = null,
21+
mixed $data = [],
22+
mixed ...$options,
23+
) {
24+
# 1. Set Model attributes (config_path, many, subsystem, ...)
25+
# 2. Define Field objects on $this
26+
# 3. parent::__construct(...) MUST be the last statement
27+
parent::__construct($id, $parent_id, $data, ...$options);
28+
}
29+
```
30+
31+
Order matters. Field objects must exist before the parent constructor runs.
32+
33+
## Naming
34+
35+
- Class: PascalCase, singular noun. `FirewallAlias`, `DNSResolverHostOverrideAlias`, `SystemHostname`.
36+
- File: `<ClassName>.inc`.
37+
- One class per file.
38+
39+
## Schema lives on Fields
40+
41+
Define every property as a typed `Field` (`StringField`, `IntegerField`, `BooleanField`, `ForeignModelField`, `NestedModelField`, etc.). Use Field constructor arguments for `required`, `default`, `choices`, `unique`, `sensitive`, `read_only`, `write_only`, `representation_only`, `many`, `delimiter`, `internal_name`, `internal_namespace`, `conditions`, `validators`, `verbose_name`, `help_text`. Do not validate inside the constructor — let Fields and Validators do it.
42+
43+
## Validation
44+
45+
| Need | Mechanism |
46+
| ---------------------------- | ---------------------------------------------------------------------- |
47+
| Reusable rule | A class in `RESTAPI/Validators/` attached via `validators: [...]`. |
48+
| Field-specific, not reusable | `validate_<field_name>($value): <type>` returning the validated value. |
49+
| Cross-field | `validate_extra(): void`. |
50+
51+
All three throw `RESTAPI\Responses\ValidationError` with a stable, namespaced `response_id`.
52+
53+
## `apply()` and Dispatchers
54+
55+
If the Model mutates a service (filter, DNS, services, certs, HA), implement:
56+
57+
```php
58+
public function apply(): void {
59+
(new <Area>ApplyDispatcher(async: $this->async))->spawn_process();
60+
}
61+
```
62+
63+
Set `$this->always_apply = true;` for singleton settings Models that should apply on every successful update.
64+
65+
Never call `filter_configure*()`, `services_*_configure()`, or other long-running pfSense functions directly from a Model — wrap them in a Dispatcher.
66+
67+
## Non-config-backed Models
68+
69+
If the Model does not store data in `$config`, set `$this->internal_callable = 'method_name';` and provide that method returning an array (single object, or list of arrays for `many` Models). Override `_create()` / `_update()` / `_delete()` only when needed.
70+
71+
## Hard rules
72+
73+
- No bare `exec()` / `shell_exec()` / `passthru()`. Use `RESTAPI\Core\Command`.
74+
- No pfSense Plus-only functions or paths.
75+
- Sensitive values must use `sensitive: true` on the Field.
76+
- Errors throw `RESTAPI\Responses\*` with a stable `UPPER_SNAKE_CASE` `response_id`.
77+
- Never edit generated files in the pfSense webroot — they are produced by `manage.php buildendpoints` from your Endpoint class.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
applyTo: "pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/**/*.inc"
3+
---
4+
5+
# pfSense-pkg-RESTAPI — global PHP rules
6+
7+
These rules apply to **every** PHP `.inc` file under `pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/`. Path-specific instructions in sibling files extend these for `Models/`, `Endpoints/`, `Validators/`, `Dispatchers/`, and `Tests/`.
8+
9+
Authoritative reference: `AGENTS.md` and `.github/skills/`.
10+
11+
## File preamble (required, in this order)
12+
13+
```php
14+
<?php
15+
16+
namespace RESTAPI\<Subnamespace>;
17+
18+
require_once 'RESTAPI/autoloader.inc';
19+
20+
use RESTAPI\Core\<...>;
21+
```
22+
23+
- File extension is **`.inc`** (not `.php`).
24+
- File name matches the class name exactly (`SystemHostname.inc``class SystemHostname`).
25+
- Namespace mirrors the directory: `RESTAPI\Models`, `RESTAPI\Endpoints`, `RESTAPI\Validators`, `RESTAPI\Dispatchers`, `RESTAPI\Tests`, etc.
26+
27+
## Conventions
28+
29+
- **PHP 8.2.** Use named arguments at call sites — the codebase consistently does this (`new StringField(required: true, default: '', ...)`).
30+
- **Comments:** `#` for short inline notes (matches existing code). Use `/** ... */` PHPDoc on every public class and method — PHPDoc rendering is gated by CI.
31+
- **`response_id` values:** `UPPER_SNAKE_CASE`, prefixed with the class/feature (e.g. `INVALID_FIREWALL_ALIAS_NAME`, `IP_ADDRESS_VALIDATOR_FAILED`).
32+
- **Throw, don't return errors.** Use a `RESTAPI\Responses\*` class with a stable `response_id`. Never throw bare `\Exception`.
33+
- **Shell execution:** use `RESTAPI\Core\Command`, never bare `exec()` / `shell_exec()` / `passthru()`.
34+
- **Sensitive values:** mark Fields `sensitive: true`. Never log secrets.
35+
36+
## pfSense CE only
37+
38+
Never reference symbols, files, or behaviors that exist only in pfSense Plus. If you cannot find it in pfSense CE source, do not use it.
39+
40+
## Formatting
41+
42+
Code must pass:
43+
44+
```bash
45+
./node_modules/.bin/prettier --check ./pfSense-pkg-RESTAPI/files
46+
phplint -vvv --no-cache
47+
./phpdoc
48+
```
49+
50+
CI enforces all three.

0 commit comments

Comments
 (0)