diff --git a/src/Illuminate/Support/Facades/Validator.php b/src/Illuminate/Support/Facades/Validator.php index a032cc53ef4c..8adab5af73f3 100755 --- a/src/Illuminate/Support/Facades/Validator.php +++ b/src/Illuminate/Support/Facades/Validator.php @@ -11,6 +11,7 @@ * @method static void replacer(string $rule, \Closure|string $replacer) * @method static void includeUnvalidatedArrayKeys() * @method static void excludeUnvalidatedArrayKeys() + * @method static void fakeDnsLookups(bool $value = true) * @method static void resolver(\Closure $resolver) * @method static \Illuminate\Contracts\Translation\Translator getTranslator() * @method static \Illuminate\Validation\PresenceVerifierInterface getPresenceVerifier() diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 6c22272cc5c2..c774c9699819 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -22,6 +22,7 @@ use Illuminate\Support\Exceptions\MathException; use Illuminate\Support\Facades\Date; use Illuminate\Support\Str; +use Illuminate\Validation\FakeDnsGetRecordWrapper; use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Unique; use Illuminate\Validation\ValidationData; @@ -147,6 +148,16 @@ public function validateActiveUrl($attribute, $value) */ protected function getDnsRecords($hostname, $type) { + if (static::$fakeDnsLookups) { + $hostname = rtrim($hostname, '.'); + + if (filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false || filter_var($hostname, FILTER_VALIDATE_IP) !== false) { + return false; + } + + return [['host' => $hostname, 'class' => 'IN', 'ttl' => 60, 'type' => 'A', 'ip' => '127.0.0.1']]; + } + return dns_get_record($hostname, $type); } @@ -959,14 +970,13 @@ public function validateEmail($attribute, $value, $parameters) ->unique() ->map(fn ($validation) => match (true) { $validation === 'strict' => new NoRFCWarningsValidation(), - $validation === 'dns' => new DNSCheckValidation(), + $validation === 'dns' => new DNSCheckValidation(static::$fakeDnsLookups ? new FakeDnsGetRecordWrapper : null), $validation === 'spoof' => new SpoofCheckValidation(), $validation === 'filter' => new FilterEmailValidation(), $validation === 'filter_unicode' => FilterEmailValidation::unicode(), is_string($validation) && class_exists($validation) => $this->container->make($validation), default => new RFCValidation(), }) - ->values() ->all() ?: [new RFCValidation]; $emailValidator = Container::getInstance()->make(EmailValidator::class); diff --git a/src/Illuminate/Validation/Factory.php b/src/Illuminate/Validation/Factory.php index 8cf5027eb26f..62d229597dc1 100755 --- a/src/Illuminate/Validation/Factory.php +++ b/src/Illuminate/Validation/Factory.php @@ -267,6 +267,17 @@ public function excludeUnvalidatedArrayKeys() $this->excludeUnvalidatedArrayKeys = true; } + /** + * Fake the DNS lookups performed by validation rules so they always succeed. + * + * @param bool $value + * @return void + */ + public function fakeDnsLookups($value = true) + { + Validator::fakeDnsLookups($value); + } + /** * Set the Validator instance resolver. * diff --git a/src/Illuminate/Validation/FakeDnsGetRecordWrapper.php b/src/Illuminate/Validation/FakeDnsGetRecordWrapper.php new file mode 100644 index 000000000000..26efbea9d53d --- /dev/null +++ b/src/Illuminate/Validation/FakeDnsGetRecordWrapper.php @@ -0,0 +1,21 @@ + 'A']]); + } +} diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 16d4a0abb93e..0880ddf061b6 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -314,6 +314,13 @@ class Validator implements ValidatorContract */ protected static $placeholderHash; + /** + * Indicates if DNS lookups performed by validation rules should be faked to always succeed. + * + * @var bool + */ + protected static $fakeDnsLookups = false; + /** * The exception to throw upon failure. * @@ -1739,6 +1746,17 @@ protected static function decodeAttributeWithPlaceholder(string $attribute) return str_replace('__dot__'.static::$placeholderHash, '\\.', $attribute); } + /** + * Fake the DNS lookups performed by validation rules so they always succeed. + * + * @param bool $value + * @return void + */ + public static function fakeDnsLookups($value = true) + { + static::$fakeDnsLookups = $value; + } + /** * Flush the validator's global state. * @@ -1747,6 +1765,7 @@ protected static function decodeAttributeWithPlaceholder(string $attribute) public static function flushState() { static::$placeholderHash = null; + static::$fakeDnsLookups = false; } /** diff --git a/tests/Validation/ValidationFactoryTest.php b/tests/Validation/ValidationFactoryTest.php index 2a3e3bca56c7..9061eeeaaf18 100755 --- a/tests/Validation/ValidationFactoryTest.php +++ b/tests/Validation/ValidationFactoryTest.php @@ -9,9 +9,17 @@ use Illuminate\Validation\Validator; use Mockery as m; use PHPUnit\Framework\TestCase; +use ReflectionProperty; class ValidationFactoryTest extends TestCase { + protected function tearDown(): void + { + Validator::flushState(); + + parent::tearDown(); + } + public function testMakeMethodCreatesValidValidator() { $translator = m::mock(TranslatorInterface::class); @@ -146,4 +154,11 @@ public function testSetContainer() $this->assertSame($container, $factory->setContainer($container)->getContainer()); } + + public function testFakeDnsLookupsDelegatesToTheValidator() + { + (new Factory(m::mock(TranslatorInterface::class)))->fakeDnsLookups(); + + $this->assertTrue((new ReflectionProperty(Validator::class, 'fakeDnsLookups'))->getValue()); + } } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 39bb2a6bdf91..d63975b41d23 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -31,10 +31,10 @@ use Illuminate\Validation\Validator; use InvalidArgumentException; use Mockery as m; -use Mockery\MockInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; +use ReflectionProperty; use RuntimeException; use stdClass; use Symfony\Component\HttpFoundation\File\File; @@ -42,6 +42,13 @@ class ValidationValidatorTest extends TestCase { + protected function tearDown(): void + { + Validator::flushState(); + + parent::tearDown(); + } + public function testNestedErrorMessagesAreRetrievedFromLocalArray() { $trans = $this->getIlluminateArrayTranslator(); @@ -5327,18 +5334,10 @@ public static function invalidUrls() #[DataProvider('activeUrlDataProvider')] public function testValidateActiveUrl($data, $outcome) { + Validator::fakeDnsLookups(); + $trans = $this->getIlluminateArrayTranslator(); - $v = m::mock( - new Validator($trans, $data, ['x' => 'active_url']), - function (MockInterface $mock) { - $mock - ->shouldAllowMockingProtectedMethods() - ->shouldReceive('getDnsRecords') - ->withAnyArgs() - ->zeroOrMoreTimes() - ->andReturn(['hit']); - } - ); + $v = new Validator($trans, $data, ['x' => 'active_url']); $this->assertEquals($outcome, $v->passes()); } @@ -5368,6 +5367,65 @@ public static function activeUrlDataProvider() ]; } + public function testValidateActiveUrlWithFakedDnsLookups() + { + Validator::fakeDnsLookups(); + + $trans = $this->getIlluminateArrayTranslator(); + + $v = new Validator($trans, ['x' => 'https://this-domain-does-not-exist.invalid'], ['x' => 'active_url']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'aslsdlks'], ['x' => 'active_url']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => ['not-a-string']], ['x' => 'active_url']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 'http://foo..com'], ['x' => 'active_url']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 'http://127.0.0.1'], ['x' => 'active_url']); + $this->assertFalse($v->passes()); + } + + public function testValidateEmailWithDnsCheckWithFakedDnsLookups() + { + Validator::fakeDnsLookups(); + + $trans = $this->getIlluminateArrayTranslator(); + + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.com'], ['x' => 'email:dns']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.com'], ['x' => 'email:rfc,dns']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'not-an-email'], ['x' => 'email:dns']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => '.invalid@gmail.com'], ['x' => 'email:dns']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.invalid'], ['x' => 'email:dns']); + $this->assertFalse($v->passes()); + } + + public function testFakedDnsLookupsCanBeToggledAndAreFlushed() + { + $property = new ReflectionProperty(Validator::class, 'fakeDnsLookups'); + + Validator::fakeDnsLookups(); + $this->assertTrue($property->getValue()); + + Validator::fakeDnsLookups(false); + $this->assertFalse($property->getValue()); + + Validator::fakeDnsLookups(); + Validator::flushState(); + $this->assertFalse($property->getValue()); + } + public function testValidateImage() { $trans = $this->getIlluminateArrayTranslator();