diff --git a/CHANGELOG-4.x.md b/CHANGELOG-4.x.md index e3117be..4c3c8ed 100644 --- a/CHANGELOG-4.x.md +++ b/CHANGELOG-4.x.md @@ -53,3 +53,6 @@ must instantiate the object first, and then call `fromText` method. All paramete child or subdomain zone files. * [PR #82](https://github.com/Badcow/DNS/pull/82) - Fix character escaping in TXT records. (Thank you, [@fbett](https://github.com/fbett)) * [Issue #84](https://github.com/Badcow/DNS/issues/84) - `TXT::toText()` now splits string into 255-byte chunks. (Thank you, [@fbett](https://github.com/fbett)) +* [Issue #85](https://github.com/Badcow/DNS/issues/85) - `Badow\DNS\AlignedBuilder` now has finer controls. You can now + define the order of rendering Resource Records and add or change Rdata output formatters (see `Docs/AlignedZoneBuilder`. +* `Badow\DNS\AlignedBuilder` cannot be called statically anymore. It must be instantiated. diff --git a/README.md b/README.md index f14a97e..3dd0dbf 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ $zone->addResourceRecord($a6); $zone->addResourceRecord($ns2); $zone->addResourceRecord($mx1); -echo AlignedBuilder::build($zone); +$builder = new AlignedBuilder(); +echo $builder->build($zone); ``` ### Output diff --git a/docs/AlignedZoneBuilder.md b/docs/AlignedZoneBuilder.md new file mode 100644 index 0000000..611b6ab --- /dev/null +++ b/docs/AlignedZoneBuilder.md @@ -0,0 +1,217 @@ +AlignedZoneBuilder +================== +The `Badcow\DNS\AlignedZoneBuilder` class takes a `Badcow\DNS\Zone` and creates aesthetically pleasing BIND style zone +record. + +## Example +```php +require_once '/path/to/vendor/autoload.php'; + +use Badcow\DNS\Classes; +use Badcow\DNS\Zone; +use Badcow\DNS\Rdata\Factory; +use Badcow\DNS\ResourceRecord; +use Badcow\DNS\AlignedBuilder; + +$zone = new Zone('example.com.'); +$zone->setDefaultTtl(3600); + +$soa = new ResourceRecord; +$soa->setName('@'); +$soa->setClass(Classes::INTERNET); +$soa->setRdata(Factory::Soa( + 'example.com.', + 'post.example.com.', + '2014110501', + 3600, + 14400, + 604800, + 3600 +)); + +$ns1 = new ResourceRecord; +$ns1->setName('@'); +$ns1->setClass(Classes::INTERNET); +$ns1->setRdata(Factory::Ns('ns1.nameserver.com.')); + +$ns2 = new ResourceRecord; +$ns2->setName('@'); +$ns2->setClass(Classes::INTERNET); +$ns2->setRdata(Factory::Ns('ns2.nameserver.com.')); + +$a = new ResourceRecord; +$a->setName('sub.domain'); +$a->setRdata(Factory::A('192.168.1.42')); +$a->setComment('This is a local ip.'); + +$a6 = new ResourceRecord; +$a6->setName('ipv6.domain'); +$a6->setRdata(Factory::Aaaa('::1')); +$a6->setComment('This is an IPv6 domain.'); + +$mx1 = new ResourceRecord; +$mx1->setName('@'); +$mx1->setRdata(Factory::Mx(10, 'mail-gw1.example.net.')); + +$mx2 = new ResourceRecord; +$mx2->setName('@'); +$mx2->setRdata(Factory::Mx(20, 'mail-gw2.example.net.')); + +$mx3 = new ResourceRecord; +$mx3->setName('@'); +$mx3->setRdata(Factory::Mx(30, 'mail-gw3.example.net.')); + +$zone->addResourceRecord($soa); +$zone->addResourceRecord($mx2); +$zone->addResourceRecord($ns1); +$zone->addResourceRecord($mx3); +$zone->addResourceRecord($a); +$zone->addResourceRecord($a6); +$zone->addResourceRecord($ns2); +$zone->addResourceRecord($mx1); + +$builder = new AlignedBuilder(); +echo $builder->build($zone); +``` + +### Output +```txt +$ORIGIN example.com. +$TTL 3600 +@ IN SOA ( + example.com. ; MNAME + post.example.com. ; RNAME + 2014110501 ; SERIAL + 3600 ; REFRESH + 14400 ; RETRY + 604800 ; EXPIRE + 3600 ; MINIMUM + ) + +; NS RECORDS +@ IN NS ns1.nameserver.com. +@ IN NS ns2.nameserver.com. + +; A RECORDS +sub.domain A 192.168.1.42; This is a local ip. + +; AAAA RECORDS +ipv6.domain AAAA ::1; This is an IPv6 domain. + +; MX RECORDS +@ MX 10 mail-gw1.example.net. +@ MX 20 mail-gw2.example.net. +@ MX 30 mail-gw3.example.net. +``` + +## Customisations +### Resource Record Order +You can change the order in which the Resource Records are rendered, e.g. +```php +$alignedBuilder = new \Badcow\DNS\AlignedBuilder(); +$myNewOrder = ['SOA', 'A', 'MX', 'AAAA', 'NS']; +$alignedBuilder->setOrder($myNewOrder); +echo $alignedBuilder->build($zone); +``` +#### Output +```txt +$ORIGIN example.com. +$TTL 3600 +@ IN SOA ( + example.com. ; MNAME + post.example.com. ; RNAME + 2014110501 ; SERIAL + 3600 ; REFRESH + 14400 ; RETRY + 604800 ; EXPIRE + 3600 ; MINIMUM + ) + +; A RECORDS +sub.domain A 192.168.1.42; This is a local ip. + +; MX RECORDS +@ MX 10 mail-gw1.example.net. +@ MX 20 mail-gw2.example.net. +@ MX 30 mail-gw3.example.net. + +; AAAA RECORDS +ipv6.domain AAAA ::1; This is an IPv6 domain. + +; NS RECORDS +@ IN NS ns1.nameserver.com. +@ IN NS ns2.nameserver.com. +``` + +### Adding special handlers + +It may be the case that you want to define (or change) the way that some Rdata is formatted; you can define custom Rdata +formatters in the AlignedBuilder. The parameters that are exposed to the callable are: + * `\Badcow\DNS\Rdata\RdataInterface $rdata` This is the Rdata that needs special handling. + * `int $padding` the amount of spaces before the start of the Rdata column + +Below is an example where `TXT` rdata is split over multiple lines: +```php +function specialTxtFormatter(Badcow\DNS\Rdata\TXT $rdata, int $padding): string +{ + //If the text length is less than or equal to 50 characters, just return it unaltered. + if (strlen($rdata->getText()) <= 50) { + return sprintf('"%s"', addcslashes($rdata->getText(), '"\\')); + } + + $returnVal = "(\n"; + $chunks = str_split($rdata->getText(), 50); + foreach ($chunks as $chunk) { + $returnVal .= str_repeat(' ', $padding). + sprintf('"%s"', addcslashes($chunk, '"\\')). + "\n"; + } + $returnVal .= str_repeat(' ', $padding) . ")"; + + return $returnVal; +} + +$zone = new Badcow\DNS\Zone('example.com.'); +$zone->setDefaultTtl(3600); + +$txt = new Badcow\DNS\ResourceRecord; +$txt->setName('txt.example.com.'); +$txt->setClass('IN'); +$txt->setRdata(Badcow\DNS\Rdata\Factory::Txt( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ac suscipit risus. Curabitur ac urna et quam'. + 'porttitor bibendum ut ac ipsum. Duis congue diam sed velit interdum ornare. Nullam dolor quam, aliquam sit amet'. + 'lacinia vel, rutrum et lacus. Aenean condimentum, massa a consectetur feugiat, massa augue accumsan tellus, ac'. + 'fringilla turpis velit a velit. Nunc ut tincidunt nisi. Ut pretium laoreet nisi, quis commodo lectus porta'. + 'vulputate. Vestibulum ullamcorper sed sapien ut venenatis. Morbi ut nulla eget dolor mattis dictum. Suspendisse'. + 'ut rutrum quam. Praesent id mi id justo maximus tristique.' +)); + +$zone->addResourceRecord($txt); + +$alignedBuilder = new Badcow\DNS\AlignedBuilder(); +$alignedBuilder->addRdataFormatter('TXT', 'specialTxtFormatter'); + +echo $alignedBuilder->build($zone); +``` +####Output +``` +$ORIGIN example.com. +$TTL 3600 + +; TXT RECORDS +txt.example.com. IN TXT ( + "Lorem ipsum dolor sit amet, consectetur adipiscing" + " elit. Quisque ac suscipit risus. Curabitur ac urn" + "a et quamporttitor bibendum ut ac ipsum. Duis cong" + "ue diam sed velit interdum ornare. Nullam dolor qu" + "am, aliquam sit ametlacinia vel, rutrum et lacus. " + "Aenean condimentum, massa a consectetur feugiat, m" + "assa augue accumsan tellus, acfringilla turpis vel" + "it a velit. Nunc ut tincidunt nisi. Ut pretium lao" + "reet nisi, quis commodo lectus portavulputate. Ves" + "tibulum ullamcorper sed sapien ut venenatis. Morbi" + " ut nulla eget dolor mattis dictum. Suspendisseut " + "rutrum quam. Praesent id mi id justo maximus trist" + "ique." + ) +``` \ No newline at end of file diff --git a/lib/AlignedBuilder.php b/lib/AlignedBuilder.php index 076ec86..56a897a 100755 --- a/lib/AlignedBuilder.php +++ b/lib/AlignedBuilder.php @@ -38,7 +38,7 @@ class AlignedBuilder * * @var array */ - private static $order = [ + private $order = [ SOA::TYPE, NS::TYPE, A::TYPE, @@ -56,10 +56,54 @@ class AlignedBuilder RRSIG::TYPE, ]; + /** + * @var callable[] array of Rdata type indexed, callables that handle the output formatting of Rdata + */ + private $rdataFormatters = []; + + public function __construct() + { + $this->rdataFormatters = AlignedRdataFormatters::$rdataFormatters; + } + + /** + * Adds or changes an Rdata output formatter. + * + * @param string $type the Rdata type to be handled by the $formatter + * @param callable $formatter callable that will handle the output formatting of the Rdata + */ + public function addRdataFormatter(string $type, callable $formatter): void + { + $this->rdataFormatters[$type] = $formatter; + } + + public function getRdataFormatters(): array + { + return $this->rdataFormatters; + } + + /** + * @return string[] + */ + public function getOrder(): array + { + return $this->order; + } + + /** + * Set the order in which Resource Records should appear in a zone.. + * + * @param string[] $order Simple string array of Rdata types + */ + public function setOrder(array $order): void + { + $this->order = $order; + } + /** * Build an aligned BIND zone file. */ - public static function build(Zone $zone): string + public function build(Zone $zone): string { $master = self::generateControlEntries($zone); $resourceRecords = $zone->getResourceRecords(); @@ -84,7 +128,7 @@ public static function build(Zone $zone): string str_pad((string) $resourceRecord->getTtl(), $ttlPadding, Tokens::SPACE, STR_PAD_RIGHT), str_pad((string) $resourceRecord->getClass(), $classPadding, Tokens::SPACE, STR_PAD_RIGHT), str_pad($rdata->getType(), $typePadding, Tokens::SPACE, STR_PAD_RIGHT), - self::generateRdataOutput($rdata, $rdataPadding) + $this->generateRdataOutput($rdata, $rdataPadding) ); $master .= self::generateComment($resourceRecord); @@ -121,8 +165,15 @@ private static function generateComment(ResourceRecord $resourceRecord): string /** * Compares two ResourceRecords to determine which is the higher order. Used with the usort() function. + * + * @param ResourceRecord $a The first ResourceRecord + * @param ResourceRecord $b The second ResourceRecord + * + * @return int $a is higher precedence than $b if return value is less than 0. + * $b is higher precedence than $a if return value is greater than 0. + * $a and $b have the same precedence if the return value is 0. */ - public static function compareResourceRecords(ResourceRecord $a, ResourceRecord $b): int + public function compareResourceRecords(ResourceRecord $a, ResourceRecord $b): int { $a_rdata = (null === $a->getRdata()) ? '' : $a->getRdata()->toText(); $b_rdata = (null === $b->getRdata()) ? '' : $b->getRdata()->toText(); @@ -133,8 +184,8 @@ public static function compareResourceRecords(ResourceRecord $a, ResourceRecord } //Find the precedence (if any) for the two types. - $_a = array_search($a->getType(), self::$order); - $_b = array_search($b->getType(), self::$order); + $_a = array_search($a->getType(), $this->order); + $_b = array_search($b->getType(), $this->order); //If neither types have defined precedence. if (!is_int($_a) && !is_int($_b)) { @@ -157,12 +208,14 @@ public static function compareResourceRecords(ResourceRecord $a, ResourceRecord /** * Composes the RDATA of the Resource Record. + * + * @param RdataInterface $rdata the Rdata to be formatted + * @param int $padding the number of spaces before the Rdata column */ - private static function generateRdataOutput(RdataInterface $rdata, int $padding): string + private function generateRdataOutput(RdataInterface $rdata, int $padding): string { - $rdataFormatters = AlignedRdataFormatters::getRdataFormatters(); - if (array_key_exists($rdata->getType(), $rdataFormatters)) { - return call_user_func($rdataFormatters[$rdata->getType()], $rdata, $padding); + if (array_key_exists($rdata->getType(), $this->rdataFormatters)) { + return call_user_func($this->rdataFormatters[$rdata->getType()], $rdata, $padding); } return $rdata->toText(); @@ -171,6 +224,8 @@ private static function generateRdataOutput(RdataInterface $rdata, int $padding) /** * Get the padding required for a zone. * + * @param Zone $zone the DNS Zone being processed + * * @return int[] Array order: [name, ttl, type, class, rdata] */ private static function getPadding(Zone $zone): array diff --git a/tests/AlignedBuilderTest.php b/tests/AlignedBuilderTest.php index 854aacf..5b1fc5e 100644 --- a/tests/AlignedBuilderTest.php +++ b/tests/AlignedBuilderTest.php @@ -187,20 +187,22 @@ public function testCompareResourceRecords(): void $spf = new ResourceRecord(); $spf->setRdata(Factory::SPF('skjdfskjasdfjh')); - $this->assertTrue(AlignedBuilder::compareResourceRecords($soa, $ns1) < 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($aaaa, $cname) < 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $mx2) < 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $mx2) < 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $spf) < 0); + $alignedBuilder = new AlignedBuilder(); - $this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $a) > 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($ns2, $ns1) > 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($spf, $txt) > 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($soa, $ns1) < 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($aaaa, $cname) < 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $mx2) < 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $mx2) < 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $spf) < 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($nsec3, $rrsig) < 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($rrsig, $nsec3) > 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $a) > 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($ns2, $ns1) > 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($spf, $txt) > 0); - $this->assertTrue(AlignedBuilder::compareResourceRecords($rp, $spf) < 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($nsec3, $rrsig) < 0); + $this->assertTrue($alignedBuilder->compareResourceRecords($rrsig, $nsec3) > 0); + + $this->assertTrue($alignedBuilder->compareResourceRecords($rp, $spf) < 0); } public function testBuild(): void diff --git a/tests/ZoneTest.php b/tests/ZoneTest.php index 75613c3..b82e2ac 100644 --- a/tests/ZoneTest.php +++ b/tests/ZoneTest.php @@ -127,6 +127,7 @@ public function testSetName(): void public function testFillOut(): void { $zone = self::buildTestZone(); + $alignedBuilder = new AlignedBuilder(); ZoneBuilder::fillOutZone($zone); $expectation = file_get_contents(__DIR__.'/Resources/example.com_filled-out.txt'); @@ -134,7 +135,7 @@ public function testFillOut(): void //This is a fix for Windows systems that may expect a carriage return char. $expectation = str_replace("\r", '', $expectation); - $this->assertEquals($expectation, AlignedBuilder::build($zone)); + $this->assertEquals($expectation, $alignedBuilder->build($zone)); } public function testOtherFunctions(): void