<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
  Copyright (C) 2015-2018  FusionDirectory

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License 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.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/

/* DnsRecords edition showing only records for our host */
class DnsRecordsFilteredAttribute extends DnsRecordsAttribute
{
  private static $templateValueSeparator = '^';

  function __construct ($label, $description, $ldapName, $zoneDn, $defaultValue = [], $acl = "")
  {
    parent::__construct($label, $description, $ldapName, FALSE, $defaultValue, $acl);
    $this->zoneDn = $zoneDn;
  }

  protected function loadAttrValue (array $attrs)
  {
  }

  function setParent (&$plugin)
  {
    parent::setParent($plugin);
    if (is_object($this->plugin)) {
      if (!$this->plugin->is_template) {
        $this->loadRecords();
      } else {
        $this->value = [];
        $this->loadAdditionalTemplatesValues();
      }
    }
  }

  function loadAdditionalTemplatesValues ()
  {
    global $config;

    $ldap = $config->get_ldap_link();
    $ldap->cat($this->getZoneDn());
    $attrs = $ldap->fetch();
    $this->zoneName = $attrs['zoneName'][0];
    $this->loadReverseZones($attrs);

    if (isset($this->plugin->attrs[$this->getLdapName()]['count'])) {
      for ($i = 0; $i < $this->plugin->attrs[$this->getLdapName()]['count']; $i++) {
        $this->value[] = explode(static::$templateValueSeparator, $this->plugin->attrs[$this->getLdapName()][$i]);
      }
    }
  }

  function loadRecords ()
  {
    global $config;

    $ldap = $config->get_ldap_link();
    $ldap->cat($this->getZoneDn());
    $attrs = $ldap->fetch();
    $this->zoneName = $attrs['zoneName'][0];
    parent::loadAttrValue($attrs);
    $this->initialValue = $this->getValue();
  }

  protected function getAttributeArrayValue ($key, $value)
  {
    $values = parent::getAttributeArrayValue($key, $value);
    if ($this->plugin->is_template) {
      return $values;
    }
    $ips    = $this->plugin->parent->getBaseObject()->ipHostNumber;
    $cn     = preg_replace('/\$$/', '', $this->plugin->parent->getBaseObject()->cn);
    if (
      in_array($values[2], $ips) ||
      (
        !empty($cn) &&
        (
          ($values[0] == $cn) ||
          preg_match('/^'.preg_quote($cn.'.'.$this->getZoneName(), '/i').'$/', $value[2]) ||
          preg_match('/ '.preg_quote($cn.'.'.$this->getZoneName(), '/i').'$/', $value[2])
        )
      )
    ) {
      return $values;
    } else {
      return [];
    }
  }

  function fillLdapValue (array &$attrs)
  {
    if ($this->plugin->is_template) {
      $attrs[$this->getLdapName()] = array_map(
        function ($v)
        {
          return implode(static::$templateValueSeparator, $v);
        },
        $this->value
      );
    }
  }

  /* Special LDAP treatment that this attribute does after plugin ldap save */
  function postLdapSave ($ldap): array
  {
    global $ui;

    if ($this->plugin->is_template) {
      return [];
    }

    /* Check locking */
    if ($locks = Lock::get($this->getZoneDn())) {
      return [sprintf(_('"%s" is locked (by "%s" since %s), could not save modifications to this DNS zone'), $locks[0]->objectDn, $locks[0]->userDn, date('%Y-%m-%d, %H:%M:%S', $locks[0]->timestamp))];
    } else {
      /* Lock the current entry */
      Lock::add($this->getZoneDn());
    }
    $zoneTabs = objects::open($this->getZoneDn(), 'dnsZone');
    $zoneTabs->getBaseObject()->attributesAccess['dnsRecords']->setValue($this->getValue());
    $errors = $zoneTabs->save();

    Lock::deleteByObject($this->getZoneDn());

    return $errors;
  }
}

class dnsHost extends simplePlugin
{
  protected $displayHeader  = TRUE;

  protected $zonesCache;
  protected $loaded = FALSE;

  static function plInfo (): array
  {
    return [
      'plShortName'     => _('DNS'),
      'plDescription'   => _('Edit the DNS zones of a system'),
      'plIcon'          => 'geticon.php?context=applications&icon=dns&size=48',
      'plSmallIcon'     => 'geticon.php?context=applications&icon=dns&size=16',
      'plObjectClass'   => ['fdDNSHost'],
      'plObjectType'    => ['server','workstation','terminal','printer','component','phone','mobilePhone'],
      'plPriority'      => 5,
      'plForeignKeys'  => [
        'fdDNSZoneDn' => 'dnsZone'
      ],

      'plProvidedAcls'  => parent::generatePlProvidedAcls(static::getAttributesInfo())
    ];
  }

  static function getAttributesInfo (): array
  {
    return [
      'zones' => [
        'name'  => _('DNS zones'),
        'attrs' => [
          new SetAttribute(
            new SelectAttribute(
              '', _('DNS zones for this host'),
              'fdDNSZoneDn', TRUE
            )
          )
        ]
      ],
      'soa' => [
        'name'  => _('SOA records'),
        'attrs' => []
      ],
      'records' => [
        'name'  => _('DNS Records'),
        'class' => ['fullwidth'],
        'attrs' => []
      ],
    ];
  }

  function __construct ($dn = NULL, $object = NULL, $parent = NULL, $mainTab = FALSE)
  {
    global $config;
    parent::__construct($dn, $object, $parent, $mainTab);
    $this->reloadZoneChoices();
  }

  protected function reloadZoneChoices ()
  {
    global $config;

    /* List DNS zones we can read */
    $this->zonesCache = objects::ls('dnsZone', NULL, NULL, '', TRUE);
    /* Check if some values are missing */
    $dnsZones = $this->fdDNSZoneDn;
    foreach ($dnsZones as $dn) {
      if (!isset($this->zonesCache[$dn])) {
        try {
          $zone = objects::ls('dnsZone', NULL, $dn);
          $this->zonesCache = array_merge($zone, $this->zonesCache);
        } catch (FusionDirectoryException $e) {
          continue;
        }
      }
    }
    $counts = array_count_values($this->zonesCache);
    foreach ($this->zonesCache as $dn => &$name) {
      if ($counts[$name] > 1) {
        $name .= ' ('.preg_replace(['/^[^,]+,/', '/,'.preg_quote($config->current['BASE']).'$/'], '', $dn).')';
      }
    }
    unset($name);
    $this->attributesAccess['fdDNSZoneDn']->attribute->setChoices(array_keys($this->zonesCache), array_values($this->zonesCache));
    $this->attributesAccess['fdDNSZoneDn']->setSubmitForm('zoneChanged');
  }

  function zoneChanged ()
  {
    global $config;
    $ldap     = $config->get_ldap_link();
    $dnsZones = $this->fdDNSZoneDn;
    /* Removes references to old attributes */
    if (isset($this->attributesInfo['soa'])) {
      foreach (array_keys($this->attributesInfo['records']['attrs']) as $oldattr) {
        $this->removeAttribute('records', $oldattr);
      }
      foreach (array_keys($this->attributesInfo['soa']['attrs']) as $oldattr) {
        $this->removeAttribute('soa', $oldattr);
      }
    }
    /* Remove sections or fill them */
    if (empty($dnsZones)) {
      unset($this->attributesInfo['soa']);
      unset($this->attributesInfo['records']);
    } else {
      if (is_object($this->parent)) {
        $cn = $this->parent->getBaseObject()->cn;
      } elseif (isset($this->attrs['cn'][0])) {
        /* When called by __construct, parent is not set yet */
        $cn = $this->attrs['cn'][0];
      } else {
        $cn = '';
      }
      $cn = preg_replace('/\$$/', '', $cn);
      $this->attributesInfo['soa'] = [
        'name'  => _('Primary servers'),
        'attrs' => []
      ];
      $this->attributesInfo['records'] = [
        'name'  => _('DNS Records'),
        'class' => ['fullwidth'],
        'attrs' => []
      ];
      foreach ($dnsZones as $dn) {
        if (isset($this->zonesCache[$dn])) {
          $name   = $this->zonesCache[$dn];
          $zoneid = preg_replace('/[\/\-,.#:; =]/', '_', $dn);
        } else {
          continue;
        }
        $attrId = 'zoneRecords'.$zoneid;
        $this->addAttribute(
          'records',
          new DnsRecordsFilteredAttribute(
            $name, sprintf(_('The DNS records for zone "%s"'), $name),
            $attrId, $dn,
            [], 'dnsRecords'
          )
        );

        /* Fetch SOA record */
        $ldap->cat($dn);
        if ($attrs = $ldap->fetch()) {
          list ($fqdn) = explode(' ', $attrs['sOARecord'][0]);
          $servers = dnsManagement::findServerByFQDN($fqdn, $dn);
          if (isset($servers[$this->dn])) {
            $fqdn = '<strong>'.$fqdn.'</strong>';
            if (class_available('supportDaemon')) {
              $id = 'ldap2zone_refresh_'.base64_encode($attrs['zoneName'][0]);
              $fqdn .= ' <input type="submit" formnovalidate="formnovalidate" name="'.$id.'" id="'.$id.'" value="'._('Refresh zone file').'"/>';
            }
          }
          $attrId = 'soaRecord'.$zoneid;
          $this->addAttribute(
            'soa',
            new DisplayAttribute(
              $name, sprintf(_('The primary server for zone "%s"'), $name),
              $attrId, FALSE,
              $fqdn
            )
          );
          $this->attributesInfo['soa']['attrs'][$attrId]->setAllowHTML(TRUE);
        }
      }
    }
  }

  function load ()
  {
    if (!$this->loaded) {
      $this->zoneChanged();
      $this->loaded = TRUE;
    }
  }

  public function update (): bool
  {
    parent::update();
    $this->load();
    return TRUE;
  }

  public function render (): string
  {
    $smarty = get_smarty();
    $ui     = get_userinfo();
    $acl    = $ui->get_permissions($this->getAclBase(), 'dnsZone/dnsZone', 'dnsRecords', $this->read_only);
    $smarty->assign('dnsRecordsACL', $acl);
    return parent::render();
  }

  public function readPost ()
  {
    $this->load();
    parent::readPost();
    if (isset($_POST[get_class($this).'_posted'])) {
      foreach ($_POST as $name => $value) {
        if (preg_match('/^ldap2zone_refresh_(.+)$/', $name, $m)) {
          try {
            $zone = base64_decode($m[1]);
            if (!$this->parent->by_object['argonautClient']->isActiveOrInherited()) {
              throw new SimplePluginError($this, htmlescape(_('Argonaut client needs to be activated to use ldap2zone remotely')));
            }
            $s_daemon = new supportDaemon();
            if ($s_daemon->is_error()) {
              throw new SimplePluginError($this, msgPool::siError($s_daemon->get_error()));
            }
            $target = $this->parent->getBaseObject()->macAddress;
            if (is_array($target)) {
              if (count($target) === 0) {
                throw new SimplePluginError($this, _('Cannot use argonaut client on a system without a Mac address'));
              }
              $target = $target[0];
            }
            $s_daemon->append_call('Ldap2Zone.start', $target, ['args' => [$zone]]);
            if ($s_daemon->is_error()) {
              throw new SimplePluginError($this, msgPool::siError($s_daemon->get_error()));
            }
            msg_dialog::display(_('Ldap2zone'), htmlescape(sprintf(_('Ldap2Zone called for zone "%s"'), $zone)), INFO_DIALOG);
          } catch (SimplePluginError $error) {
            $error->display();
          }
        }
      }
    }
  }

  function save (): array
  {
    global $config, $ui;

    $errors = parent::save();

    foreach ($this->attributesInfo['records']['attrs'] as &$attr) {
      if ($attr->hasChanged()) {
        $errors = array_merge(
          $errors,
          $attr->postLdapSave($config->get_ldap_link())
        );
      }
    }
    unset($attr);

    if (!empty($errors)) {
      return $errors;
    }

    /* Update records if IP or CN changed */
    if ($this->initially_was_account) {
      $baseObject = $this->parent->getBaseObject();
      $messages   = [];
      $oldcn      = preg_replace('/\$$/', '', $baseObject->attributeInitialValue('cn'));
      $oldips     = $baseObject->attributeInitialValue('ipHostNumber');

      $oldzones     = $this->attributeInitialValue('fdDNSZoneDn');
      $currentzones = $this->attributeValue('fdDNSZoneDn');
      $removedzones = array_diff($oldzones, $currentzones);

      $removalsNeeded = ($this->attributeHaveChanged('fdDNSZoneDn') && !empty($oldzones));
      $cnChanged      = ($baseObject->attributeHaveChanged('cn') && !empty($oldcn));
      $ipChanged      = $baseObject->attributeHaveChanged('ipHostNumber');

      $lockedDns  = [];

      /* Lock zones */
      $zonesToLock = [];
      if ($removalsNeeded) {
        $zonesToLock = $removedzones;
      }
      if ($cnChanged || $ipChanged) {
        $zonesToLock = array_merge($zonesToLock, $currentzones);
      }
      foreach ($zonesToLock as $zoneDn) {
        /* Check locking */
        if ($locks = Lock::get($zoneDn)) {
          $errors[] = sprintf(_('"%s" is locked (by "%s" since %s), could not save modifications to this DNS zone'), $locks[0]->objectDn, $locks[0]->userDn, date('%Y-%m-%d, %H:%M:%S', $locks[0]->timestamp));
          break;
        } else {
          /* Lock the current entry */
          Lock::add($zoneDn);
          $lockedDns[] = $zoneDn;
        }
      }

      if (!empty($errors)) {
        Lock::deleteByObject($lockedDns);
        return $errors;
      }

      if ($removalsNeeded) {
        $messages = array_merge(
          $messages,
          static::removeIpsFromZones($removedzones, $oldips)
        );
        $messages = array_merge(
          $messages,
          static::removeCnFromZones($removedzones, $oldcn)
        );
      }

      if ($cnChanged) {
        $newcn = preg_replace('/\$$/', '', $baseObject->attributeValue('cn'));
        foreach ($currentzones as $dn) {
          $tabObject = objects::open($dn, 'dnsZone');
          $nbRecords = $tabObject->getBaseObject()->cnChanged($oldcn, $newcn);
          if ($nbRecords > 0) {
            $messages[] = sprintf(
              _('%d records were updated from %s to %s in zone %s'),
              $nbRecords,
              $oldcn,
              $newcn,
              $tabObject->getBaseObject()->zoneName
            );
          }
        }
      }

      if ($ipChanged) {
        $newips = $baseObject->attributeValue('ipHostNumber');
        if ((count($oldips) == 1) && (count($newips) == 1)) {
          foreach ($currentzones as $dn) {
            $tabObject = objects::open($dn, 'dnsZone');
            $nbRecords = $tabObject->getBaseObject()->ipChanged(reset($oldips), reset($newips));
            if ($nbRecords > 0) {
              $messages[] = sprintf(
                _('%d records were updated from %s to %s in zone %s'),
                $nbRecords,
                reset($oldips),
                reset($newips),
                $tabObject->getBaseObject()->zoneName
              );
            }
          }
        } elseif (!empty($oldips)) {
          static::removeIpsFromZones($currentzones, array_diff($oldips, $newips));
        }
      }

      if (!empty($messages)) {
        msg_dialog::display(_('DNS update'), implode("<br/>\n", array_map('htmlescape', $messages))."<br/>\n", INFO_DIALOG);
      }

      Lock::deleteByObject($lockedDns);
    }

    return $errors;
  }

  protected function post_remove ()
  {
    /* Remove records of our IP or CN */
    if ($this->initially_was_account) {
      $baseObject = $this->parent->getBaseObject();
      $messages   = [];
      $oldcn      = preg_replace('/\$$/', '', $baseObject->attributeInitialValue('cn'));
      $oldips     = $baseObject->attributeInitialValue('ipHostNumber');

      $oldzones = $this->attributeInitialValue('fdDNSZoneDn');
      if (!empty($oldzones)) {
        $messages = array_merge(
          $messages,
          static::removeIpsFromZones($oldzones, $oldips)
        );
        $messages = array_merge(
          $messages,
          static::removeCnFromZones($oldzones, $oldcn)
        );
      }
    }
    if (!empty($messages)) {
      msg_dialog::display(_('DNS update'), implode("<br/>\n", array_map('htmlescape', $messages))."<br/>\n", INFO_DIALOG);
    }

    return parent::post_remove();
  }

  function adapt_from_template (array $attrs, array $skip = [])
  {
    parent::adapt_from_template($attrs, $skip);
    $this->reloadZoneChoices();
    $this->zoneChanged();
    if (isset($this->attributesInfo['records'])) {
      foreach ($this->attributesInfo['records']['attrs'] as &$attr) {
        $attr->loadAdditionalTemplatesValues();
      }
      unset($attr);
    }
  }

  function foreignKeyUpdate (string $field, $oldvalue, $newvalue, array $source)
  {
    parent::foreignKeyUpdate($field, $oldvalue, $newvalue, $source);
    if (empty($this->fdDNSZoneDn)) {
      $this->is_account = FALSE;
    }
  }

  /* Record example: array('host', 'aRecord', '192.168.1.1', '')
   * Fields are relativeDomainName, record type, record content, reverse zone (for a/aaaa) */
  function addRecord ($zoneDn, array $record)
  {
    if (isset($this->zonesCache[$zoneDn])) {
      $zoneid = preg_replace('/[\/\-,.#:; =]/', '_', $zoneDn);
    } else {
      return FALSE;
    }
    $attrId = 'zoneRecords'.$zoneid;
    $this->attributesAccess[$attrId]->addValue('', $record);
  }

  static function removeIpsFromZones (array $zoneDns, array $ips)
  {
    $messages = [];
    foreach ($zoneDns as $zoneDn) {
      try {
        $tabObject = objects::open($zoneDn, 'dnsZone');
      } catch (NonExistingLdapNodeException $e) {
        /* In this case the zone was just deleted/renamed, so nothing to do here, go on */
        continue;
      }
      $nbRecords = $tabObject->getBaseObject()->ipRemoved($ips);
      if ($nbRecords > 0) {
        $messages[] = sprintf(
          _('%d records were removed in zone %s'),
          $nbRecords,
          $tabObject->getBaseObject()->zoneName
        );
      }
    }
    return $messages;
  }

  static function removeCnFromZones (array $zoneDns, $cn)
  {
    $messages = [];
    foreach ($zoneDns as $zoneDn) {
      try {
        $tabObject = objects::open($zoneDn, 'dnsZone');
      } catch (NonExistingLdapNodeException $e) {
        continue;
      }
      $nbRecords = $tabObject->getBaseObject()->cnRemoved($cn);
      if ($nbRecords > 0) {
        $messages[] = sprintf(
          _('%d records were removed in zone %s'),
          $nbRecords,
          $tabObject->getBaseObject()->zoneName
        );
      }
    }
    return $messages;
  }
}
