From 367dde6f19dbf716f705f39e34fe9628a9673596 Mon Sep 17 00:00:00 2001
From: Mark Schouten <mark@tuxis.nl>
Date: Wed, 3 Aug 2016 13:16:30 +0200
Subject: [PATCH] Add new classes to handle zones and pdns-api v4.0

---
 includes/class/ApiHandler.php | 124 ++++++++++++++++
 includes/class/PdnsApi.php    |  84 +++++++++++
 includes/class/Zone.php       | 270 ++++++++++++++++++++++++++++++++++
 3 files changed, 478 insertions(+)
 create mode 100644 includes/class/ApiHandler.php
 create mode 100644 includes/class/PdnsApi.php
 create mode 100644 includes/class/Zone.php

diff --git a/includes/class/ApiHandler.php b/includes/class/ApiHandler.php
new file mode 100644
index 0000000..80b5a9a
--- /dev/null
+++ b/includes/class/ApiHandler.php
@@ -0,0 +1,124 @@
+<?php
+
+include_once('../config.inc.php');
+
+class ApiHandler {
+    public function __construct() {
+        global $apiip, $apiport, $apipass, $apiproto, $apisslverify;
+
+        $this->headers = Array();
+        $this->hostname = $apiip;
+        $this->port = $apiport;
+        $this->auth = $apipass;
+        $this->proto = $apiproto;
+        $this->sslverify = $apisslverify;
+        $this->curlh = curl_init();
+        $this->method = 'GET';
+        $this->content = FALSE;
+        $this->apiurl = '';
+    }
+
+    public function addheader($field, $content) {
+        $this->headers[$field] = $content;
+    }
+
+    private function authheaders() {
+        $this->addheader('X-API-Key', $this->auth);
+    }
+
+    private function apiurl() {
+        $tmp = new ApiHandler();
+
+        $tmp->url = '/api';
+        $tmp->go();
+        
+        if ($tmp->json[0]['version'] <= 1) {
+            $this->apiurl = $tmp->json[0]['url'];
+        } else {
+            throw new Exception("Unsupported API version");
+        }
+
+    }
+
+    private function curlopts() {
+        $this->authheaders();
+        $this->addheader('Accept', 'application/json');
+
+        curl_reset($this->curlh);
+        curl_setopt($this->curlh, CURLOPT_HTTPHEADER, Array());
+        curl_setopt($this->curlh, CURLOPT_RETURNTRANSFER, 1);
+
+        if (strcasecmp($this->proto, 'https')) {
+            curl_setopt($this->curlh, CURLOPT_SSL_VERIFYPEER, $this->sslverify);
+        }
+
+        $setheaders = Array();
+
+        foreach ($this->headers as $k => $v) {
+            array_push($setheaders, join(": ", Array($k, $v)));
+        }
+        curl_setopt($this->curlh, CURLOPT_HTTPHEADER, $setheaders);
+    }
+
+    private function baseurl() {
+        return $this->proto.'://'.$this->hostname.':'.$this->port.$this->apiurl;
+    }
+
+    private function go() {
+        $this->curlopts();
+
+        if ($this->content) {
+            $this->addheader('Content-Type', 'application/json');
+            curl_setopt($this->curlh, CURLOPT_POST, 1);
+            curl_setopt($this->curlh, CURLOPT_POSTFIELDS, $this->content);
+        }
+
+        switch ($this->method) {
+            case 'POST':
+                curl_setopt($this->curlh, CURLOPT_POST, 1);
+                break;
+            case 'GET':
+                curl_setopt($this->curlh, CURLOPT_POST, 0);
+                break;
+            case 'DELETE':
+            case 'PATCH':
+            case 'PUT':
+                curl_setopt($this->curlh, CURLOPT_CUSTOMREQUEST, $this->method);
+                break;
+        }
+
+        curl_setopt($this->curlh, CURLOPT_URL, $this->baseurl().$this->url);
+
+        print "Here we go:\n";
+        print "Request: ".$this->method.' '.$this->baseurl().$this->url."\n";
+        if ($this->content != '') {
+            print "Content: ".$this->content."\n";
+        }
+
+        $return = curl_exec($this->curlh);
+        $code = curl_getinfo($this->curlh, CURLINFO_HTTP_CODE);
+        $json = json_decode($return, 1);
+
+        if (isset($json['error'])) {
+            throw new Exception("API Error $code: ".$json['error']);
+        } elseif ($code < 200 || $code >= 300) {
+            if ($code == 401) {
+                throw new Exception("Authentication failed. Have you configured your authmethod correct?");
+            }
+            throw new Exception("Curl Error: $code ".curl_error($this->curlh));
+        }
+
+        $this->json = $json;
+    }
+
+    public function call() {
+        if (substr($this->url, 0, 1) == '/') {
+            $this->apiurl();
+        } else {
+            $this->apiurl = '/';
+        }
+
+        $this->go();
+    }
+}
+
diff --git a/includes/class/PdnsApi.php b/includes/class/PdnsApi.php
new file mode 100644
index 0000000..d9ed390
--- /dev/null
+++ b/includes/class/PdnsApi.php
@@ -0,0 +1,84 @@
+<?php
+
+include_once('apihandler.php');
+
+class PdnsAPI {
+    public function __construct() {
+        $this->http = new ApiHandler();
+    }
+
+    public function listzones($q = FALSE) {
+        $api = clone $this->http;
+        $api->method = 'GET';
+        if ($q) {
+            $api->url = "/servers/localhost/search-data?q=*".$q."*&max=25";
+            $api->call();
+            $ret = Array();
+            $seen = Array();
+
+            foreach ($api->json as $result) {
+                if (isset($seen[$result['zone_id']])) {
+                    continue;
+                }
+                $zone = $this->loadzone($result['zone_id']);
+                unset($zone['rrsets']);
+                array_push($ret, $zone);
+                $seen[$result['zone_id']] = 1;
+            }
+
+            return $ret;
+        }
+        $api->url = "/servers/localhost/zones";
+        $api->call();
+
+        return $api->json;
+    }
+
+    public function loadzone($zoneid) {
+        $api = clone $this->http;
+        $api->method = 'GET';
+        $api->url = "/servers/localhost/zones/$zoneid";
+        $api->call();
+
+        return $api->json;
+    }
+
+    public function savezone($zone) {
+        $api = clone $this->http;
+        // We have to split up RRSets and Zoneinfo.
+        // First, update the zone
+        $zonedata = $zone;
+        unset($zonedata['id']);
+        unset($zonedata['url']);
+        unset($zonedata['rrsets']);
+
+        if ($zone['serial'] == '') {
+            $api->method = 'POST';
+            $api->url = '/servers/localhost/zones';
+            $api->content = json_encode($zonedata);
+            $api->call();
+
+            return $api->json;
+        }
+        $api->method = 'PUT';
+        $api->url = $zone['url'];
+        $api->content = json_encode($zonedata);
+        $api->call();
+
+        // Then, update the rrsets
+        $api->method = 'PATCH';
+        $api->content = json_encode(Array('rrsets' => $zone['rrsets']));
+        $api->call();
+    }
+
+    public function deletezone($zoneid) {
+        $api = clone $this->http;
+        $api->method = 'DELETE';
+        $api->url = "/servers/localhost/zones/$zoneid";
+        $api->call();
+
+        return $api->json;
+    }
+}
+
+?>
diff --git a/includes/class/Zone.php b/includes/class/Zone.php
new file mode 100644
index 0000000..fc2f57e
--- /dev/null
+++ b/includes/class/Zone.php
@@ -0,0 +1,270 @@
+<?php
+
+class Zone {
+    public function __construct() {
+        $this->id = '';
+        $this->name = '';
+        $this->kind = '';
+        $this->url = '';
+        $this->serial = '';
+        $this->dnssec = '';
+        $this->soa_edit = '';
+        $this->soa_edit_api = '';
+        $this->account = '';
+        $this->nameservers = Array();
+        $this->rrsets = Array();
+        $this->masters = Array();
+    }
+
+    public function parse($data) {
+        $this->setid($data['id']);
+        $this->setname($data['name']);
+        $this->setkind($data['kind']);
+        $this->setdnssec($data['dnssec']);
+        $this->setaccount($data['account']);
+        $this->setserial($data['serial']);
+        $this->url = $data['url'];
+        $this->setsoaedit($data['soa_edit']);
+        $this->setsoaeditapi($data['soa_edit_api']);
+
+        foreach ($data['masters'] as $master) {
+            $this->addmaster($master);
+        }
+
+        foreach ($data['rrsets'] as $rrset) {
+            $toadd = new RRSet($rrset['name'], $rrset['type']);
+            foreach ($rrset['comments'] as $comment) {
+                $toadd->addComment($comment['content'], $comment['account'], $comment['modified_at']);
+            }
+            foreach ($rrset['records'] as $record) {
+                $toadd->addRecord($record['content'], $record['disabled']);
+            }
+            $toadd->setttl($rrset['ttl']);
+            array_push($this->rrsets, $toadd);        }
+    }
+
+    public function addnameserver($nameserver) {
+        foreach ($this->nameservers as $ns) {
+            if ($nameserver == $ns) {
+                throw new Exception("We already have this as a nameserver");
+            }
+        }
+        array_push($this->nameservers, $nameserver);
+
+    }
+
+    public function setserial($serial) {
+        $this->serial = $serial;
+    }
+
+    public function setsoaedit($soaedit) {
+        $this->soa_edit = $soaedit;
+    }
+
+    public function setsoaeditapi($soaeditapi) {
+        $this->soa_edit_api = $soaeditapi;
+    }
+    public function setname($name) {
+        $this->name = $name;
+    }
+
+    public function setkind($kind) {
+        $this->kind = $kind;
+    }
+
+    public function setaccount($account) {
+        $this->account = $account;
+    }
+
+    public function setdnssec($dnssec) {
+        $this->dnssec = $dnssec;
+    }
+
+    private function setid($id) {
+        $this->id = $id;
+    }
+
+    public function addmaster($ip) {
+        foreach ($this->masters as $master) {
+            if ($ip == $master) {
+                throw new Exception("We already have this as a master");
+            }
+        }
+        array_push($this->masters, $ip);
+    }
+
+    public function deleterrset($name, $type) {
+        foreach ($this->rrsets as $rrset) {
+            if ($rrset->name == $name and $rrset->type == $type) {
+                $rrset->delete();
+            }
+        }
+    }
+
+    public function setrrsetttl($name, $type, $ttl) {
+        foreach ($this->rrsets as $rrset) {
+            if ($rrset->name == $name and $rrset->type == $type) {
+                $rrset->setttl($ttl);
+            }
+        }   
+    }
+
+    public function addrrset($name, $type, $content, $ttl = 3600) {
+        foreach ($this->rrsets as $rrset) {
+            if ($rrset->name == $name and $rrset->type == $type) {
+                throw new Exception("This rrset already exists.");
+            }
+        }
+        $rrset = new RRSet($name, $type, $content, $ttl);
+        array_push($this->rrsets, $rrset);
+    }
+
+    public function addrecord($name, $type, $content, $disabled = false, $ttl = 3600) {
+        $found = FALSE;
+
+        foreach ($this->rrsets as $rrset) {
+            if ($rrset->name == $name and $rrset->type == $type) {
+                $rrset->addRecord($content, $disabled);
+                $found = TRUE;
+            }
+        }
+
+        if (!$found) {
+            throw new Exception("RRset does not exist for this record");
+        }
+    }
+
+    public function export() {
+        $ret = Array();
+        $ret['account'] = $this->account;
+        $ret['dnssec'] = $this->dnssec;
+        $ret['id'] = $this->id;
+        $ret['kind'] = $this->kind;
+        $ret['masters'] = $this->masters;
+        $ret['name'] = $this->name;
+        if (count($this->nameservers) > 0) {
+            $ret['nameservers'] = $this->nameservers;
+        }
+        $ret['rrsets'] = $this->export_rrsets();
+        $ret['serial'] = $this->serial;
+        $ret['soa_edit'] = $this->soa_edit;
+        $ret['soa_edit_api'] = $this->soa_edit_api;
+        $ret['url'] = $this->url;
+        
+        return $ret;
+    }
+
+    private function export_rrsets() {
+        $ret = Array();
+        foreach ($this->rrsets as $rrset) {
+            array_push($ret, $rrset->export());
+        }
+
+        return $ret;
+    }
+}
+
+class RRSet {
+    public function __construct($name = '', $type = '', $content = '', $ttl = 3600) {
+        $this->name = $name;
+        $this->type = $type;
+        $this->ttl  = $ttl;
+        $this->changetype = 'REPLACE';
+        $this->records = Array();
+        $this->comments = Array();
+
+        if (isset($content) and $content != '') {
+            $this->addRecord($content);
+        }
+    }
+
+    public function delete() {
+        $this->changetype = 'DELETE';
+    }
+
+    public function setttl($ttl) {
+        $this->ttl = $ttl;
+    }
+
+    public function addRecord($content, $disabled = false) {
+        foreach ($this->records as $record) {
+            if ($record->content == $content) {
+                throw Exception("Record already exists");
+            }
+        }
+
+        $record = new Record($content, $disabled);
+        array_push($this->records, $record);
+    }
+
+    public function addComment($content, $account, $modified_at = false) {
+        $comment = new Comment($content, $account, $modified_at);
+        array_push($this->comments, $comment);
+    }
+
+    public function export() {
+        $ret = Array();
+        $ret['comments'] = $this->export_comments();
+        $ret['name'] = $this->name;
+        $ret['records'] = $this->export_records();
+        if ($this->changetype != 'DELETE') {
+            $ret['ttl'] = $this->ttl;
+        }
+        $ret['type'] = $this->type;
+        $ret['changetype'] = $this->changetype;
+        return $ret;
+    }
+
+    private function export_records() {
+        $ret = Array();
+        foreach ($this->records as $record) {
+            array_push($ret, $record->export());
+        }
+
+        return $ret;
+    }
+
+    private function export_comments() {
+        $ret = Array();
+        foreach ($this->comments as $comment) {
+            array_push($ret, $comment->export());
+        }
+        
+        return $ret;
+    }
+
+}
+
+class Record {
+    public function __construct($content, $disabled = FALSE) {
+        $this->content = $content;
+        $this->disabled = $disabled;
+    }
+
+    public function export() {
+        $ret;
+
+        $ret['content'] = $this->content;
+        $ret['disabled'] = $this->disabled;
+
+        return $ret;
+    }
+}
+
+class Comment {
+    public function __construct($content, $account, $modified_at) {
+        $this->content = $content;
+        $this->account = $account;
+        $this->modified_at = $modified_at;
+    }
+
+    public function export() {
+        $ret;
+
+        $ret['content'] = $this->content;
+        $ret['account'] = $this->account;
+        $ret['modified_at'] = $this->modified_at;
+    }
+}
+
+?>