diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/BaseConstraint.php b/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/BaseConstraint.php
new file mode 100644
index 000000000..ac7c64686
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/BaseConstraint.php
@@ -0,0 +1,54 @@
+getOption('message');
+ $name = $this->getOption('name');
+ if (empty($message)) {
+ $message = 'validation failure ' . get_class($this);
+ }
+ if (empty($name)) {
+ $name = get_class($this);
+ }
+ $validator->appendMessage(new Message($message, $attribute, $name));
+ }
+}
\ No newline at end of file
diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/UniqueConstraint.php b/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/UniqueConstraint.php
new file mode 100644
index 000000000..50062f75a
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/UniqueConstraint.php
@@ -0,0 +1,92 @@
+getOption('node');
+ $addFields = $this->getOption('addFields');
+ $fieldSeparator = chr(10) . chr(0);
+ if ($node) {
+ $containerNode = $node;
+ $nodeName = $node->getInternalXMLTagName();
+ $parentNode = $node->getParentNode();
+ $level = 0;
+ // dive into parent
+ while ($containerNode != null &&
+ get_class($containerNode) != 'OPNsense\Base\FieldTypes\ArrayField') {
+ $containerNode = $containerNode->getParentNode();
+ $level++;
+ }
+ if ($containerNode != null && $level == 2) {
+ // collect (additional) key fields
+ $keyFields = array($nodeName);
+ if (!empty($addFields)) {
+ foreach ($addFields as $field) {
+ $keyFields[] = $field;
+ }
+ }
+ // calculate the key for this node
+ $nodeKey = '';
+ foreach ($keyFields as $field) {
+ $nodeKey .= $fieldSeparator . $parentNode->$field;
+ }
+ // when an ArrayField is found in range, traverse nodes and compare keys
+ foreach ($containerNode->__items as $item) {
+ if ($item !== $parentNode) {
+ $itemKey = '';
+ foreach ($keyFields as $field) {
+ $itemKey .= $fieldSeparator . $item->$field;
+ }
+ if ($itemKey == $nodeKey) {
+ $this->appendMessage($validator, $attribute);
+ return false;
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/BaseField.php b/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/BaseField.php
index 5398b3376..f3ec820f1 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/BaseField.php
+++ b/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/BaseField.php
@@ -47,6 +47,11 @@ abstract class BaseField
*/
protected $internalChildnodes = array();
+ /**
+ * @var array constraints for this field, additional to fieldtype
+ */
+ protected $internalConstraints = array();
+
/**
* @var null pointer to parent
*/
@@ -211,6 +216,15 @@ abstract class BaseField
$this->internalParentNode = $node;
}
+ /**
+ * return this nodes parent (or null if not found)
+ * @return null|BaseField
+ */
+ public function getParentNode()
+ {
+ return $this->internalParentNode;
+ }
+
/**
* Reflect default getter to internal child nodes.
* Implements the special attribute __items to return all items and __reference to identify the field in this model.
@@ -347,13 +361,35 @@ abstract class BaseField
}
}
+ /**
+ * fetch all additional validators
+ */
+ private function getConstraintValidators()
+ {
+ $result = array();
+ foreach ($this->internalConstraints as $name => $constraint) {
+ if (!empty($constraint['type'])) {
+ try {
+ $constr_class = new \ReflectionClass($constraint['type']);
+ if ($constr_class->getParentClass()->name == 'OPNsense\Base\Constraints\BaseConstraint') {
+ $constraint['name'] = $name;
+ $constraint['node'] = $this;
+ $result[] = $constr_class->newInstance($constraint);
+ }
+ } catch (\ReflectionException $e) {
+ null; // ignore configuration errors, if the constraint can't be found, skip.
+ }
+ }
+ }
+ return $result;
+ }
/**
* return field validators for this field
* @return array returns validators for this field type (empty if none)
*/
public function getValidators()
{
- $validators = array();
+ $validators = $this->getConstraintValidators();
if ($this->isEmptyAndRequired()) {
$validators[] = new PresenceOf(array('message' => $this->internalValidationMessage)) ;
}
@@ -561,6 +597,15 @@ abstract class BaseField
}
}
+ /**
+ * set additional constraints
+ * @param $constraints
+ */
+ public function setConstraints($constraints)
+ {
+ $this->internalConstraints = $constraints;
+ }
+
/**
* apply change case to this node, called by setValue
*/
diff --git a/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModel/TestModel.xml b/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModel/TestModel.xml
index 8494e96cc..d0023c3e4 100644
--- a/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModel/TestModel.xml
+++ b/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModel/TestModel.xml
@@ -20,7 +20,18 @@
65535
not a valid number
Y
+
+
+ number should be unique
+ OPNsense\Base\Constraints\UniqueConstraint
+
+ optfield
+
+
+
+
+
diff --git a/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModelTest.php b/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModelTest.php
index cfbde9c37..3292b1811 100644
--- a/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModelTest.php
+++ b/src/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModelTest.php
@@ -234,4 +234,34 @@ class BaseModelTest extends \PHPUnit_Framework_TestCase
$data = BaseModelTest::$model->arraytypes->item->getNodes();
$this->assertEquals(count($data), 9);
}
+
+ /**
+ * @depends testGetNodes
+ * @expectedException \Phalcon\Validation\Exception
+ * @expectedExceptionMessage number should be unique
+ */
+ public function testConstraintsNok()
+ {
+ $count = 2;
+ foreach (BaseModelTest::$model->arraytypes->item->__items as $nodeid => $node) {
+ $count-- ;
+ if ($count >= 0) {
+ $node->number = 999;
+ }
+ }
+ BaseModelTest::$model->serializeToConfig();
+ }
+
+ /**
+ * @depends testConstraintsNok
+ */
+ public function testConstraintsOk()
+ {
+ $count = 1;
+ foreach (BaseModelTest::$model->arraytypes->item->__items as $nodeid => $node) {
+ $count++ ;
+ $node->number = $count;
+ }
+ BaseModelTest::$model->serializeToConfig();
+ }
}