From 00d10e62b555c79eb9793aeb0290cf365fe4005c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 4 Mar 2026 22:58:48 +1300 Subject: [PATCH] (feat): Add AppDomain validator for installer input --- .../Installer/Validator/AppDomain.php | 82 ++++++++ .../Installer/Validator/AppDomainTest.php | 196 ++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 src/Appwrite/Platform/Installer/Validator/AppDomain.php create mode 100644 tests/unit/Platform/Modules/Installer/Validator/AppDomainTest.php diff --git a/src/Appwrite/Platform/Installer/Validator/AppDomain.php b/src/Appwrite/Platform/Installer/Validator/AppDomain.php new file mode 100644 index 0000000000..f631015654 --- /dev/null +++ b/src/Appwrite/Platform/Installer/Validator/AppDomain.php @@ -0,0 +1,82 @@ + 2) { + return false; + } + if (count($parts) === 2) { + [$host, $port] = $parts; + } + } + + if ($port !== null && $port !== '') { + $portInt = (int) $port; + if ((string) $portInt !== $port || $portInt < 1 || $portInt > 65535) { + return false; + } + } + + return $this->isValidDomain($host); + } + + private function isValidDomain(string $value): bool + { + if ($value === 'localhost') { + return true; + } + if (filter_var($value, FILTER_VALIDATE_IP) !== false) { + return true; + } + return filter_var($value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; + } +} diff --git a/tests/unit/Platform/Modules/Installer/Validator/AppDomainTest.php b/tests/unit/Platform/Modules/Installer/Validator/AppDomainTest.php new file mode 100644 index 0000000000..4a780afdac --- /dev/null +++ b/tests/unit/Platform/Modules/Installer/Validator/AppDomainTest.php @@ -0,0 +1,196 @@ +validator = new AppDomain(); + } + + public function tearDown(): void + { + $this->validator = null; + } + + // --- Metadata --- + + public function testDescription(): void + { + $this->assertNotEmpty($this->validator->getDescription()); + $this->assertIsString($this->validator->getDescription()); + } + + public function testIsArray(): void + { + $this->assertFalse($this->validator->isArray()); + } + + public function testType(): void + { + $this->assertEquals($this->validator::TYPE_STRING, $this->validator->getType()); + } + + // --- Non-string types --- + + public function testRejectsNonStringTypes(): void + { + $this->assertFalse($this->validator->isValid(null)); + $this->assertFalse($this->validator->isValid(false)); + $this->assertFalse($this->validator->isValid(true)); + $this->assertFalse($this->validator->isValid(123)); + $this->assertFalse($this->validator->isValid(12.34)); + $this->assertFalse($this->validator->isValid([])); + $this->assertFalse($this->validator->isValid(new \stdClass())); + } + + // --- Empty / whitespace --- + + public function testRejectsEmptyString(): void + { + $this->assertFalse($this->validator->isValid('')); + } + + public function testRejectsWhitespaceOnly(): void + { + $this->assertFalse($this->validator->isValid(' ')); + $this->assertFalse($this->validator->isValid("\t")); + $this->assertFalse($this->validator->isValid("\n")); + } + + // --- Localhost --- + + public function testAcceptsLocalhost(): void + { + $this->assertTrue($this->validator->isValid('localhost')); + } + + public function testAcceptsLocalhostWithPort(): void + { + $this->assertTrue($this->validator->isValid('localhost:8080')); + $this->assertTrue($this->validator->isValid('localhost:80')); + $this->assertTrue($this->validator->isValid('localhost:443')); + $this->assertTrue($this->validator->isValid('localhost:1')); + $this->assertTrue($this->validator->isValid('localhost:65535')); + } + + // --- Valid domains --- + + public function testAcceptsValidDomains(): void + { + $this->assertTrue($this->validator->isValid('example.com')); + $this->assertTrue($this->validator->isValid('sub.example.com')); + $this->assertTrue($this->validator->isValid('deep.sub.example.com')); + $this->assertTrue($this->validator->isValid('appwrite.io')); + $this->assertTrue($this->validator->isValid('my-app.example.org')); + } + + public function testAcceptsDomainsWithPort(): void + { + $this->assertTrue($this->validator->isValid('example.com:443')); + $this->assertTrue($this->validator->isValid('example.com:8080')); + $this->assertTrue($this->validator->isValid('sub.example.com:3000')); + } + + // --- Valid IPv4 --- + + public function testAcceptsIPv4Addresses(): void + { + $this->assertTrue($this->validator->isValid('127.0.0.1')); + $this->assertTrue($this->validator->isValid('192.168.1.1')); + $this->assertTrue($this->validator->isValid('10.0.0.1')); + $this->assertTrue($this->validator->isValid('0.0.0.0')); + $this->assertTrue($this->validator->isValid('255.255.255.255')); + } + + public function testAcceptsIPv4WithPort(): void + { + $this->assertTrue($this->validator->isValid('127.0.0.1:8080')); + $this->assertTrue($this->validator->isValid('192.168.1.1:443')); + $this->assertTrue($this->validator->isValid('10.0.0.1:3000')); + } + + // --- Valid IPv6 bracket notation --- + + public function testAcceptsIPv6BracketNotation(): void + { + $this->assertTrue($this->validator->isValid('[::1]')); + $this->assertTrue($this->validator->isValid('[::1]:8080')); + $this->assertTrue($this->validator->isValid('[2001:db8::1]')); + $this->assertTrue($this->validator->isValid('[2001:db8::1]:443')); + // Scoped IPv6 with zone ID is not supported by FILTER_VALIDATE_IP + $this->assertFalse($this->validator->isValid('[fe80::1%25eth0]')); + } + + // --- Invalid domains --- + + public function testRejectsInvalidDomains(): void + { + $this->assertFalse($this->validator->isValid('-invalid.com')); + $this->assertFalse($this->validator->isValid('invalid-.com')); + $this->assertFalse($this->validator->isValid('.example.com')); + } + + // --- Invalid port --- + + public function testRejectsInvalidPorts(): void + { + $this->assertFalse($this->validator->isValid('localhost:0')); + $this->assertFalse($this->validator->isValid('localhost:65536')); + $this->assertFalse($this->validator->isValid('localhost:99999')); + $this->assertFalse($this->validator->isValid('localhost:abc')); + $this->assertFalse($this->validator->isValid('localhost:-1')); + } + + // --- Multiple colons without brackets --- + + public function testRejectsMultipleColonsWithoutBrackets(): void + { + $this->assertFalse($this->validator->isValid('::1')); + $this->assertFalse($this->validator->isValid('2001:db8::1')); + $this->assertFalse($this->validator->isValid('a:b:c')); + } + + // --- Malformed IPv6 brackets --- + + public function testRejectsMalformedIPv6Brackets(): void + { + $this->assertFalse($this->validator->isValid('[')); + $this->assertFalse($this->validator->isValid('[]')); + $this->assertFalse($this->validator->isValid('[::1')); + $this->assertFalse($this->validator->isValid('::1]')); + $this->assertFalse($this->validator->isValid('[invalid')); + } + + // --- Port boundary values --- + + public function testPortBoundaryValues(): void + { + $this->assertTrue($this->validator->isValid('localhost:1')); + $this->assertTrue($this->validator->isValid('localhost:65535')); + $this->assertFalse($this->validator->isValid('localhost:0')); + $this->assertFalse($this->validator->isValid('localhost:65536')); + } + + // --- Trimming --- + + public function testTrimsWhitespace(): void + { + $this->assertTrue($this->validator->isValid(' localhost ')); + $this->assertTrue($this->validator->isValid(' example.com ')); + } + + // --- Empty port segment --- + + public function testAcceptsEmptyPortSegment(): void + { + // 'localhost:' splits into host='localhost', port='' — empty port is skipped + $this->assertTrue($this->validator->isValid('localhost:')); + } +}