diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5faef96..0ba335f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,7 +121,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "20" + node-version: "26" - name: Install npm dependencies run: npm i diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 6358f03d..f0a19bab 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 20 + node-version: 26 - name: Install npm packages run: npm install - name: Check Prettier @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: psf/black@c6755bb741b6481d6b3d3bb563c83fa060db96c9 # 26.3.1 + - uses: psf/black@87928e6d6761a4a6d22250e1fee5601b3998086e # 26.5.1 lint_php: runs-on: ubuntu-latest @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: "Setup PHP ${{ matrix.PHP_VERSION }}" - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 with: php-version: "${{ matrix.PHP_VERSION }}" coverage: "none" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcf184b1..19098db9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,8 @@ jobs: PFSENSE_VERSION: "25.11.1" - FREEBSD_VERSION: FreeBSD-16.0-CURRENT PFSENSE_VERSION: "26.03" + - FREEBSD_VERSION: FreeBSD-16.0-CURRENT + PFSENSE_VERSION: "26.03.1" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -165,7 +167,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Pages - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Make website directory run: mkdir ./www diff --git a/.github/workflows/schedule_weekly.yml b/.github/workflows/schedule_weekly.yml index 4e643a72..7a2ad380 100644 --- a/.github/workflows/schedule_weekly.yml +++ b/.github/workflows/schedule_weekly.yml @@ -15,7 +15,7 @@ jobs: env: DAYS_BEFORE_STALE: 60 steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: stale-issue-message: | This issue has been automatically marked as stale because it has had no recent activity in the last ${{ env.DAYS_BEFORE_STALE }} days. diff --git a/README.md b/README.md index 8b902d1e..3687aa56 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ pkg-static add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/dow Install on pfSense Plus: ```bash -pkg-static -C /dev/null add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/download/pfSense-26.03-pkg-RESTAPI.pkg +pkg-static -C /dev/null add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/download/pfSense-26.03.1-pkg-RESTAPI.pkg ``` > [!WARNING] diff --git a/docs/INSTALL_AND_CONFIG.md b/docs/INSTALL_AND_CONFIG.md index 63fadf30..0c21b0f2 100644 --- a/docs/INSTALL_AND_CONFIG.md +++ b/docs/INSTALL_AND_CONFIG.md @@ -17,6 +17,8 @@ run pfSense. It's recommended to follow Netgate's [minimum hardware requirements - pfSense CE 2.8.1 - pfSense Plus 25.11.1 - pfSense Plus 26.03 +- pfSense Plus 26.03.1 + !!! Warning Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. diff --git a/docs/QUERIES_FILTERS_AND_SORTING.md b/docs/QUERIES_FILTERS_AND_SORTING.md index 5e85533d..3d7773bf 100644 --- a/docs/QUERIES_FILTERS_AND_SORTING.md +++ b/docs/QUERIES_FILTERS_AND_SORTING.md @@ -56,10 +56,11 @@ value array contains a given value for array fields. Search for objects whose field value is within a given array of values (when the filter value is an array), or search for objects whose field value is a substring of a given string (when the filter value is a string). + - Name: `in` - Examples: - - `https://pfsense.example.com/api/v2/examples?fieldname__in=example` - - `https://pfsense.example.com/api/v2/examples?fieldname__in[]=example1&fieldname__in[]=example2` + - `https://pfsense.example.com/api/v2/examples?fieldname__in=example` + - `https://pfsense.example.com/api/v2/examples?fieldname__in[]=example1&fieldname__in[]=example2` ### Less Than (lt) @@ -95,8 +96,8 @@ Search for objects whose field value matches a given format. - Name: `format` - Examples: - - `https://pfsense.example.com/api/v2/examples?fieldname__format=ipv4` - - `https://pfsense.example.com/api/v2/examples?fieldname__format=email` + - `https://pfsense.example.com/api/v2/examples?fieldname__format=ipv4` + - `https://pfsense.example.com/api/v2/examples?fieldname__format=email` #### Supported Formats diff --git a/package-lock.json b/package-lock.json index fdc00204..2b7ac456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "devDependencies": { "@prettier/plugin-php": "^0.25.0", - "@stoplight/spectral-cli": "^6.15.1", + "@stoplight/spectral-cli": "^6.16.0", "prettier": "^3.8.3" } }, @@ -241,9 +241,9 @@ } }, "node_modules/@stoplight/spectral-cli": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.15.1.tgz", - "integrity": "sha512-ev72bUglbaZvFSMWCP5o1Iso5NGgbLZOAuedvRxYrUMey9dVCR83i033tSFvDv6cpj86HsbEmiilh8vwrY/asQ==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.16.0.tgz", + "integrity": "sha512-P1acHIV/hDiO3w0YNUc3pD7/0q68SMAMyWVxAPUGzsAeq50lLpl0obN5j3QITMgJPhPByvBIjBV4ftkBd8nwMg==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index e0401f37..8eb7545f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,6 @@ "devDependencies": { "prettier": "^3.8.3", "@prettier/plugin-php": "^0.25.0", - "@stoplight/spectral-cli": "^6.15.1" + "@stoplight/spectral-cli": "^6.16.0" } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc index 7eddd76a..ca9b9277 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc @@ -32,7 +32,16 @@ class RESTAPIVersionReleasesCache extends Cache { ); $releases = json_decode($releases_json, true); - # Return the fetched releases if in a valid format, otherwise retain the existing cache - return is_array($releases) ? $releases : $this->read(); + # If we don't have an 'assets' key, the response is invalid. Log the response, keep the existing cache + if (!is_array($releases) or !array_key_exists(0, $releases) or !array_key_exists('assets', $releases[0])) { + Cache::log( + level: LOG_ERR, + message: "Releases data is malformed, received $releases_json. Keeping existing cache.", + ); + return $this->read(); + } + + # Return the fetched releases + return $releases; } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc index 5411d4a1..021e7591 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc @@ -3,6 +3,7 @@ namespace RESTAPI\Core; use RESTAPI\Models\CronJob; +use RESTAPI\Models\RESTAPISettings; use RESTAPI\Responses\FailedDependencyError; use RESTAPI\Responses\ServerError; use RESTAPI\Responses\ServiceUnavailableError; @@ -119,10 +120,18 @@ class Dispatcher { * @throws FailedDependencyError When a package requires a PHP include file that could not be found. */ private function check_required_packages(): void { + # Check if the user has opted in to using development (-devel) package variants + $pkg_config = RESTAPISettings::get_pkg_config(); + $allow_development_packages = ($pkg_config['allow_development_packages'] ?? '') === 'enabled'; + # Loop through each required package and ensure it is present on the system. foreach ($this->required_packages as $pkg) { - # Return an error if the package is not installed - if (!is_pkg_installed($pkg)) { + # When the development packages setting is enabled, also accept the -devel variant of the package + $devel_pkg = $pkg . '-devel'; + $pkg_installed = is_pkg_installed($pkg) || ($allow_development_packages && is_pkg_installed($devel_pkg)); + + # Return an error if neither the package nor its -devel variant (when allowed) is installed + if (!$pkg_installed) { throw new FailedDependencyError( message: "The requested action requires the '$pkg' package but it is not installed.", response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc index fcf38ce6..63a3b53b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc @@ -145,6 +145,18 @@ class WireGuardTunnel extends Model { return wg_is_key_clamped($privatekey) ? $privatekey : wg_clamp_key($privatekey); } + /** + * Extends the default _update method to ensure addresses are removed if the tunnel has an interface assignment + */ + public function _update(): void { + # Remove any existing addresses if this tunnel has an existing interface assignment + if (NetworkInterface::query(if: $this->name->value)->exists()) { + $this->addresses->value = []; + } + + parent::_update(); + } + /** * Obtains the next available WireGuard tunnel interface name. * @return string The next available WireGuard tunnel interface name (i.e. tun_wg0) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc index ceddaf44..52ea30d5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc @@ -6,6 +6,7 @@ use RESTAPI\Core\Dispatcher; use RESTAPI\Core\TestCase; use RESTAPI\Dispatchers\WireGuardApplyDispatcher; use RESTAPI\Models\Package; +use RESTAPI\Models\RESTAPISettings; class APICoreDispatcherTestCase extends TestCase { /** @@ -176,4 +177,100 @@ class APICoreDispatcherTestCase extends TestCase { # Remove the installed package $package->delete(); } + + /** + * Checks that a Dispatcher requiring a package that is not installed throws DISPATCHER_MISSING_REQUIRED_PACKAGE + * when the `allow_development_packages` setting is disabled (default). + */ + public function test_required_package_missing_throws_by_default(): void { + # Ensure the allow_development_packages setting is disabled + $settings = new RESTAPISettings(); + $settings->from_internal(); + $original = $settings->allow_development_packages->value; + + if ($original) { + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } + + # A Dispatcher requiring a package that is not installed should fail + $this->assert_throws_response( + response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', + code: 424, + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-some_package_we_dont_have']; + $dispatcher->process(); + }, + ); + } + + /** + * Checks that when `allow_development_packages` is enabled, a Dispatcher whose required package is not installed + * and whose -devel counterpart is also not installed still throws DISPATCHER_MISSING_REQUIRED_PACKAGE. Since we + * cannot install arbitrary packages in tests, we verify the inverse: a package whose -devel name also doesn't + * exist still fails even with the setting enabled. + */ + public function test_devel_variant_still_fails_when_neither_installed(): void { + # Enable the allow_development_packages setting + $settings = new RESTAPISettings(); + $settings->from_internal(); + $settings->allow_development_packages->value = true; + $settings->update(apply: false); + + # Even with the setting on, if neither the base package nor the -devel variant is installed, it should fail + $this->assert_throws_response( + response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', + code: 424, + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-some_package_we_dont_have']; + $dispatcher->process(); + }, + ); + + # Restore the setting to its original value + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } + + /** + * Checks that when `allow_development_packages` is enabled, a Dispatcher whose required base package is installed + * (using pfSense-pkg-RESTAPI as a proxy for the base package) still passes the required package check. + */ + public function test_base_package_accepted_when_devel_enabled(): void { + # Enable the allow_development_packages setting + $settings = new RESTAPISettings(); + $settings->from_internal(); + $settings->allow_development_packages->value = true; + $settings->update(apply: false); + + # The base package (pfSense-pkg-RESTAPI) is installed; the package check should still pass + $this->assert_does_not_throw( + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-RESTAPI']; + $dispatcher->process(); + }, + ); + + # Restore the setting to its original value + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } +} + +/** + * Defines a test-only Dispatcher used to exercise the $required_packages check without spawning a real background + * process. The $required_packages property is exposed publicly so individual tests can assign the package(s) to + * check, and _process() is intentionally a no-op so calling process() only runs the required package validation. + */ +class RequiredPackagesTestDispatcher extends Dispatcher { + public array $required_packages = []; + + /** + * Overrides the default _process() with a no-op so tests can validate the package check in isolation. + * @param mixed ...$arguments Unused arguments accepted to match the parent signature. + */ + protected function _process(mixed ...$arguments): void {} } diff --git a/requirements.txt b/requirements.txt index 138b0b14..96ee0a89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ jinja2~=3.1.6 pylint~=4.0.5 -black~=26.3.1 +black~=26.5.1 mkdocs~=1.6.1