Merge branch 'fabianfrz-contraints'

pull/217/head
Ad Schellevis 5 years ago
commit f4244fb80b

@ -10,203 +10,14 @@ by creating a clear abstraction layer.
In this chapter we will explain how models are designed and build.
-------------------
Designing the model
-------------------
Creating models for OPNsense is divided into two separate blocks:
.. toctree::
:maxdepth: 2
:titlesonly:
:glob:
#. A PHP class describing the actions on our data (also acts as a
wrapper to our data),
#. The definition of the data and the rules it should apply to.
Every model's class should be derived from OPNsense\\Base\\BaseModel, a very
simple model without any (additional) logic is defined with:
.. code-block:: php
<?php
namespace myVendorName\myModule;
 
use OPNsense\Base\BaseModel;
 
class myModel extends BaseModel
{
}
This class should be placed inside the model directory of our project, in this
case the full path for our class file would be
- /usr/local/opnsense/mvc/app/models/**myVendorName/myModule/myModel.php**
When you design a model, the next thing to do is to figure out what data is
relevant for your application or module and think of the rules it should comply
to (for example, if you need an email address you might want to validate the
input). Designing the actual model is as simple as creating an xml file and
putting in your structure, the name of our XML file should be the same as the
base name of our model suffixed by .xml.
Using the same model, we would create the following file:
- /usr/local/opnsense/mvc/app/models/**myVendorName/myModule/myModel.xml**
And start describing our (information) model, like this:
.. code-block:: xml
<model>
<mount>//myManufacturer/myModule</mount>
<description>A description of this model (metadata)</description>
<items>
<exampleNumber type="TextField">
<Mask>/^([0-9]){0,1}$/</Mask>
<Default>5</Default>
<ValidationMessage>you should input a number from 0 to 9</ValidationMessage>
</exampleNumber>
<contacts>
<entity type="ArrayField">
<email type="EmailField">
<ValidationMessage>you should input a valid email address!</ValidationMessage>
</email>
<name type="TextField"/>
</entity>
<someText type="TextField"/>
</contacts>
</items>
</model>
Now let's explain what's happing here one tag at a time.
#. the <model> tag is used for identification of the file. (this is a
model file)
#. Next in line is the <mount> tag, this tells the system where this
information lives in the configuration file, in this case
ROOT\_tag/myManufacturer/myModule
#. If desired, there is some space reserved to explain the usage of the
model, the <description> tag
#. Last item on the top of our list is the <items> tag, this is where
the magic begins.
The content of a items tag describes the full tree based structure which holds
our data, in theory this could be as large as you want it to be, but keep in
mind that the content for your model should be logical and understandable. Every
node in the tree could have a type, which defines its behavior, nodes without a
type are just containers.
From top to bottom we find the following nodes in our tree:
- exampleNumber, defined as a TextField
- Mask, validation can be performed by a regex expression, this sets
the expression
- Default, this field is default filled with a number: 5
- ValidationMessage, when validation fails, this message is returned
- contacts, this is a container
- entity, is defined as a recurring item, which holds the next items
- email, defined as an EmailField
- when validation fails, the **ValidationMessage** is returned
- name, defined as a TextField without any validations
- someText, not part of the entity tag and also defined as text without
validation
The fieldtypes are easily extendable in the base system and all common ones live in
their own namespace at *OPNsense\\Base\\FieldTypes* deriving from *BaseField*.
.. Note::
When designing application specific fieldtypes, you can point to a field
type within the application namespace using a full or partial path.
For example using *Vendor\\Component\\FieldTypes\\MyFieldType* to point to a specific non
common field type or *.\\MyFieldType* when linked from the application model itself (which assumes a namespace FieldTypes
exists)
-------------
Usage example
-------------
Now let's test our model using a small PHP script (in /usr/local/opnsense/mvc/script/ ):
.. code-block:: php
<?php
// initialize phalcon components for our script
require_once("load_phalcon.php");
 
// include myModel and the shared config component
use myVendorName\myModule\myModel;
use OPNsense\Core\Config;
 
// create a new model, reading the model definition and the current data from our config.xml
$myMdl = new myModel();
$myMdl->exampleNumber =1;
$myMdl->contacts->someText = "just a test";
 
// add a new contact node
$node = $myMdl->contacts->entity->add();
$node->email = "test@test.com";
$node->name = "my test user";
 
// perform validation on the data in our model
$validationMessages = $myMdl->performValidation();
foreach ($validationMessages as $messsage) {
echo "validation failure on field ". $messsage->getField()." returning message : ". $messsage->getMessage()."\n";
}
 
// if validation succeeded, write data back to config
if ($validationMessages->count() == 0) {
// serialize our model to the config file (config.xml)
// (this raises an error on validation failures)
$myMdl->serializeToConfig();
$cnf = Config::getInstance();
$cnf->save();
}
If you fill in an invalid value to one of the validated fields, you can easily
try the validation. Try to input the text "X" into the field exampleNumber to try out.
When inspecting our config.xml file, you will notice the following content has
been added to the root:
.. code-block:: xml
<myManufacturer>
<myModule>
<exampleNumber>1</exampleNumber>
<contacts>
<entity>
<email>test@test.com</email>
<name>my test user</name>
</entity>
<someText>just a test</someText>
</contacts>
</myModule>
</myManufacturer>
----------
Guidelines
----------
.. rubric:: Some (simple) guidelines developing models
:name: some-simple-guidelines-developing-models
#. One model should always be completely responsible for the its mount
point, so if there's a model at mount point /A/B there can't be a
model at /A/B/C
#. Try to keep models logical and understandable, it's better to build
two models for you application if the content of two parts aren't
related to each other. It's no issue to create models at deeper
levels of the structure.
#. When using more models in a application/module, you might want to
consider the following naming convention: /Vendor/Module/Model
#. Try to avoid more disc i/o actions than necessary, only call save()
if you actually want to save content, serializeToConfig just keeps
the data in memory.
models_design
models_example
models_guidelines
models_fieldtypes
models_constraints

@ -0,0 +1,144 @@
==================================
Adding constraints
==================================
Constraints can be used on top of type validations provided by the base fields described in the previous chapter.
The base class `BaseConstraint` is used as base type and contains shared functionality.
Since fields are usually only validated when there's change detected, it's usually necessary to add
back references to chain validations.
DependConstraint
-------------------------
When choices depend on each other, you can use the `DependConstraint` type.
The following example makes `sslurlonly` depend on the `sslbump` option, since the `<reference>`
tag is needed to make sure when one of the settings changes, the other is validated again too.
.. code-block:: xml
<sslbump type="BooleanField">
<Constraints>
<check001>
<type>DependConstraint</type>
<addFields>
<field1>sslurlonly</field1>
</addFields>
</check001>
</Constraints>
</sslbump>
<sslurlonly type="BooleanField">
<Constraints>
<check001>
<reference>sslbump.check001</reference>
</check001>
</Constraints>
</sslurlonly>
AllOrNoneConstraint
-------------------------
Make sure that options are only valid when selected in combination.
SingleSelectConstraint
-------------------------
When options are not valid in combination, you can hook options together so only one of the
options can be selected at the same time.
UniqueConstraint
-------------------------
This constraint helps to make sure all items within an `ArrayField` are unique, such as the following example
which makes sure all "filenames" in this set are unique.
.. code-block:: xml
<filename type="TextField">
<Constraints>
<check001>
<type>UniqueConstraint</type>
</check001>
</Constraints>
</filename>
ComparedToFieldConstraint
-------------------------
The ComparedToFieldConstraint compares the value of the current field to the value of a referenced field
on the same level (for example in the same ArrayField).
It is for example used for the rspamd_ plugin, to ensure that some values are in the correct order.
This constraint can be used to compare numeric values.
.. _rspamd: https://github.com/opnsense/plugins/blob/master/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml
Example:
::
<check003>
<ValidationMessage>This field must be bigger than the greylist score.</ValidationMessage>
<type>ComparedToFieldConstraint</type>
<field>greylistscore</field>
<operator>gt</operator>
</check003>
In this example, the valueof the current field must be greater than the value of the field "greylistscore".
Field values:
================= ====================================================================================
ValidationMessage Validation message (translateable) which will be shown in case the validation fails.
type ComparedToFieldConstraint
field the other field which we reference
operator The operator to check the value. Valid operators are gt, gte, lt, lte, eq, neq
================= ====================================================================================
Operators:
=== =====================
gt greater than
gte greater than or equal
lt lesser than
lte lesser than or equal
eq equal
neq not equal
=== =====================
SetIfConstraint
------------------------------------
The SetIfConstraint is used to make some fields conditionally mandatory. It is mainly used in the nginx_
plugin for example to choose an implementation type. In general the other field should be an OptionField,
but does not need to. In general it is a good idea to hide or show fields which are (not)
required by an implementation in the frontend as well to simplify the web interface.
Please note: the checked field is intended to be on the same level (for example ArrayField).
.. _nginx: https://github.com/opnsense/plugins/blob/master/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml
Example:
::
<check001>
<ValidationMessage>This field must be set.</ValidationMessage>
<type>SetIfConstraint</type>
<field>match_type</field>
<check>id</check>
</check001>
In this example, the value will be mandatory, if the field "match_type" has the value "id".
Field Values:
================= ====================================================================================
ValidationMessage Validation message (translateable) which will be shown in case the validation fails.
type SetIfConstraint
field the other field which we reference
check The value of the other field which makes this field required
================= ====================================================================================

@ -0,0 +1,114 @@
-------------------
Designing the model
-------------------
Creating models for OPNsense is divided into two separate blocks:
#. A PHP class describing the actions on our data (also acts as a
wrapper to our data),
#. The definition of the data and the rules it should apply to.
Every model's class should be derived from OPNsense\\Base\\BaseModel, a very
simple model without any (additional) logic is defined with:
.. code-block:: php
<?php
namespace myVendorName\myModule;
 
use OPNsense\Base\BaseModel;
 
class myModel extends BaseModel
{
}
This class should be placed inside the model directory of our project, in this
case the full path for our class file would be
- /usr/local/opnsense/mvc/app/models/**myVendorName/myModule/myModel.php**
When you design a model, the next thing to do is to figure out what data is
relevant for your application or module and think of the rules it should comply
to (for example, if you need an email address you might want to validate the
input). Designing the actual model is as simple as creating an xml file and
putting in your structure, the name of our XML file should be the same as the
base name of our model suffixed by .xml.
Using the same model, we would create the following file:
- /usr/local/opnsense/mvc/app/models/**myVendorName/myModule/myModel.xml**
And start describing our (information) model, like this:
.. code-block:: xml
<model>
<mount>//myManufacturer/myModule</mount>
<description>A description of this model (metadata)</description>
<items>
<exampleNumber type="TextField">
<Mask>/^([0-9]){0,1}$/</Mask>
<Default>5</Default>
<ValidationMessage>you should input a number from 0 to 9</ValidationMessage>
</exampleNumber>
<contacts>
<entity type="ArrayField">
<email type="EmailField">
<ValidationMessage>you should input a valid email address!</ValidationMessage>
</email>
<name type="TextField"/>
</entity>
<someText type="TextField"/>
</contacts>
</items>
</model>
Now let's explain what's happing here one tag at a time.
#. the <model> tag is used for identification of the file. (this is a
model file)
#. Next in line is the <mount> tag, this tells the system where this
information lives in the configuration file, in this case
ROOT\_tag/myManufacturer/myModule
#. If desired, there is some space reserved to explain the usage of the
model, the <description> tag
#. Last item on the top of our list is the <items> tag, this is where
the magic begins.
The content of a items tag describes the full tree based structure which holds
our data, in theory this could be as large as you want it to be, but keep in
mind that the content for your model should be logical and understandable. Every
node in the tree could have a type, which defines its behavior, nodes without a
type are just containers.
From top to bottom we find the following nodes in our tree:
- exampleNumber, defined as a TextField
- Mask, validation can be performed by a regex expression, this sets
the expression
- Default, this field is default filled with a number: 5
- ValidationMessage, when validation fails, this message is returned
- contacts, this is a container
- entity, is defined as a recurring item, which holds the next items
- email, defined as an EmailField
- when validation fails, the **ValidationMessage** is returned
- name, defined as a TextField without any validations
- someText, not part of the entity tag and also defined as text without
validation
The fieldtypes are easily extendable in the base system and all common ones live in
their own namespace at *OPNsense\\Base\\FieldTypes* deriving from *BaseField*.
.. Note::
When designing application specific fieldtypes, you can point to a field
type within the application namespace using a full or partial path.
For example using *Vendor\\Component\\FieldTypes\\MyFieldType* to point to a specific non
common field type or *.\\MyFieldType* when linked from the application model itself (which assumes a namespace FieldTypes
exists)

@ -0,0 +1,62 @@
-------------
Usage example
-------------
Now let's test our model using a small PHP script (in /usr/local/opnsense/mvc/script/ ):
.. code-block:: php
<?php
// initialize phalcon components for our script
require_once("load_phalcon.php");
 
// include myModel and the shared config component
use myVendorName\myModule\myModel;
use OPNsense\Core\Config;
 
// create a new model, reading the model definition and the current data from our config.xml
$myMdl = new myModel();
$myMdl->exampleNumber =1;
$myMdl->contacts->someText = "just a test";
 
// add a new contact node
$node = $myMdl->contacts->entity->add();
$node->email = "test@test.com";
$node->name = "my test user";
 
// perform validation on the data in our model
$validationMessages = $myMdl->performValidation();
foreach ($validationMessages as $messsage) {
echo "validation failure on field ". $messsage->getField()." returning message : ". $messsage->getMessage()."\n";
}
 
// if validation succeeded, write data back to config
if ($validationMessages->count() == 0) {
// serialize our model to the config file (config.xml)
// (this raises an error on validation failures)
$myMdl->serializeToConfig();
$cnf = Config::getInstance();
$cnf->save();
}
If you fill in an invalid value to one of the validated fields, you can easily
try the validation. Try to input the text "X" into the field exampleNumber to try out.
When inspecting our config.xml file, you will notice the following content has
been added to the root:
.. code-block:: xml
<myManufacturer>
<myModule>
<exampleNumber>1</exampleNumber>
<contacts>
<entity>
<email>test@test.com</email>
<name>my test user</name>
</entity>
<someText>just a test</someText>
</contacts>
</myModule>
</myManufacturer>

@ -0,0 +1,194 @@
----------------------
Standard field types
----------------------
OPNsense comes with a collection of standard field types, which can be used to perform standard field type validations.
These field types can be found in `/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/ <https://github.com/opnsense/core/tree/master/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes>`__
and usually decent from the `BaseField` type.
This paragraph aims to provide an overview of the types included by default and their use.
.. Tip::
When using lists, the `Multiple` (Y/N) keyword defines if there may be more than one item selected at a time.
.. Tip::
The xml keyword `Required` can be used to mark a field as being required.
ArrayField
------------------------------------
The basic field type to describe a container of objects, such as a list of addresses.
.. Note::
This type can't be nested, only one level of ArrayField types is supported, you can use ModelRelationField to
describe master-detail constructions.
AuthGroupField
------------------------------------
Returns and validates system (user) groups (found in :menuselection:`System --> Access --> Groups`)
AuthenticationServerField
------------------------------------
Select and validate authentication providers, maintained in :menuselection:`System --> Access --> Servers`.
AutoNumberField
------------------------------------
An integer sequence, which automatically increments on every new item of the same type in the same level.
BooleanField
------------------------------------
Boolean field, where 0 means `false` and 1 is defined as `true`
CSVListField
------------------------------------
List of (comma) separated values, which can be validated using a regex.
CertificateField
------------------------------------
Option list with system certificates defined in :menuselection:`System --> Trust`, use the `Type` keyword to distinct between the
available options (`ca`, `crl`, `cert`), defaults to `cert`.
ConfigdActionsField
------------------------------------
Select available configd actions, supports filters to limit the number of choices. For example, the example below
only shows actions which have a description.
.. code-block:: xml
<command type="ConfigdActionsField">
<filters>
<description>/(.){1,255}/</description>
</filters>
</command>
CountryField
------------------------------------
Select and validate countries in the world.
EmailField
------------------------------------
Validate if the input contains an email address.
HostnameField
------------------------------------
Check if hostnames are valid (optionally allows IP addresses as well)
IntegerField
------------------------------------
Validate if the input contains an integere value, optionally constrained by minimum and maximum values.
InterfaceField
------------------------------------
Option list with interfaces defined in :menuselection:`Interfaces --> Assignments`, supports filters.
The example below shows a list of non-dhcp active interfaces, for which multiple items may be selected, but at least one
should be. It defaults to `lan`
.. code-block:: xml
<interfaces type="InterfaceField">
<Required>Y</Required>
<multiple>Y</multiple>
<default>lan</default>
<filters>
<enable>/^(?!0).*$/</enable>
<ipaddr>/^((?!dhcp).)*$/</ipaddr>
</filters>
</interfaces>
JsonKeyValueStoreField
------------------------------------
A construct to validate against a json dataset retreived via configd, such as
.. code-block:: xml
<program type="JsonKeyValueStoreField">
<ConfigdPopulateAct>syslog list applications</ConfigdPopulateAct>
<SourceFile>/tmp/syslog_applications.json</SourceFile>
<ConfigdPopulateTTL>20</ConfigdPopulateTTL>
<SortByValue>Y</SortByValue>
</program>
In which case `syslog list applications` is called to retrieved options, which is valid for 20 seconds (TTL) before fetching again.
ModelRelationField
------------------------------------
Define relations to other nodes in the model, such as to point the attribute `pipe` to a `pipe` node in the TrafficShaper model.
.. code-block:: xml
<pipe type="ModelRelationField">
<Model>
<pipes>
<source>OPNsense.TrafficShaper.TrafficShaper</source>
<items>pipes.pipe</items>
<display>description</display>
</pipes>
</Model>
</pipe>
NetworkField
------------------------------------
Validate if the value is a valid network address (IPv4, IPv6).
NumericField
------------------------------------
Validate input to be of numeric type.
OptionField
------------------------------------
Validate against a static list of options.
PortField
------------------------------------
Check if the input contains a valid portnumber or (optionally) predefined service name. Can be a range when
`EnableRanges` is set to `Y`.
TextField
------------------------------------
Validate regular text using a regex.
UniqueIdField
==================================
Generate unique id numbers.
UpdateOnlyTextField
------------------------------------
Write only text fields, can be used to store passwords
UrlField
------------------------------------
Validate if the input contains a valid URL.

@ -0,0 +1,21 @@
----------
Guidelines
----------
.. rubric:: Some (simple) guidelines developing models
:name: some-simple-guidelines-developing-models
#. One model should always be completely responsible for the its mount
point, so if there's a model at mount point /A/B there can't be a
model at /A/B/C
#. Try to keep models logical and understandable, it's better to build
two models for you application if the content of two parts aren't
related to each other. It's no issue to create models at deeper
levels of the structure.
#. When using more models in a application/module, you might want to
consider the following naming convention: /Vendor/Module/Model
#. Try to avoid more disc i/o actions than necessary, only call save()
if you actually want to save content, serializeToConfig just keeps
the data in memory.
Loading…
Cancel
Save