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(); + } }