mirror of
https://github.com/opnsense/core.git
synced 2026-05-28 04:34:51 -04:00
mvc: Add RegexField and RegexFieldTest that validate PCRE2 engine regular expressions (#9291)
* mvc: Add RegexField and RegexFieldTest that validate PCRE2 engine regular expressions * Add tests for delimiters with modifiers
This commit is contained in:
parent
51bdb9ac84
commit
03c8d0a36f
3 changed files with 264 additions and 0 deletions
2
plist
2
plist
|
|
@ -631,6 +631,7 @@
|
|||
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/OptionField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/PortField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/ProtocolField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/RegexField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/TextField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/UniqueIdField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/UpdateOnlyTextField.php
|
||||
|
|
@ -1036,6 +1037,7 @@
|
|||
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/OptionFieldTest.php
|
||||
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/PortFieldTest.php
|
||||
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/ProtocolFieldTest.php
|
||||
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/RegexFieldTest.php
|
||||
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/TextFieldTest.php
|
||||
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/VirtualIPFieldTest.php
|
||||
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/VirtualIPFieldTest/config.xml
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
namespace OPNsense\Base\FieldTypes;
|
||||
|
||||
use OPNsense\Base\Validators\CallbackValidator;
|
||||
|
||||
class RegexField extends BaseField
|
||||
{
|
||||
/**
|
||||
* @var bool marks if this is a data node or a container
|
||||
*/
|
||||
protected $internalIsContainer = false;
|
||||
|
||||
/**
|
||||
* @var bool whether PHP-style delimiters are required
|
||||
*/
|
||||
private $internalRequireDelimiters = false;
|
||||
|
||||
/**
|
||||
* @param string $value Y/N
|
||||
*/
|
||||
public function setRequireDelimiters($value): void
|
||||
{
|
||||
$this->internalRequireDelimiters = strtoupper(trim($value)) === 'Y';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function defaultValidationMessage()
|
||||
{
|
||||
return gettext("Invalid regular expression pattern.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array list of validators for this field
|
||||
*/
|
||||
public function getValidators()
|
||||
{
|
||||
$validators = parent::getValidators();
|
||||
|
||||
$validators[] = new CallbackValidator([
|
||||
"callback" => function ($value) {
|
||||
$item = (string)$value;
|
||||
|
||||
// Skip validation if empty
|
||||
if ($item === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try using the pattern as-is to check if it has delimiters
|
||||
$hasDelimiters = @preg_match($item, '') !== false;
|
||||
|
||||
if ($this->internalRequireDelimiters) {
|
||||
// Delimiters are required
|
||||
if (!$hasDelimiters) {
|
||||
return [$this->getValidationMessage()];
|
||||
}
|
||||
} else {
|
||||
// No delimiters expected
|
||||
if ($hasDelimiters) {
|
||||
return [$this->getValidationMessage()];
|
||||
}
|
||||
|
||||
// Wrap pattern with delimiter for validation
|
||||
$delimiter = chr(1);
|
||||
$testPattern = $delimiter . $item . $delimiter;
|
||||
|
||||
if (@preg_match($testPattern, '') === false) {
|
||||
return [$this->getValidationMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
]);
|
||||
|
||||
return $validators;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
namespace tests\OPNsense\Base\FieldTypes;
|
||||
|
||||
// @CodingStandardsIgnoreStart
|
||||
require_once __DIR__ . '/Field_Framework_TestCase.php';
|
||||
// @CodingStandardsIgnoreEnd
|
||||
|
||||
use OPNsense\Base\FieldTypes\RegexField;
|
||||
|
||||
class RegexFieldTest extends Field_Framework_TestCase
|
||||
{
|
||||
public function testCanBeCreated()
|
||||
{
|
||||
$this->assertInstanceOf(RegexField::class, new RegexField());
|
||||
}
|
||||
|
||||
public function testIsContainer()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$this->assertFalse($field->isContainer());
|
||||
}
|
||||
|
||||
public function testRequiredEmpty()
|
||||
{
|
||||
$this->expectException(\OPNsense\Base\ValidationException::class);
|
||||
$field = new RegexField();
|
||||
$field->setRequired("Y");
|
||||
$field->setValue("");
|
||||
$field->eventPostLoading();
|
||||
$this->validateThrow($field);
|
||||
}
|
||||
|
||||
public function testRequiredNotEmpty()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->setRequired("Y");
|
||||
$field->setValue("^test$");
|
||||
$field->eventPostLoading();
|
||||
$this->assertEmpty($this->validate($field));
|
||||
}
|
||||
|
||||
public function testValidPatternsWithoutDelimiters()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->setRequireDelimiters("N");
|
||||
$field->eventPostLoading();
|
||||
foreach (["^test$", "[a-z]+", "\\d{3}", "foo|bar", "(?i)case.*insensitive"] as $value) {
|
||||
$field->setValue($value);
|
||||
$this->assertEmpty($this->validate($field), "$value should be valid");
|
||||
}
|
||||
}
|
||||
|
||||
public function testValidPatternsWithDelimiters()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->setRequireDelimiters("Y");
|
||||
$field->eventPostLoading();
|
||||
foreach (["/^test$/", "/[a-z]+/i", "#\\d{3}#", "/foo|bar/"] as $value) {
|
||||
$field->setValue($value);
|
||||
$this->assertEmpty($this->validate($field), "$value should be valid");
|
||||
}
|
||||
}
|
||||
|
||||
public function testInvalidPatternsWithoutDelimiters()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->setRequireDelimiters("N");
|
||||
$field->eventPostLoading();
|
||||
foreach (["[unclosed", "bad)", "(?bad-group"] as $value) {
|
||||
$field->setValue($value);
|
||||
$this->assertNotEmpty($this->validate($field), "$value should be invalid");
|
||||
}
|
||||
}
|
||||
|
||||
public function testInvalidPatternsWithDelimiters()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->setRequireDelimiters("Y");
|
||||
$field->eventPostLoading();
|
||||
foreach (["/[unclosed", "/bad)/", "no-delimiters", "/(?bad-group/"] as $value) {
|
||||
$field->setValue($value);
|
||||
$this->assertNotEmpty($this->validate($field), "$value should be invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Test patterns with delimiters and trailing modifiers (PHP PCRE2 style)
|
||||
public function testAllowDelimitersWithModifiers()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->setRequireDelimiters("Y");
|
||||
$field->eventPostLoading();
|
||||
foreach (["/^test$/im", "#[a-z]+#iu", "~\\d{3}~ms"] as $value) {
|
||||
$field->setValue($value);
|
||||
$this->assertEmpty($this->validate($field), "$value should be valid");
|
||||
}
|
||||
}
|
||||
|
||||
public function testRejectDelimitersWithModifiers()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->setRequireDelimiters("N");
|
||||
$field->eventPostLoading();
|
||||
foreach (["/^test$/im", "#[a-z]+#iu", "~\\d{3}~ms"] as $value) {
|
||||
$field->setValue($value);
|
||||
$this->assertNotEmpty($this->validate($field), "$value should be invalid (has delimiters)");
|
||||
}
|
||||
}
|
||||
|
||||
public function testDefaultBehaviorNoDelimiters()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->eventPostLoading();
|
||||
|
||||
// Default should be no delimiters required
|
||||
$field->setValue("^test$");
|
||||
$this->assertEmpty($this->validate($field), "Default should accept patterns without delimiters");
|
||||
|
||||
$field->setValue("/^test$/");
|
||||
$this->assertNotEmpty($this->validate($field), "Default should reject PHP-style delimiters");
|
||||
}
|
||||
|
||||
public function testEmptyValue()
|
||||
{
|
||||
$field = new RegexField();
|
||||
$field->eventPostLoading();
|
||||
$field->setValue("");
|
||||
$this->assertEmpty($this->validate($field), "Empty value should be valid when not required");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue