<?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
  ----------------------------------------------------------------------
*/

/**
 * bind9 file management class
 * 
 * @copyright AlternC-Team 2000-2017 https://alternc.com/
 */
class system_bind {
    var $ZONE_TEMPLATE ="/etc/alternc/templates/bind/templates/zone.template";
    var $NAMED_TEMPLATE ="/etc/alternc/templates/bind/templates/named.template";
    var $NAMED_CONF ="/var/lib/alternc/bind/automatic.conf";
    var $RNDC ="/usr/sbin/rndc";

    var $dkim_trusted_host_file = "/etc/opendkim/TrustedHosts";
    var $dkim_keytable_file = "/etc/opendkim/KeyTable";
    var $dkim_signingtable_file = "/etc/opendkim/SigningTable";

    var $cache_conf_db = array();
    var $cache_get_persistent = array();
    var $cache_zone_file = array();
    var $cache_domain_summary = array();
    var $zone_file_directory = '/var/lib/alternc/bind/zones/';


    /**
     * Return the part of the conf we got from the database
     * 
     * @global m_mysql $db
     * @param string $domain
     * @return array $this->cache_conf_db
     */
    function conf_from_db($domain=false) {
        global $db;
        // Use cache, fill cache if empty
        if (empty($this->cache_conf_db)) {
            $db->query("
        select 
          sd.domaine, 
          replace(replace(dt.entry,'%TARGET%',sd.valeur), '%SUB%', if(length(sd.sub)>0,sd.sub,'@')) as entry 
        from 
          sub_domaines sd,
          domaines_type dt 
        where 
          sd.type=dt.name 
          and sd.enable in ('ENABLE', 'ENABLED') 
        order by entry ;");
            $t=array();
            while ($db->next_record()) {
                $t[$db->f('domaine')][] = $db->f('entry');
            }
            $this->cache_conf_db = $t;
        }
        if ($domain) {
            if (isset($this->cache_conf_db[$domain])) {
                return $this->cache_conf_db[$domain];
            } else {
                return array();
            }
        } // if domain
        return $this->cache_conf_db;
    }


    /**
     * Return full path of the zone configuration file
     * 
     * @param string $domain
     * @return string
     */
    function get_zone_file_uri($domain) {
        return $this->zone_file_directory.$domain;
    }


    /**
     * 
     * @param string $domain
     * @return string zone file path
     */
    function get_zone_file($domain) {
        // Use cache, fill cache if empty
        if (!isset($this->cache_zone_file[$domain]) ) {
            if (file_exists($this->get_zone_file_uri($domain))) {
                $this->cache_zone_file[$domain] = @file_get_contents($this->get_zone_file_uri($domain));
            } else {
                $this->cache_zone_file[$domain] = false;
            }
        }
        return $this->cache_zone_file[$domain] ;
    }


    /**
     * 
     * @param string $domain
     * @return string 
     */
    function get_serial($domain) {
        // Return the next serial the domain must have.
        // Choose between a generated and an incremented.
    
        // Calculated :
        $calc = date('Ymd').'00'."\n";

        // Old one :
        $old=$calc; // default value
        $file = $this->get_zone_file($domain);
        preg_match_all("/\s*(\d{10})\s+\;\sserial\s?/", $file, $output_array);
        if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
            $old = $output_array[1][0];
        }

        // Return max between newly calculated, and old one incremented
        return max(array($calc,$old)) + 1 ;
    }


