From e2bd4af4f26802e09ad305c93a66b2b88b846e81 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 11 Jan 2025 13:48:48 -0700 Subject: [PATCH 01/22] style: run prettier on changed files --- composer.lock | 290 ++++++++++++++++++++++++-------------------------- 1 file changed, 140 insertions(+), 150 deletions(-) diff --git a/composer.lock b/composer.lock index 34381120d..b6c70b121 100644 --- a/composer.lock +++ b/composer.lock @@ -1,156 +1,146 @@ { - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "df390555a5bc256768abe12103f30b54", - "packages": [ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "df390555a5bc256768abe12103f30b54", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "shasum": "" + }, + "require": { + "php": "^7.4||^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["BSD-3-Clause"], + "authors": [ { - "name": "firebase/php-jwt", - "version": "v6.10.0", - "source": { - "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", - "shasum": "" - }, - "require": { - "php": "^7.4||^8.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" - }, - "suggest": { - "ext-sodium": "Support EdDSA (Ed25519) signatures", - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" - }, - "type": "library", - "autoload": { - "psr-4": { - "Firebase\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Neuman Vong", - "email": "neuman+pear@twilio.com", - "role": "Developer" - }, - { - "name": "Anant Narayanan", - "email": "anant@php.net", - "role": "Developer" - } - ], - "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", - "keywords": [ - "jwt", - "php" - ], - "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" - }, - "time": "2023-12-01T16:26:39+00:00" + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" }, { - "name": "webonyx/graphql-php", - "version": "v15.19.1", - "source": { - "type": "git", - "url": "https://github.com/webonyx/graphql-php.git", - "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/fa01712b1a170ddc1d92047011b2f4c2bdfa8234", - "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^7.4 || ^8" - }, - "require-dev": { - "amphp/amp": "^2.6", - "amphp/http-server": "^2.1", - "dms/phpunit-arraysubset-asserts": "dev-master", - "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "3.65.0", - "mll-lab/php-cs-fixer-config": "^5.9.2", - "nyholm/psr7": "^1.5", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "1.12.12", - "phpstan/phpstan-phpunit": "1.4.1", - "phpstan/phpstan-strict-rules": "1.6.1", - "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", - "psr/http-message": "^1 || ^2", - "react/http": "^1.6", - "react/promise": "^2.0 || ^3.0", - "rector/rector": "^1.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5 || ^6 || ^7", - "thecodingmachine/safe": "^1.3 || ^2" - }, - "suggest": { - "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", - "psr/http-message": "To use standard GraphQL server", - "react/promise": "To leverage async resolving on React PHP platform" - }, - "type": "library", - "autoload": { - "psr-4": { - "GraphQL\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP port of GraphQL reference implementation", - "homepage": "https://github.com/webonyx/graphql-php", - "keywords": [ - "api", - "graphql" - ], - "support": { - "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v15.19.1" - }, - "funding": [ - { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" - } - ], - "time": "2024-12-19T10:52:18+00:00" + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": ["jwt", "php"], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + }, + "time": "2023-12-01T16:26:39+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v15.19.1", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/fa01712b1a170ddc1d92047011b2f4c2bdfa8234", + "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8" + }, + "require-dev": { + "amphp/amp": "^2.6", + "amphp/http-server": "^2.1", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.65.0", + "mll-lab/php-cs-fixer-config": "^5.9.2", + "nyholm/psr7": "^1.5", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "1.12.12", + "phpstan/phpstan-phpunit": "1.4.1", + "phpstan/phpstan-strict-rules": "1.6.1", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^1.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7", + "thecodingmachine/safe": "^1.3 || ^2" + }, + "suggest": { + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": ["api", "graphql"], + "support": { + "issues": "https://github.com/webonyx/graphql-php/issues", + "source": "https://github.com/webonyx/graphql-php/tree/v15.19.1" + }, + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.6.0" + ], + "time": "2024-12-19T10:52:18+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" } From 21188aacd4cf9c8825acebca69d74b841f36eba2 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 11 Jan 2025 13:51:09 -0700 Subject: [PATCH 02/22] docs: add info about unavailability during updates #633 --- .../local/pkg/RESTAPI/Forms/SystemRESTAPIUpdatesForm.inc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPIUpdatesForm.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPIUpdatesForm.inc index 4acc4bb61..8d99a84b7 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPIUpdatesForm.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPIUpdatesForm.inc @@ -31,8 +31,10 @@ class SystemRESTAPIUpdatesForm extends Form { public function on_save(string $success_banner_msg = ''): void { parent::on_save( - success_banner_msg: 'The requested version is being installed in the background. Check this page again ' . - 'later to see the status.', + success_banner_msg: 'The requested version is being installed in the background. During the update, the ' . + "REST API may be intermittently unavailable. Attempts to access the REST API's endpoints and web " . + 'pages during the update may result in errors until it completes. Check this page again later to see ' . + 'the status.', ); } } From c181ed6137f380e329912a19b78af88ae895b2ad Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 11 Jan 2025 15:11:58 -0700 Subject: [PATCH 03/22] fix: give friendlier error messages during updates #633 Before, access REST API resources resulted in a PHP error traceback which could cause unnecessary concern. This change will allow both endpoints and forms to give a friendlier, more informative notice. --- .../usr/local/pkg/RESTAPI/Core/Endpoint.inc | 11 ++++++++++- .../files/usr/local/pkg/RESTAPI/Core/Form.inc | 17 +++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index 86e7f93d5..14d7aa934 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -1199,9 +1199,18 @@ class Endpoint { $nq_class_name = $this->get_class_shortname(); # Specify the PHP code to write to the Endpoints index.php file + $unavailable_error = new ServiceUnavailableError( + message: 'This resource is either not installed or is currently updating. Please try again later.', + response_id: 'ENDPOINT_UNAVAILABLE', + ); + $unavailable_error_json = json_encode($unavailable_error->to_representation()); $code = "process_request();\n" . "header('Referer: no-referrer');\n" . "session_destroy();\n" . diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc index c80ed75ba..c72ae9e45 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc @@ -529,14 +529,15 @@ class Form { # Specify the PHP code to write to the Endpoints index.php file $code = - "print_form();'; + "Service Unavailable';\n" . + " echo 'This resource is either not installed or is currently updating. Please try again later.';\n" . + " exit(503);\n" . + "}\n" . + "(new $fq_class_name())->print_form();\n"; # Assign the absolute path to the file. Assume index.php filename if not specified. $filename = "/usr/local/www/$this->url"; From eac2c032dfcfff4c5a1f466eff393c1452804543 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 11 Jan 2025 15:22:20 -0700 Subject: [PATCH 04/22] fix: correctly set http response code for 503s during updates --- .../files/usr/local/pkg/RESTAPI/Core/Endpoint.inc | 2 ++ pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index 14d7aa934..6b6e26557 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -1208,6 +1208,8 @@ class Endpoint { "Service Unavailable';\n" . " echo 'This resource is either not installed or is currently updating. Please try again later.';\n" . - " exit(503);\n" . + " exit();\n" . "}\n" . "(new $fq_class_name())->print_form();\n"; @@ -549,7 +550,6 @@ class Form { if (is_file($filename)) { return true; } - return false; } } From 1f12c1fd41cd50d564a474d70dc18c10535528f9 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 6 Feb 2025 20:21:41 -0700 Subject: [PATCH 05/22] chore: increase OpenVPN local_network's maximum items to 10000 #644 --- .../files/usr/local/pkg/RESTAPI/Models/OpenVPNServer.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNServer.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNServer.inc index d66183361..46bbd6a94 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNServer.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNServer.inc @@ -325,6 +325,7 @@ class OpenVPNServer extends Model { default: [], allow_empty: true, many: true, + many_maximum: 10000, conditions: ['gwredir' => false], validators: [new SubnetValidator(allow_ipv4: true, allow_ipv6: false, allow_alias: true)], help_text: 'The IPv4 networks that will be accessible from the remote endpoint. Expressed as a ' . From cf114b7fc788ddb560be4f39a9e1202211224a65 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 6 Feb 2025 20:57:34 -0700 Subject: [PATCH 06/22] fix: correct whitespace in IntegerField error --- .../files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc index 79f8ab30e..cfe04cb85 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc @@ -142,8 +142,8 @@ class IntegerField extends Field { # Otherwise, the internal value cannot be represented by this Field. Throw an error. throw new RESTAPI\Responses\ServerError( - message: "Cannot parse IntegerField '$this->name' from internal because its internal value is not - a numeric value. Consider changing this field to a StringField.", + message: "Cannot parse IntegerField '$this->name' from internal because its internal value is not a " . + "numeric value. Consider changing this field to a StringField.", response_id: 'INTEGER_FIELD_WITH_NON_INTEGER_INTERNAL_VALUE', ); } From b7c69cbcacec13a83dacd506fbc4059f74588d57 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 6 Feb 2025 21:17:59 -0700 Subject: [PATCH 07/22] feat: allow endpoints, models and clients to specify sort flags #646 --- .../usr/local/pkg/RESTAPI/Core/Endpoint.inc | 52 ++++++++++++++++++- .../usr/local/pkg/RESTAPI/Core/Model.inc | 23 +++++++- .../usr/local/pkg/RESTAPI/Core/ModelSet.inc | 10 +++- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index 6b6e26557..bfde8c404 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -316,7 +316,7 @@ class Endpoint { public bool $reverse = false; /** - * @var string|null $sort_by + * @var string|array|null $sort_by * Sets the default value(s) for the `sort_by` field in the request data. This value is used to control the sorting of * the Model objects returned by this Endpoint. This value can be overridden by the client in the request data. Use * caution when assigning this value as it may force objects to be sorted in this order when they are written to the @@ -333,6 +333,15 @@ class Endpoint { */ public string $sort_order = 'SORT_ASC'; + /** + * @var string $sort_flags + * Sets the default value for the `sort_flags` field in the request data. This value is used to control the sorting + * flags of the Model objects returned by this Endpoint. This value can be overridden by the client in the request + * data. This value only takes effect when the `sort_by` field is also set. This value must be the name of a valid + * PHP sort flags constant (e.g. 'SORT_REGULAR', 'SORT_NATURAL') + */ + public string $sort_flags = 'SORT_REGULAR'; + /** * @var bool $append * Sets the default value for the `append` field in the request data. This value is used to control how Model objects @@ -854,6 +863,43 @@ class Endpoint { } } + /** + * Validates the $sort_flags common control parameter for this request. + * @note If the `sort_flags` field was not provided in the request data, the request will default to the + * Endpoint's assigned $sort_flags property value. + * @throws ValidationError When the `sort_flags` field is not a string or is not a valid sort flags constant. + */ + private function validate_sort_flags(): void + { + # Valid sort_flags options + $flag_opts = [ + 'SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL', 'SORT_FLAG_CASE' + ]; + + # Only validate this field if the client specifically requested it in the request data + if (isset($this->request_data['sort_flags'])) { + # Ensure value is a string + if (!is_string($this->request_data['sort_flags'])) { + throw new ValidationError( + message: 'Field `sort_flags` must be of type `string`.', + response_id: 'ENDPOINT_SORT_FLAGS_FIELD_INVALID_TYPE', + ); + } + + # Ensure the field is a valid sort flags constant + if (!in_array($this->request_data['sort_flags'], $flag_opts)) { + throw new ValidationError( + message: 'Field `sort_flags` must be one of: ' . json_encode($flag_opts), + response_id: 'ENDPOINT_SORT_FLAGS_FIELD_UNKNOWN_SORT_FLAGS', + ); + } + + # Update the sort_flags property to use the client's requested value and remove it from the request data + $this->sort_flags = $this->request_data['sort_flags']; + unset($this->request_data['sort_flags']); + } + } + /** * Validates the $append common control parameter for this request. * @note If the `append` field was not provided in the request data, the request will default to the Endpoint's assigned @@ -964,6 +1010,7 @@ class Endpoint { $this->validate_reverse(); $this->validate_sort_by(); $this->validate_sort_order(); + $this->validate_sort_flags(); $this->validate_append(); $this->validate_remove(); $this->validate_limit(); @@ -1082,6 +1129,7 @@ class Endpoint { reverse: $this->reverse, sort_by: $this->sort_by, sort_order: constant($this->sort_order), + sort_flags: constant($this->sort_flags), ); } # For GET requests on many Endpoints, obtain all objects from the assigned Model. @@ -1105,6 +1153,7 @@ class Endpoint { # Allow the endpoint/client to override the Model's sort_by and sort_order properties $this->model->sort_by = $this->sort_by ?? $this->model->sort_by; $this->model->sort_order = constant($this->sort_order) ?? $this->model->sort_order; + $this->model->sort_flags = constant($this->sort_flags) ?? $this->model->sort_flags; # Create the object and return it return $this->model->create(apply: $this->request_data['apply'] === true); @@ -1128,6 +1177,7 @@ class Endpoint { # Allow the endpoint/client to override the Model's sort_by and sort_order properties $this->model->sort_by = $this->sort_by ?? $this->model->sort_by; $this->model->sort_order = constant($this->sort_order) ?? $this->model->sort_order; + $this->model->sort_flags = constant($this->sort_flags) ?? $this->model->sort_flags; # Update the object and return it return $this->model->update( diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index c8c4caf3f..760a6ba92 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -222,6 +222,15 @@ class Model { */ public int|null $sort_order = null; + /** + * @var int|null $sort_flags + * For $many enabled Models, this property can be used to set the PHP sort flags used when writing Model objects to + * config. This property is only applicable if the $sort_by_field property is also defined. This property only applies + * to Models with a $config_path defined. For valid value options for this property, refer to: + * https://www.php.net/manual/en/function.array-multisort.php + */ + public int|null $sort_flags = null; + /** * @var array|null $sort_by * Sets the field names this Model will use when sorting objects written to the pfSense configuration. These fields must @@ -1703,7 +1712,12 @@ class Model { } # Obtain all Model objects for this Model and sort them by the requested criteria - $modelset = $this->query(parent_id: $this->parent_id, sort_by: $this->sort_by, sort_order: $this->sort_order); + $modelset = $this->query( + parent_id: $this->parent_id, + sort_by: $this->sort_by, + sort_order: $this->sort_order, + sort_flags: $this->sort_flags + ); # Loop through the sorted object and assign it's internal value $internal_objects = []; @@ -1915,6 +1929,7 @@ class Model { bool $reverse = false, ?array $sort_by = null, int $sort_order = SORT_ASC, + int $sort_flags = SORT_REGULAR, ...$vl_query_params, ): ModelSet { # Merge the $query_params and any provided variable-length arguments into a single variable @@ -1929,7 +1944,11 @@ class Model { $modelset = self::read_all(parent_id: $parent_id)->query(query_params: $query_params, excluded: $excluded); # Sort the set if a sort field was provided - $modelset = $sort_by ? $modelset->sort(fields: $sort_by, order: $sort_order, retain_ids: true) : $modelset; + if ($sort_by) { + $modelset = $modelset->sort( + fields: $sort_by, order: $sort_order, flags: $sort_flags, retain_ids: true + ); + } # Apply pagination to limit the number of objects returned and/or reverse the order if requested $modelset->model_objects = self::paginate($modelset->model_objects, $limit, $offset); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc index ff824c07e..ee0a9c9a7 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc @@ -83,10 +83,16 @@ class ModelSet { * Sorts the Model objects in this ModelSet by a specific field and order. * @param string|array $fields The field(s) to sort the Model objects by. * @param int $order The order to sort the Model objects by. This must be a PHP sort order constant. + * @param int $flags The flags to use when sorting the Model objects. This must be a PHP sort flags constant. * @param bool $retain_ids Retain the original Model object IDs when sorting. * @return ModelSet A new ModelSet object with the Model objects sorted by the specified field and order. */ - public function sort(string|array $fields, int $order = SORT_ASC, bool $retain_ids = false): ModelSet { + public function sort( + string|array $fields, + int $order = SORT_ASC, + int $flags = SORT_REGULAR, + bool $retain_ids = false + ): ModelSet { # Variables $model_objects = $this->model_objects; $fields = is_array($fields) ? $fields : [$fields]; @@ -107,7 +113,7 @@ class ModelSet { } # Sort using array_multisort - array_multisort($sort_criteria, $order, $model_objects); + array_multisort($sort_criteria, $order, $flags, $model_objects); # Re-assign the model object IDs if they should not be retained if (!$retain_ids) { From 8045dcfe78e45140cc0ac6ddc255da5052365377 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 6 Feb 2025 21:32:44 -0700 Subject: [PATCH 08/22] docs: add docs for sort_flags #646 --- docs/COMMON_CONTROL_PARAMETERS.md | 15 +++++++++++++++ docs/QUERIES_FILTERS_AND_SORTING.md | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/COMMON_CONTROL_PARAMETERS.md b/docs/COMMON_CONTROL_PARAMETERS.md index e88efce96..823f67a2a 100644 --- a/docs/COMMON_CONTROL_PARAMETERS.md +++ b/docs/COMMON_CONTROL_PARAMETERS.md @@ -102,6 +102,21 @@ parameters you can use: behavior of this parameter varies based on the request method and endpoint type. Refer to the [Sorting](QUERIES_FILTERS_AND_SORTING.md#sorting) section for more information. +## sort_flags + +- Type: String +- Default: `SORT_REGULAR` +- Choices: + - `SORT_REGULAR` + - `SORT_NUMERIC` + - `SORT_STRING` + - `SORT_NATURAL` + - `SORT_FLAG_CASE` + - `SORT_FLAG_LOCALE_STRING` +- Description: This parameter allows you to control the sorting behavior of the objects related to the endpoint. The + behavior of this parameter varies based on the request method and endpoint type. Refer to the + [Sorting](QUERIES_FILTERS_AND_SORTING.md#sorting) section for more information. + ## sort_order - Type: String diff --git a/docs/QUERIES_FILTERS_AND_SORTING.md b/docs/QUERIES_FILTERS_AND_SORTING.md index 1f9944aa4..a98a63dc2 100644 --- a/docs/QUERIES_FILTERS_AND_SORTING.md +++ b/docs/QUERIES_FILTERS_AND_SORTING.md @@ -122,8 +122,8 @@ For advanced users, the REST API's framework allows for custom query filter clas ## Sorting Sorting can be used to order the data that is returned from the API based on specific criteria, as well as sorting the -objects written to the pfSense configuration. Sorting is controlled by two common control parameters: -[`sort_by`](COMMON_CONTROL_PARAMETERS.md#sort_by) and [`sort_order`](COMMON_CONTROL_PARAMETERS.md#sort_order). +objects written to the pfSense configuration. Sorting is controlled by three common control parameters: +[`sort_by`](COMMON_CONTROL_PARAMETERS.md#sort_by), [`sort_flags`](COMMON_CONTROL_PARAMETERS.md#sort_flags), and [`sort_order`](COMMON_CONTROL_PARAMETERS.md#sort_order). !!! Note - Sorting is only available for model objects that allow many instances, meaning multiple objects of its type can From a596b906cbae236ac9e71c5bfb5a54ec573c8ba0 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 6 Feb 2025 21:44:18 -0700 Subject: [PATCH 09/22] docs(oas): add sort_flags to OpenAPI schema #646 --- .../pkg/RESTAPI/Schemas/OpenAPISchema.inc | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc index 4a67f54b5..937e065b5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc @@ -336,13 +336,30 @@ class OpenAPISchema extends Schema { [ 'in' => 'query', 'name' => 'sort_order', - 'description' => 'Sort the response data in descending order.', + 'description' => 'The order to sort response data by.', 'schema' => [ 'type' => 'string', 'enum' => ['SORT_ASC', 'SORT_DESC'], 'nullable' => true, ], ], + [ + 'in' => 'query', + 'name' => 'sort_flags', + 'description' => 'The sort flag to use to customize the behavior of the sort.', + 'schema' => [ + 'type' => 'string', + 'nullable' => true, + 'enum' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE' + ] + ], + ], [ 'in' => 'query', 'name' => 'query', From 621e70c7048bef68a51027b10248fe12eaed5781 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 14:52:27 -0700 Subject: [PATCH 10/22] fix(endpoint): default to empty array if request body is empty --- .../files/usr/local/pkg/RESTAPI/Core/Endpoint.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index bfde8c404..0a7494a30 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -743,7 +743,7 @@ class Endpoint { $this->check_decode_content_handler_supported($content_handler); # Decode the request data - $this->request_data = $content_handler->decode(); + $this->request_data = $content_handler->decode() ?? []; $this->validate_endpoint_fields(); } From 89bd428191d8aace879070e80aeb3d07804cde42 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 14:53:21 -0700 Subject: [PATCH 11/22] fix(alias): correct error message for invalid network alias address --- .../files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc index b626884b5..13fff9622 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc @@ -120,7 +120,7 @@ class FirewallAlias extends Model { $network_alias_q = $aliases->query(name: $address, type: 'network'); if ($type === 'network' and !is_subnet($address) and !is_fqdn($address) and !$network_alias_q->exists()) { throw new ValidationError( - message: "Host alias 'address' value '$address' is not a valid CIDR, FQDN, or alias.", + message: "Network alias 'address' value '$address' is not a valid CIDR, FQDN, or alias.", response_id: 'INVALID_NETWORK_ALIAS_ADDRESS', ); } From 9c37bf21ba88d752010c9e6a3631d8eeb2e680dc Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 15:00:38 -0700 Subject: [PATCH 12/22] fix: don't serialize routes before they exist #654 --- .../files/usr/local/pkg/RESTAPI/Models/StaticRoute.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/StaticRoute.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/StaticRoute.inc index e8cd13505..2b4dbbc06 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/StaticRoute.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/StaticRoute.inc @@ -221,6 +221,7 @@ class StaticRoute extends Model { */ private function serialize_route(): void { global $g; + $old_network = []; # Include pending route changes that haven't been applied yet, or simply initialize our pending change var if (file_exists("{$g['tmp_path']}/.system_routes.apply")) { From 97323ed592f6a40e39c5c4a31ff8f30941b89708 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 15:07:23 -0700 Subject: [PATCH 13/22] tests: ensure we can update non-applied static routes #654 --- .../Tests/APIModelsStaticRouteTestCase.inc | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc index 2a9c9c5d8..f1669c5c5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc @@ -341,4 +341,34 @@ class APIModelsStaticRouteTestCase extends TestCase { # Delete the gateway used for testing $test_gw->delete(apply: true); } + + /** + * Checks that we can update a newly created static route that has not yet been applied. Regression test for #654. + */ + public function test_update_static_route_before_its_applied(): void { + # Create a gateway to use for testing + $test_gw = new RoutingGateway( + name: 'TESTGW', + interface: 'lan', + ipprotocol: 'inet', + gateway: '192.168.1.10', + async: false, + ); + $test_gw->create(); + + # Create a static route using the gateway above, but do not apply it! + $test_route = new StaticRoute( + gateway: $test_gw->name->value, + network: '1.2.3.4/32' + ); + $test_route->create(); + + # Ensure we can update the static route before applying it + $test_route->network->value = '4.3.2.1/32'; + $test_route->update(); + + # Remove the static route and gateway + $test_route->delete(); + $test_gw->delete(apply: true); + } } From 1e35c1b107abd100fa3b75fd1a5ca176f4ca930a Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 19:00:20 -0700 Subject: [PATCH 14/22] style: run prettier on changed files --- composer.lock | 290 +++++++++--------- .../usr/local/pkg/RESTAPI/Core/Endpoint.inc | 10 +- .../usr/local/pkg/RESTAPI/Core/Model.inc | 6 +- .../usr/local/pkg/RESTAPI/Core/ModelSet.inc | 2 +- .../local/pkg/RESTAPI/Fields/IntegerField.inc | 2 +- .../pkg/RESTAPI/Schemas/OpenAPISchema.inc | 4 +- .../Tests/APIModelsStaticRouteTestCase.inc | 5 +- 7 files changed, 154 insertions(+), 165 deletions(-) diff --git a/composer.lock b/composer.lock index 3212d3e8e..6bcbe8621 100644 --- a/composer.lock +++ b/composer.lock @@ -1,156 +1,146 @@ { - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "a32ab4a8fc071e68a251a9446caf15b9", - "packages": [ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a32ab4a8fc071e68a251a9446caf15b9", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.11.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712", + "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["BSD-3-Clause"], + "authors": [ { - "name": "firebase/php-jwt", - "version": "v6.11.0", - "source": { - "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712", - "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^7.4", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "psr/cache": "^2.0||^3.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" - }, - "suggest": { - "ext-sodium": "Support EdDSA (Ed25519) signatures", - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" - }, - "type": "library", - "autoload": { - "psr-4": { - "Firebase\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Neuman Vong", - "email": "neuman+pear@twilio.com", - "role": "Developer" - }, - { - "name": "Anant Narayanan", - "email": "anant@php.net", - "role": "Developer" - } - ], - "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", - "keywords": [ - "jwt", - "php" - ], - "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.0" - }, - "time": "2025-01-23T05:11:06+00:00" + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" }, { - "name": "webonyx/graphql-php", - "version": "v15.19.1", - "source": { - "type": "git", - "url": "https://github.com/webonyx/graphql-php.git", - "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/fa01712b1a170ddc1d92047011b2f4c2bdfa8234", - "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^7.4 || ^8" - }, - "require-dev": { - "amphp/amp": "^2.6", - "amphp/http-server": "^2.1", - "dms/phpunit-arraysubset-asserts": "dev-master", - "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "3.65.0", - "mll-lab/php-cs-fixer-config": "^5.9.2", - "nyholm/psr7": "^1.5", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "1.12.12", - "phpstan/phpstan-phpunit": "1.4.1", - "phpstan/phpstan-strict-rules": "1.6.1", - "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", - "psr/http-message": "^1 || ^2", - "react/http": "^1.6", - "react/promise": "^2.0 || ^3.0", - "rector/rector": "^1.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5 || ^6 || ^7", - "thecodingmachine/safe": "^1.3 || ^2" - }, - "suggest": { - "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", - "psr/http-message": "To use standard GraphQL server", - "react/promise": "To leverage async resolving on React PHP platform" - }, - "type": "library", - "autoload": { - "psr-4": { - "GraphQL\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP port of GraphQL reference implementation", - "homepage": "https://github.com/webonyx/graphql-php", - "keywords": [ - "api", - "graphql" - ], - "support": { - "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v15.19.1" - }, - "funding": [ - { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" - } - ], - "time": "2024-12-19T10:52:18+00:00" + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": ["jwt", "php"], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.0" + }, + "time": "2025-01-23T05:11:06+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v15.19.1", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/fa01712b1a170ddc1d92047011b2f4c2bdfa8234", + "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8" + }, + "require-dev": { + "amphp/amp": "^2.6", + "amphp/http-server": "^2.1", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.65.0", + "mll-lab/php-cs-fixer-config": "^5.9.2", + "nyholm/psr7": "^1.5", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "1.12.12", + "phpstan/phpstan-phpunit": "1.4.1", + "phpstan/phpstan-strict-rules": "1.6.1", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^1.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7", + "thecodingmachine/safe": "^1.3 || ^2" + }, + "suggest": { + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": ["api", "graphql"], + "support": { + "issues": "https://github.com/webonyx/graphql-php/issues", + "source": "https://github.com/webonyx/graphql-php/tree/v15.19.1" + }, + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.3.0" + ], + "time": "2024-12-19T10:52:18+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index 0a7494a30..90ceb727f 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -869,11 +869,15 @@ class Endpoint { * Endpoint's assigned $sort_flags property value. * @throws ValidationError When the `sort_flags` field is not a string or is not a valid sort flags constant. */ - private function validate_sort_flags(): void - { + private function validate_sort_flags(): void { # Valid sort_flags options $flag_opts = [ - 'SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL', 'SORT_FLAG_CASE' + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', ]; # Only validate this field if the client specifically requested it in the request data diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index 760a6ba92..752516685 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -1716,7 +1716,7 @@ class Model { parent_id: $this->parent_id, sort_by: $this->sort_by, sort_order: $this->sort_order, - sort_flags: $this->sort_flags + sort_flags: $this->sort_flags, ); # Loop through the sorted object and assign it's internal value @@ -1945,9 +1945,7 @@ class Model { # Sort the set if a sort field was provided if ($sort_by) { - $modelset = $modelset->sort( - fields: $sort_by, order: $sort_order, flags: $sort_flags, retain_ids: true - ); + $modelset = $modelset->sort(fields: $sort_by, order: $sort_order, flags: $sort_flags, retain_ids: true); } # Apply pagination to limit the number of objects returned and/or reverse the order if requested diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc index ee0a9c9a7..9bb29025a 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc @@ -91,7 +91,7 @@ class ModelSet { string|array $fields, int $order = SORT_ASC, int $flags = SORT_REGULAR, - bool $retain_ids = false + bool $retain_ids = false, ): ModelSet { # Variables $model_objects = $this->model_objects; diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc index cfe04cb85..be02ee039 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/IntegerField.inc @@ -143,7 +143,7 @@ class IntegerField extends Field { # Otherwise, the internal value cannot be represented by this Field. Throw an error. throw new RESTAPI\Responses\ServerError( message: "Cannot parse IntegerField '$this->name' from internal because its internal value is not a " . - "numeric value. Consider changing this field to a StringField.", + 'numeric value. Consider changing this field to a StringField.', response_id: 'INTEGER_FIELD_WITH_NON_INTEGER_INTERNAL_VALUE', ); } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc index 937e065b5..cb06afe2a 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc @@ -356,8 +356,8 @@ class OpenAPISchema extends Schema { 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL', - 'SORT_FLAG_CASE' - ] + 'SORT_FLAG_CASE', + ], ], ], [ diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc index f1669c5c5..5914d1b7b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsStaticRouteTestCase.inc @@ -357,10 +357,7 @@ class APIModelsStaticRouteTestCase extends TestCase { $test_gw->create(); # Create a static route using the gateway above, but do not apply it! - $test_route = new StaticRoute( - gateway: $test_gw->name->value, - network: '1.2.3.4/32' - ); + $test_route = new StaticRoute(gateway: $test_gw->name->value, network: '1.2.3.4/32'); $test_route->create(); # Ensure we can update the static route before applying it From d8e68bcc90cba4302b32bc74903054cfa92e9193 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 19:57:36 -0700 Subject: [PATCH 15/22] fix: adjust what criteria is required for sorting --- .../files/usr/local/pkg/RESTAPI/Core/Model.inc | 12 ++++++------ .../usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index 752516685..921ff495d 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -214,22 +214,22 @@ class Model { public int|null $placement = null; /** - * @var int|null $sort_order + * @var int $sort_order * For $many enabled Models, this property can be used to set the PHP sort option used when writing Model objects to * config. This property is only applicable if the $sort_by_field property is also defined. This property only applies * to Models with a $config_path defined. For valid value options for this property, refer to: * https://www.php.net/manual/en/function.array-multisort.php */ - public int|null $sort_order = null; + public int $sort_order = SORT_ASC; /** - * @var int|null $sort_flags + * @var int $sort_flags * For $many enabled Models, this property can be used to set the PHP sort flags used when writing Model objects to * config. This property is only applicable if the $sort_by_field property is also defined. This property only applies * to Models with a $config_path defined. For valid value options for this property, refer to: * https://www.php.net/manual/en/function.array-multisort.php */ - public int|null $sort_flags = null; + public int $sort_flags = SORT_REGULAR; /** * @var array|null $sort_by @@ -1698,8 +1698,8 @@ class Model { * internal objects must be written in a specific order. */ protected function sort(): void { - # Do not sort if there is no `sort_order` or `sort_by` set - if (!$this->sort_order or !$this->sort_by) { + # Do not sort if there is no `sort_by` fields set + if (!$this->sort_by) { return; } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc index 7ef312e2f..2662cbcb9 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc @@ -216,7 +216,7 @@ class GraphQLSchema extends Schema { foreach ($model->get_fields() as $field) { # For NestedModelFields, ensure we pass the Type for the nested Model if ($model->$field instanceof NestedModelField) { - $nested_model_type = $this->get_model_object_type($model->$field->model_class); + $nested_model_type = $this->get_model_input_object_type($model->$field->model_class); $fields[$field]['type'] = $this->field_to_type(field: $model->$field, type: $nested_model_type); $fields[$field]['description'] = $model->$field->help_text; continue; From 8f716fc65dd256561eaf85ced983ff1a1c3411b1 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 21:27:52 -0700 Subject: [PATCH 16/22] fix(graphql): ensure correct object types are set in schema #623 --- .../files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc index 2662cbcb9..ae76bc2d7 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc @@ -216,7 +216,12 @@ class GraphQLSchema extends Schema { foreach ($model->get_fields() as $field) { # For NestedModelFields, ensure we pass the Type for the nested Model if ($model->$field instanceof NestedModelField) { - $nested_model_type = $this->get_model_input_object_type($model->$field->model_class); + if ($input) { + $nested_model_type = $this->get_model_input_object_type($model->$field->model_class); + } + else { + $nested_model_type = $this->get_model_object_type($model->$field->model_class); + } $fields[$field]['type'] = $this->field_to_type(field: $model->$field, type: $nested_model_type); $fields[$field]['description'] = $model->$field->help_text; continue; From 4d2079c940504177d6becad095b25179aabe339f Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 21:29:17 -0700 Subject: [PATCH 17/22] style: run prettier on changed files --- .../files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc index ae76bc2d7..10b9d9e64 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/GraphQLSchema.inc @@ -218,8 +218,7 @@ class GraphQLSchema extends Schema { if ($model->$field instanceof NestedModelField) { if ($input) { $nested_model_type = $this->get_model_input_object_type($model->$field->model_class); - } - else { + } else { $nested_model_type = $this->get_model_object_type($model->$field->model_class); } $fields[$field]['type'] = $this->field_to_type(field: $model->$field, type: $nested_model_type); From 05267ace03da7d7296df941e7f5ee2109a231315 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 10 Feb 2025 22:34:23 -0700 Subject: [PATCH 18/22] ci: sync ntp before running tests --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01bcfd468..aa49a528c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -155,6 +155,8 @@ jobs: run: | pfsense-vshell --host ${{ matrix.PFSENSE_VERSION }}.jaredhendrickson.com -u admin -p pfsense -c 'pfSsh.php playback enablesshd' -k pfsense-vshell --host ${{ matrix.PFSENSE_VERSION }}.jaredhendrickson.com -u admin -p pfsense -c "mkdir /root/.ssh/ && echo $(cat ~/.ssh/id_rsa.pub) > /root/.ssh/authorized_keys" -k + pfsense-vshell --host ${{ matrix.PFSENSE_VERSION }}.jaredhendrickson.com -u admin -p pfsense -c "pkill ntpd" -k + pfsense-vshell --host ${{ matrix.PFSENSE_VERSION }}.jaredhendrickson.com -u admin -p pfsense -c "ntpdate pool.ntp.org" -k scp -o StrictHostKeyChecking=no pfSense-pkg-RESTAPI-${{ env.BUILD_VERSION }}-${{ matrix.FREEBSD_ID }}.pkg/pfSense-pkg-RESTAPI-${{ env.BUILD_VERSION }}-${{ matrix.FREEBSD_ID }}.pkg admin@${{ matrix.PFSENSE_VERSION }}.jaredhendrickson.com:/tmp/ - name: Install pfSense-pkg-RESTAPI on pfSense From 98368a55f68cb8a442105542a60f565ac5783865 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Tue, 11 Feb 2025 18:57:41 -0700 Subject: [PATCH 19/22] fix: adjust sorting defaults --- .../files/usr/local/pkg/RESTAPI/Core/Endpoint.inc | 8 ++++---- .../local/pkg/RESTAPI/Models/DHCPServerStaticMapping.inc | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index 90ceb727f..f5d032a01 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -334,13 +334,13 @@ class Endpoint { public string $sort_order = 'SORT_ASC'; /** - * @var string $sort_flags + * @var string|null $sort_flags * Sets the default value for the `sort_flags` field in the request data. This value is used to control the sorting * flags of the Model objects returned by this Endpoint. This value can be overridden by the client in the request * data. This value only takes effect when the `sort_by` field is also set. This value must be the name of a valid * PHP sort flags constant (e.g. 'SORT_REGULAR', 'SORT_NATURAL') */ - public string $sort_flags = 'SORT_REGULAR'; + public string|null $sort_flags = null; /** * @var bool $append @@ -1157,7 +1157,7 @@ class Endpoint { # Allow the endpoint/client to override the Model's sort_by and sort_order properties $this->model->sort_by = $this->sort_by ?? $this->model->sort_by; $this->model->sort_order = constant($this->sort_order) ?? $this->model->sort_order; - $this->model->sort_flags = constant($this->sort_flags) ?? $this->model->sort_flags; + $this->model->sort_flags = $this->sort_flags ? constant($this->sort_flags) : $this->model->sort_flags; # Create the object and return it return $this->model->create(apply: $this->request_data['apply'] === true); @@ -1181,7 +1181,7 @@ class Endpoint { # Allow the endpoint/client to override the Model's sort_by and sort_order properties $this->model->sort_by = $this->sort_by ?? $this->model->sort_by; $this->model->sort_order = constant($this->sort_order) ?? $this->model->sort_order; - $this->model->sort_flags = constant($this->sort_flags) ?? $this->model->sort_flags; + $this->model->sort_flags = $this->sort_flags ? constant($this->sort_flags) : $this->model->sort_flags; # Update the object and return it return $this->model->update( diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServerStaticMapping.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServerStaticMapping.inc index 5967a61d0..224eed996 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServerStaticMapping.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServerStaticMapping.inc @@ -42,6 +42,7 @@ class DHCPServerStaticMapping extends Model { $this->subsystem = 'dhcpd'; $this->many = true; $this->sort_by = ['ipaddr']; + $this->sort_flags = SORT_NATURAL; # Define model Fields $this->mac = new StringField( From 4c58c825985d14f92ba61029c1b6871f1cb7b014 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Tue, 11 Feb 2025 21:27:13 -0700 Subject: [PATCH 20/22] fix: fixed sorting by multiple fields --- .../usr/local/pkg/RESTAPI/Core/Endpoint.inc | 2 +- .../usr/local/pkg/RESTAPI/Core/ModelSet.inc | 37 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index f5d032a01..ccbc0482d 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -1133,7 +1133,7 @@ class Endpoint { reverse: $this->reverse, sort_by: $this->sort_by, sort_order: constant($this->sort_order), - sort_flags: constant($this->sort_flags), + sort_flags: $this->sort_flags ? constant($this->sort_flags) : $this->model->sort_flags, ); } # For GET requests on many Endpoints, obtain all objects from the assigned Model. diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc index 9bb29025a..d40c03459 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc @@ -93,36 +93,33 @@ class ModelSet { int $flags = SORT_REGULAR, bool $retain_ids = false, ): ModelSet { - # Variables - $model_objects = $this->model_objects; + # Ensure fields is an array $fields = is_array($fields) ? $fields : [$fields]; - $sort_criteria = []; - - # Loop through each Model object and add the field values to the sort criteria - foreach ($this->model_objects as $model_object) { - # Loop variables - $sort_values = []; - # Loop through each field to sort by and extract it's value - foreach ($fields as $field) { - $sort_values[] = $field === 'id' ? $model_object->id : $model_object->$field->value; - } - - # Add the sort values to the sort criteria - $sort_criteria[] = $sort_values; + # Extract sorting values for each field + $sort_criteria = []; + foreach ($fields as $field) { + $sort_criteria[] = array_map( + fn($model) => $field === 'id' ? $model->id : $model->$field->value, $this->model_objects + ); + $sort_criteria[] = $order; + $sort_criteria[] = $flags; } - # Sort using array_multisort - array_multisort($sort_criteria, $order, $flags, $model_objects); + # Append the actual model objects array for sorting + $sort_criteria[] = &$this->model_objects; + + # Sort the array + array_multisort(...$sort_criteria); - # Re-assign the model object IDs if they should not be retained + # Re-assign model object IDs if they should not be retained if (!$retain_ids) { - foreach ($model_objects as $id => $model_object) { + foreach ($this->model_objects as $id => $model_object) { $model_object->id = $id; } } - return new ModelSet($model_objects); + return new ModelSet($this->model_objects); } /** From 5e282ecafc2a9c067e91c3e86742aa2c92dbc81a Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Tue, 11 Feb 2025 21:27:59 -0700 Subject: [PATCH 21/22] style: run prettier on changed files --- .../files/usr/local/pkg/RESTAPI/Core/ModelSet.inc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc index d40c03459..5a7e572c8 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/ModelSet.inc @@ -100,7 +100,8 @@ class ModelSet { $sort_criteria = []; foreach ($fields as $field) { $sort_criteria[] = array_map( - fn($model) => $field === 'id' ? $model->id : $model->$field->value, $this->model_objects + fn($model) => $field === 'id' ? $model->id : $model->$field->value, + $this->model_objects, ); $sort_criteria[] = $order; $sort_criteria[] = $flags; From 34cf82d8fe105dd2f7a7f7d3a19dfebb40396303 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Wed, 12 Feb 2025 19:34:01 -0700 Subject: [PATCH 22/22] tests: add tests for sort flags #646 --- .../RESTAPI/Tests/APICoreEndpointTestCase.inc | 73 +++++++++++++++++++ .../RESTAPI/Tests/APICoreModelSetTestCase.inc | 32 ++++++++ 2 files changed, 105 insertions(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreEndpointTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreEndpointTestCase.inc index d502b13cc..33dde5703 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreEndpointTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreEndpointTestCase.inc @@ -810,4 +810,77 @@ class APICoreEndpointTestCase extends TestCase { $this->assert_equals($resp->response_id, 'ENDPOINT_SORT_ORDER_FIELD_UNKNOWN_SORT_ORDER'); $this->assert_equals($resp->code, 400); } + + /** + * Ensure the 'sort_flags' common control parameter must be a valid PHP sort constant. + */ + public function test_sort_flags_must_be_php_sort_constant(): void { + # Use a GET request to request a non-string sort order + $json_resp = \RESTAPI\Core\Tools\http_request( + url: 'https://localhost/api/v2/firewall/aliases', + method: 'GET', + data: ['sort_by' => 'name', 'sort_flags' => 12345], + headers: ['Content-Type' => 'application/json'], + username: 'admin', + password: 'pfsense', + validate_certs: false, + ); + # Ensure the response threw a ENDPOINT_SORT_FLAGS_FIELD_INVALID_TYPE error + $resp = json_decode($json_resp); + $this->assert_equals($resp->response_id, 'ENDPOINT_SORT_FLAGS_FIELD_INVALID_TYPE'); + $this->assert_equals($resp->code, 400); + + # Make another request to sort order that is not known + $json_resp = \RESTAPI\Core\Tools\http_request( + url: 'https://localhost/api/v2/firewall/aliases', + method: 'GET', + data: ['sort_by' => 'name', 'sort_flags' => 'unknown_sort_flags'], + headers: ['Content-Type' => 'application/json'], + username: 'admin', + password: 'pfsense', + validate_certs: false, + ); + + # Ensure the response threw a ENDPOINT_SORT_FLAGS_FIELD_UNKNOWN_SORT_FLAGS error + $resp = json_decode($json_resp); + $this->assert_equals($resp->response_id, 'ENDPOINT_SORT_FLAGS_FIELD_UNKNOWN_SORT_FLAGS'); + $this->assert_equals($resp->code, 400); + } + + /** + * Ensure clients can specify the 'sort_flags' common control parameter to sort the returned data. + */ + public function test_sort_flags(): void { + # Use a PUT request to create new aliases + \RESTAPI\Core\Tools\http_request( + url: 'https://localhost/api/v2/firewall/aliases', + method: 'PUT', + data: [ + ['name' => 'alias_a', 'type' => 'host', 'descr' => '127.0.0.22'], + ['name' => 'alias_b', 'type' => 'host', 'descr' => '127.0.0.2'], + ['name' => 'alias_c', 'type' => 'host', 'descr' => '127.0.0.100'], + ['name' => 'alias_d', 'type' => 'host', 'descr' => '127.0.0.10'], + ], + headers: ['Content-Type' => 'application/json'], + username: 'admin', + password: 'pfsense', + validate_certs: false, + ); + + # Ensure we can make a successful GET request to a `many` endpoint with the 'sort_flags' control parameter + $json_resp = \RESTAPI\Core\Tools\http_request( + url: 'https://localhost/api/v2/firewall/aliases', + method: 'GET', + data: ['sort_by' => 'descr', 'sort_flags' => 'SORT_NATURAL'], + headers: ['Content-Type' => 'application/json'], + username: 'admin', + password: 'pfsense', + validate_certs: false, + ); + $resp = json_decode($json_resp); + $this->assert_equals($resp->data[0]->name, 'alias_b'); + $this->assert_equals($resp->data[1]->name, 'alias_d'); + $this->assert_equals($resp->data[2]->name, 'alias_a'); + $this->assert_equals($resp->data[3]->name, 'alias_c'); + } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelSetTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelSetTestCase.inc index c8389fc2d..b406f6c72 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelSetTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelSetTestCase.inc @@ -435,4 +435,36 @@ class APICoreModelSetTestCase extends RESTAPI\Core\TestCase { $host_9, ]); } + + /** + * Checks that ModelSets can be sorted using the requested sort flags + */ + public function test_sort_flags(): void { + # Create test firewall aliases with descriptions set to IP addresses + $alias_a = new FirewallAlias(name: 'alias_a', type: 'host', descr: '127.0.0.22'); + $alias_b = new FirewallAlias(name: 'alias_b', type: 'host', descr: '127.0.0.200'); + $alias_c = new FirewallAlias(name: 'alias_c', type: 'host', descr: '127.0.0.100'); + $alias_a->create(); + $alias_b->create(); + $alias_c->create(); + + # Read the firewall aliases and use regular sorting to ensure the order is correct + # This should sort the IP address out of logical order since SORT_REGULAR does not sort numerically + $modelset = FirewallAlias::read_all()->sort(['descr'], order: SORT_ASC, flags: SORT_REGULAR); + $this->assert_equals($modelset->model_objects[0]->name->value, 'alias_c'); + $this->assert_equals($modelset->model_objects[1]->name->value, 'alias_b'); + $this->assert_equals($modelset->model_objects[2]->name->value, 'alias_a'); + + # Read the firewall aliases and use natural sorting to ensure the order is correct + # This should sort the IP address in numerical order since SORT_NATURAL does sort numerically + $modelset = FirewallAlias::read_all()->sort(['descr'], order: SORT_ASC, flags: SORT_NATURAL); + $this->assert_equals($modelset->model_objects[0]->name->value, 'alias_a'); + $this->assert_equals($modelset->model_objects[1]->name->value, 'alias_c'); + $this->assert_equals($modelset->model_objects[2]->name->value, 'alias_b'); + + # Delete the firewall aliases + $alias_a->delete(); + $alias_b->delete(); + $alias_c->delete(); + } }