<?php

/*
  ----------------------------------------------------------------------
  LICENSE

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License (GPL)
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  To read the license please visit http://www.gnu.org/copyleft/gpl.html
  ----------------------------------------------------------------------
*/

/**
 * Manages BIND 9+ zone management templates in AlternC 3.5+
 * 
 * @copyright AlternC-Team 2000-2018 https://alternc.com/
 */
class m_bind {

    var $shouldreload;
    var $shouldreconfig;
    
    var $ZONE_TEMPLATE ="/etc/alternc/templates/bind/templates/zone.template";
    var $NAMED_TEMPLATE = "/etc/alternc/templates/bind/templates/named.template";
    var $NAMED_DNSSEC_TEMPLATE = "/etc/alternc/templates/bind/templates/named.dnssec.template";
    var $NAMED_CONF ="/var/lib/alternc/bind/automatic.simple.conf";
    var $NAMED_DNSSEC_CONF ="/var/lib/alternc/bind/automatic.dnssec.conf";
    var $RNDC ="/usr/sbin/rndc";

    var $zone_file_directory = '/var/lib/alternc/bind/zones';

    // ------------------------------------------------------------
    /** Hook launched before any action by updatedomains 
     * initialize the reload/reconfig flags used by POST
     * @NOTE launched as ROOT 
     */
    function hook_updatedomains_dns_pre() {
        global $msg;

        $this->shouldreload=false;
        $this->shouldreconfig=false;

        //Prevent some race condition with dynamic zone (as dns-sec behavior)
        $ret=0;
        exec($this->RNDC." sync -clean 2>&1",$out,$ret);
        if ($ret!=0) {
            $msg->raise("ERROR","bind","Error while sync bind, error code is $ret\n".implode("\n",$out));
        } else {
            $msg->raise("INFO","bind","Bind sync done");
        }

        $ret=0;
        exec($this->RNDC." freeze 2>&1",$out,$ret);
        if ($ret!=0) {
            $msg->raise("ERROR","bind","Error while freeze bind, error code is $ret\n".implode("\n",$out));
        } else {
            $msg->raise("INFO","bind","Bind freeze done");
        }
    }


    // ------------------------------------------------------------
    /**
     * Hook launched for each ZONE for which we want a zone update (or create)
     * update the zone, create it if necessary, 
     * and ask for reload or reconfig of bind9 depending on what happened
     * @NOTE launched as ROOT 
     */
    function hook_updatedomains_dns_add($dominfo) {
        global $L_FQDN,$L_NS1_HOSTNAME,$L_NS2_HOSTNAME,$L_DEFAULT_MX,$L_DEFAULT_SECONDARY_MX,$L_PUBLIC_IP,$L_PUBLIC_IPV6;
        global $hooks;

        $domain = $dominfo["domaine"];
        $ttl = $dominfo["zonettl"];

        // does it already exist?
        if (file_exists($this->zone_file_directory."/".$domain)) {
            list($islocked,$serial,$more)=$this->read_zone($domain);
            $serial++; // only increment serial for new zones
        } else {
            $more="";
            $serial=date("Ymd")."00";
            $islocked=false;
        }
        if ($islocked) return 1;

        // Prepare a new zonefile from a template
        $zone = file_get_contents($this->ZONE_TEMPLATE);

        // add the SUBDOMAIN entries
        $zone .= $this->conf_from_db($domain);

        // substitute ALTERNC & domain variables
        $tokens = array();
        $user_tokens = $hooks->invoke("hook_bind_dns_tokens", array($domain));
        foreach ($user_tokens as $c => $result) {
            if (is_array($result)) {
                // The first hook to define a token will have it's definition kept,
                // unless it's for a "default token".
                $tokens = array_merge($result, $tokens);
            }
        }
        $default_tokens = array(
            "%%fqdn%%" => "$L_FQDN",
            "%%ns1%%" => "$L_NS1_HOSTNAME",
            "%%ns2%%" => "$L_NS2_HOSTNAME",
            "%%DEFAULT_MX%%" => "$L_DEFAULT_MX",
            "%%DEFAULT_SECONDARY_MX%%" => "$L_DEFAULT_SECONDARY_MX",
            "@@fqdn@@" => "$L_FQDN",
            "@@ns1@@" => "$L_NS1_HOSTNAME",
            "@@ns2@@" => "$L_NS2_HOSTNAME",
            "@@DEFAULT_MX@@" => "$L_DEFAULT_MX",
            "@@DEFAULT_SECONDARY_MX@@" => "$L_DEFAULT_SECONDARY_MX",
            "@@DOMAINE@@" => $domain,
            "@@SERIAL@@" => $serial,
            "@@PUBLIC_IP@@" => "$L_PUBLIC_IP",
            "@@PUBLIC_IPV6@@" => "$L_PUBLIC_IPV6",
            "@@ZONETTL@@" => $ttl
        );
        # Preserve the default tokens before all.
        $tokens = array_merge($tokens, $default_tokens);
        $zone = strtr($zone, $tokens);

        // add the "END ALTERNC CONF line";
        $zone .= ";;; END ALTERNC AUTOGENERATE CONFIGURATION\n";

        // add the manually entered info:
        $zone .= $more;
        file_put_contents($this->zone_file_directory."/".$domain,$zone);

        // add the line into bind9 conf:
        if (add_line_to_file(
            $this->NAMED_CONF,
            trim(strtr(
                file_get_contents($this->NAMED_TEMPLATE),
                array(
                    "@@DOMAIN@@" => $domain,
                    "@@ZONE_FILE@@" => $this->zone_file_directory."/".$domain
                )
            ))) &&
            add_line_to_file(
            $this->NAMED_DNSSEC_CONF,
            trim(strtr(
                file_get_contents($this->NAMED_DNSSEC_TEMPLATE),
                array(
                    "@@DOMAIN@@" => $domain,
                    "@@ZONE_FILE@@" => $this->zone_file_directory."/".$domain
                )
            )))
        ) {
            $this->shouldreconfig=true;
        } else {
            $this->shouldreload=true;
        }
        return 0;
    }


    // ------------------------------------------------------------
    /** 
     * Hook launched for each ZONE for which we want a zone DELETE
     * remove the zone and its file,
     * and if any action happened, ask for bind RECONFIG at posttime
     * @NOTE launched as ROOT 
     */
    function hook_updatedomains_dns_del($dominfo) {
        global $msg;

        $deleted = false;
        $domain = $dominfo["domaine"];

        if (del_line_from_file(
            $this->NAMED_CONF,
            trim(strtr(
                file_get_contents($this->NAMED_TEMPLATE),
                array(
                    "@@DOMAIN@@" => $domain,
                    "@@ZONE_FILE@@" => $this->zone_file_directory."/".$domain
                )
            )))
        ) {
            $deleted=true;
        }

        if (del_line_from_file(
            $this->NAMED_DNSSEC_CONF,
            trim(strtr(
                file_get_contents($this->NAMED_DNSSEC_TEMPLATE),
                array(
                    "@@DOMAIN@@" => $domain,
                    "@@ZONE_FILE@@" => $this->zone_file_directory."/".$domain
                )
            )))
        ) {
            $deleted=true;
        }

        if ($deleted) {
            $this->shouldreconfig=true;
            @unlink($this->zone_file_directory."/".$domain);
            exec($this->RNDC." delzone -clean ".$domain." 2>&1",$out,$ret);
            if ($ret!=0) {
                $msg->raise("ERROR","bind","Error while deleting zone, error code is $ret\n".implode("\n",$out));
            } else {
                $msg->raise("INFO","bind","Bind zone deletion done");
            }
        }
        return 0;
    }

    
    // ------------------------------------------------------------
    /** 
     * Hook function launched at the very end of updatedomains 
     * here, we just reload OR reconfig (or both) bind9 depending 
     * on what happened before.
     * @NOTE launched as ROOT 
     */ 
    function hook_updatedomains_dns_post() {
        global $msg;

        $ret=0;
        exec($this->RNDC." thaw 2>&1",$out,$ret);
        if ($ret!=0) {
            $msg->raise("ERROR","bind","Error while thawing bind, error code is $ret\n".implode("\n",$out));
        } else {
            $msg->raise("INFO","bind","Bind thaw done");
        }

        if ($this->shouldreload) {
            $ret=0;
            exec($this->RNDC." reload 2>&1",$out,$ret);
            if ($ret!=0) {
                $msg->raise("ERROR","bind","Error while reloading bind, error code is $ret\n".implode("\n",$out));
            } else {
                $msg->raise("INFO","bind","Bind reloaded");
            }
        }
        if ($this->shouldreconfig) {
            $ret=0;
            exec($this->RNDC." reconfig 2>&1",$out,$ret);
            if ($ret!=0) {
                $msg->raise("ERROR","bind","Error while reconfiguring bind, error code is $ret\n".implode("\n",$out));
            } else {
                $msg->raise("INFO","bind","Bind reconfigured");
            }
        }
    }

    
    // ------------------------------------------------------------
    /** 
     * read a zone file for $domain, 
     * @param $domain string the domain name 
     * @return array with 3 informations: 
     * is the domain locked? (boolean), what's the current serial (integer), the data after alternc conf (string of lines)
     */
    function read_zone($domain) {
        $f=fopen($this->zone_file_directory."/".$domain,"rb");
        $islocked=false;
        $more="";
        $serial=date("Ymd")."00";
        while ($s=fgets($f,4096)) {
            if (preg_match("#\;\s*LOCKED:YES#i",$s)) {
                $islocked=true;
            }
            if (preg_match("/\s*(\d{10})\s+\;\sserial\s?/", $s,$mat)) {
                $serial=$mat[1];
            }
            if (preg_match('/\;\s*END\sALTERNC\sAUTOGENERATE\sCONFIGURATION(.*)/s', $s)) {
                break;
            }
        }
        while ($s=fgets($f,4096)) {
            $more.=$s;
        }
        return array($islocked,$serial,$more);
    }


    // ------------------------------------------------------------
    /**
     * Return the part of the conf we got from the sub_domaines table
     * @global m_mysql $db
     * @param string $domain
     * @return string a zonefile excerpt
     */
    function conf_from_db($domain) {
        global $db;
        $db->query("
        SELECT 
          REPLACE(REPLACE(dt.entry,'%TARGET%',sd.valeur), '%SUB%', if(length(sd.sub)>0,sd.sub,'@')) AS ENTRY,
          dt.target AS TARGET,
          dt.entry  AS ORIGINAL_ENTRY,
          sd.valeur AS VALEUR,
          if(length(sd.sub)>0,sd.sub,'@') AS SUB
        FROM 
          sub_domaines sd,
          domaines_type dt 
        WHERE 
          sd.type=dt.name
          AND sd.enable IN ('ENABLE', 'ENABLED')
          AND sd.web_action NOT IN ('DELETE')
          AND sd.domaine = ?
        ORDER BY ENTRY ;", array($domain));
        $t="";
        while ($db->next_record()) {
            // TXT entries may be longer than 255 characters, but need
            // special treatment. @see https://kb.isc.org/docs/aa-00356
            if (strlen($db->f('VALEUR')) >= 256 && $db->f('TARGET') == 'TXT') {
                $chunks = str_split($db->f('VALEUR'), 255);
                if ($chunks !== FALSE) {
                    $new_entry = '';
                    foreach ($chunks as $chunk) {
                        $new_entry .= '"' . $chunk . '" ';
                    }
                    $new_entry = trim($new_entry, ' ');
                    $entry = strtr($db->f('ORIGINAL_ENTRY'), array(
                        '%SUB%' => $db->f('SUB'),
                        // Don't want extra double quotes in this case
                        '"%TARGET%"' => $new_entry,
                    ));
                }
                else {
                    $entry = $db->f('ENTRY');
                }
            }
            else {
                $entry = $db->f('ENTRY');
            }
            $t.= $entry . "\n";
        }
        return $t;
    }

    
} // m_bind