    /**
     * Return lines that are after ;;; END ALTERNC AUTOGENERATE CONFIGURATION
     * 
     * @param string $domain
     * @return string
     */
    function get_persistent($domain) {
        if ( ! isset($this->cache_get_persistent[$domain] )) {
            preg_match_all('/\;\s*END\sALTERNC\sAUTOGENERATE\sCONFIGURATION(.*)/s', $this->get_zone_file($domain), $output_array);
            if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
                $this->cache_get_persistent[$domain] = $output_array[1][0];
            } else {
                $this->cache_get_persistent[$domain] = false;
            }
        } // isset
        return $this->cache_get_persistent[$domain];
    }
  

    /**
     * 
     * @return string 
     */
    function get_zone_header() {
        return file_get_contents($this->ZONE_TEMPLATE);
    }
  

    /**
     * 
     * @global m_dom $dom
     * @param string $domain
     * @return array Retourne un tableau 
     */
    function get_domain_summary($domain=false) {
        global $dom;

        // Use cache if is filled, if not, fill it
        if (empty($this->cache_domain_summary)) {
            $this->cache_domain_summary = $dom->get_domain_all_summary();
        }

        if ($domain) return $this->cache_domain_summary[$domain];
        else return $this->cache_domain_summary;
    }


    /**
     * 
     * @param string $domain
     * @return boolean
     */
    function dkim_delete($domain) {
        $target_dir = "/etc/opendkim/keys/$domain";
        if (file_exists($target_dir)) {
            @unlink("$target_dir/alternc_private");
            @unlink("$target_dir/alternc.txt");
            @rmdir($target_dir);
        }
        return true;
    }


    /**
     * Generate the domain DKIM key
     * 
     * @param string $domain
     * @return null|boolean
     */
    function dkim_generate_key($domain) {
        // Stop here if we do not manage the mail
        $domainInfo = $this->get_domain_summary($domain);
        if ( !  $domainInfo['gesmx'] ) return;

        $target_dir = "/etc/opendkim/keys/$domain";

        if (file_exists($target_dir.'/alternc.txt')) return; // Do not generate if exist

        if (! is_dir($target_dir)) mkdir($target_dir); // create dir

        // Generate the key
        $old_dir=getcwd();
        chdir($target_dir);
        exec('opendkim-genkey -r -d '.escapeshellarg($domain).' -s "alternc" ');
        chdir($old_dir);

        // opendkim must be owner of the key
        chown("$target_dir/alternc.private", 'opendkim');
        chgrp("$target_dir/alternc.private", 'opendkim');

        return true; // FIXME handle error
    }


    /**
     * Refresh DKIM configuration: be sure to list the domain having a private key (and only them)
     */
    function dkim_refresh_list() { 
        // so ugly... but there is only 1 pass, not 3. Still ugly.
        $trusted_host_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n";
        $keytable_new     = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n";
        $signingtable_new = "# WARNING: this file is auto generated by AlternC.\n# Add your changes after the last line\n";

        # Generate automatic entry
        foreach ($this->get_domain_summary() as $domain => $ds ) {
            // Skip if delete in progress, or if we do not manage dns or mail
            if ( ! $ds['gesdns'] || ! $ds['gesmx'] || strtoupper($ds['dns_action']) == 'DELETE' ) continue;

            // Skip if there is no key generated
            if (! file_exists("/etc/opendkim/keys/$domain/alternc.txt")) continue; 

            // Modif the files.
            $trusted_host_new.="$domain\n";
            $keytable_new    .="alternc._domainkey.$domain $domain:alternc:/etc/opendkim/keys/$domain/alternc.private\n";
            $signingtable_new.="$domain alternc._domainkey.$domain\n";
        }
        $trusted_host_new.="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n";
        $keytable_new    .="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n";
        $signingtable_new.="# END AUTOMATIC FILE. ADD YOUR CHANGES AFTER THIS LINE\n";

        # Get old files
        $trusted_host_old=@file_get_contents($this->dkim_trusted_host_file);
        $keytable_old    =@file_get_contents($this->dkim_keytable_file);
        $signingtable_old=@file_get_contents($this->dkim_signingtable_file);
    
        # Keep manuel entry
        preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $trusted_host_old, $output_array);
        if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
            $trusted_host_new.=$output_array[1][0];
        } 
        preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $keytable_old, $output_array);
        if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
            $keytable_new.=$output_array[1][0];
        } 
        preg_match_all('/\#\s*END\ AUTOMATIC\ FILE\.\ ADD\ YOUR\ CHANGES\ AFTER\ THIS\ LINE(.*)/s', $signingtable_old, $output_array);
        if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
            $signingtable_new.=$output_array[1][0];
        } 
    
        // Save if there are some diff
        if ( $trusted_host_new != $trusted_host_old ) {
            file_put_contents($this->dkim_trusted_host_file, $trusted_host_new);
        }
        if ( $keytable_new != $keytable_old ) {
            file_put_contents($this->dkim_keytable_file, $keytable_new);
        }
        if ( $signingtable_new != $signingtable_old ) {
            file_put_contents($this->dkim_signingtable_file, $signingtable_new);
        }

    }


    /**
     * 
     * @param string $domain
     * @return string
     */
    function dkim_entry($domain) {
        $keyfile="/etc/opendkim/keys/$domain/alternc.txt";
        $domainInfo         = $this->get_domain_summary($domain);
        if (! file_exists($keyfile) &&  $domainInfo['gesmx'] ) {
            $this->dkim_generate_key($domain);
        }
        return @file_get_contents($keyfile);
    }


    /**
     * Conditionnal generation autoconfig entry for outlook / thunderbird
     * If entry with the same name allready exist, skip it.
     * 
     * @param string $domain
     * @return string
     */
    function mail_autoconfig_entry($domain) {
        $zone= implode("\n",$this->conf_from_db($domain))."\n".$this->get_persistent($domain);

        $entry='';
        $domainInfo                 = $this->get_domain_summary($domain);
        if ( $domainInfo['gesmx'] ) {
            // If we manage the mail

            // Check if there is no the same entry (defined or manual)
            // can be toto IN A or toto.fqdn.tld. IN A
            if (! preg_match("/autoconfig(\s|\.".str_replace('.','\.',$domain)."\.)/", $zone )) {
                $entry.="autoconfig IN CNAME %%fqdn%%.\n";
            }
            if (! preg_match("/autodiscover(\s|\.".str_replace('.','\.',$domain)."\.)/", $zone )) {
                $entry.="autodiscover IN CNAME %%fqdn%%.\n";
            }
        } // if gesmx
        return $entry;
    }
  
  
    /**
     * 
     * Return a fully generated zone
     * 
     * @global string $L_FQDN
     * @global string $L_NS1_HOSTNAME
     * @global string $L_NS2_HOSTNAME
     * @global string $L_DEFAULT_MX
     * @global string $L_DEFAULT_SECONDARY_MX
     * @global string $L_PUBLIC_IP
     * @param string $domain
     * @return string
     */
    function get_zone($domain) {
        global $L_FQDN, $L_NS1_HOSTNAME, $L_NS2_HOSTNAME, $L_DEFAULT_MX, $L_DEFAULT_SECONDARY_MX, $L_PUBLIC_IP;

        $zone =$this->get_zone_header();
        $zone.=implode("\n",$this->conf_from_db($domain));
        $zone.="\n;;;HOOKED ENTRY\n";

        $zone.= $this->dkim_entry($domain);
        $zone.= $this->mail_autoconfig_entry($domain);

        $zone.="\n;;; END ALTERNC AUTOGENERATE CONFIGURATION\n";
        $zone.=$this->get_persistent($domain);
        $domainInfo = $this->get_domain_summary($domain);

        // FIXME check those vars
        $zone = strtr($zone, 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@@"=>$this->get_serial($domain),
            "@@PUBLIC_IP@@"=>"$L_PUBLIC_IP",
            "@@ZONETTL@@"=> $domainInfo['zonettl'],
        ));

        return $zone;
    }


    /**
     * 
     * @param string $domain
     */
    function reload_zone($domain) {
        exec($this->RNDC." reload ".escapeshellarg($domain), $output, $return_value);
        if ($return_value != 0 ) {
            echo "ERROR: Reload zone failed for zone $domain\n";
        }
    }


    /**
     * return true if zone is locked
     * 
     * @param string $domain
     * @return boolean
     */
    function is_locked($domain) {
        preg_match_all("/(\;\s*LOCKED:YES)/i", $this->get_zone_file($domain), $output_array);
        if (isset($output_array[1][0]) && !empty($output_array[1][0])) {
            return true;
        }
        return false;
    }  


    /**
     * 
     * @global m_mysql $db
     * @global m_dom $dom
     * @param string $domain
     * @return boolean
     */
    function save_zone($domain) {
        global $db, $dom;

        // Do not save if the zone is LOCKED
        if ( $this->is_locked($domain)) {
            $dom->set_dns_result($domain, "The zone file of this domain is locked. Contact your administrator."); // If edit, change dummy_for_translation
            $dom->set_dns_action($domain, 'OK');
            return false;
        }
 
        // Save file, and apply chmod/chown
        $file=$this->get_zone_file_uri($domain);
        file_put_contents($file, $this->get_zone($domain));
        chown($file, 'bind');
        chmod($file, 0640);

        $dom->set_dns_action($domain, 'OK');
        return true; // fixme add tests
    }


    /**
     * Delete the zone configuration file
     * 
     * @param string $domain
     * @return boolean
     */
    function delete_zone($domain) {
        $file=$this->get_zone_file_uri($domain);
        if (file_exists($file)) {
            unlink($file);
        }
        $this->dkim_delete($domain);
        return true;
    }


    /**
     * 
     * @global m_hooks $hooks
     * @return boolean
     */
    function reload_named() {
        global $hooks;
        // Generate the new conf file
        $new_named_conf="// DO NOT EDIT\n// This file is generated by Alternc.\n// Every changes you'll make will be overwrited.\n";
        $tpl=file_get_contents($this->NAMED_TEMPLATE);
        foreach ($this->get_domain_summary() as $domain => $ds ) {
            if ( ! $ds['gesdns'] || strtoupper($ds['dns_action']) == 'DELETE' ) continue;
            $new_named_conf.=strtr($tpl, array("@@DOMAINE@@"=>$domain, "@@ZONE_FILE@@"=>$this->get_zone_file_uri($domain)));
        }

        // Get the actual conf file
        $old_named_conf = @file_get_contents($this->NAMED_CONF);

        // Apply new configuration only if there are some differences
        if ($old_named_conf != $new_named_conf ) {
            file_put_contents($this->NAMED_CONF,$new_named_conf);
            chown($this->NAMED_CONF, 'bind');
            chmod($this->NAMED_CONF, 0640);
            exec($this->RNDC." reconfig");
            $hooks->invoke_scripts("/usr/lib/alternc/reload.d", array('dns_reconfig')  );
        }

        return true;
    }


    /**
     * Regenerate bind configuration and load it
     * 
     * @global m_hooks $hooks
     * @param boolean $all
     * @return boolean
     */
    function regenerate_conf($all=false) {
        global $hooks;

        foreach ($this->get_domain_summary() as $domain => $ds ) {
            if ( ! $ds['gesdns'] && strtoupper($ds['dns_action']) == 'OK' ) continue; // Skip if we do not manage DNS and is up-to-date for this domain

            if ( (strtoupper($ds['dns_action']) == 'DELETE' ) || 
            (strtoupper($ds['dns_action']) == 'UPDATE' && $ds['gesdns']==false ) // in case we update the zone to disable DNS management
            ) { 
                $this->delete_zone($domain);
                continue;
            }

            if ( ( $all || strtoupper($ds['dns_action']) == 'UPDATE' ) && $ds['gesdns'] ) {
                $this->save_zone($domain);
                $this->reload_zone($domain);
                $hooks->invoke_scripts("/usr/lib/alternc/reload.d", array('dns_reload_zone', $domain)  );
            }
        } // end foreach domain

        $this->dkim_refresh_list();
        $this->reload_named();
        return true;
    }


    /**
     * 
     */
    private function dummy_for_translation() {
        _("The zone file of this domain is locked. Contact your administrator.");
    }


} /* Class system_bind */