<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
  Copyright (C) 2003-2010  Cajus Pollmeier
  Copyright (C) 2011-2016  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.
*/

class FaiPartitionSizeAttribute extends CompositeAttribute
{
  function __construct ($label, $description, $ldapName, $acl = "")
  {
    parent::__construct(
      $description, $ldapName,
      [
        new SelectAttribute(
          '', _('Type of sizing - fixed, dynamic or all remaining space'),
          'FAIpartitionSizeType', TRUE,
          ['fixed', 'dynamic', 'remaining'], '',
          [_('Fixed'), _('Dynamic'), _('Remaining space')]
        ),
        new IntAttribute(
          '', '',
          'FAIpartitionSizeStart_size', FALSE,
          1, FALSE, ''
        ),
        new SelectAttribute(
          '', '',
          'FAIpartitionSizeStart_unit', TRUE,
          ['KB','MB','GB','TB','PB','%'], 'MB',
          [_('KB'),_('MB'),_('GB'),_('TB'),_('PB'),'%']
        ),
        new IntAttribute(
          '-', '',
          'FAIpartitionSizeStop_size', FALSE,
          1, FALSE, ''
        ),
        new SelectAttribute(
          '', '',
          'FAIpartitionSizeStop_unit', TRUE,
          ['KB','MB','GB','TB','PB','%'], 'MB',
          [_('KB'),_('MB'),_('GB'),_('TB'),_('PB'),'%']
        )
      ],
      '',
      '',
      $acl, $label
    );
    $this->setLinearRendering(TRUE);
    $this->attributes[0]->setSubmitForm(TRUE);
    $this->updateAttributes();
  }

  function readValues (string $value): array
  {
    if (preg_match('/^([0-9]{1,})(KB|MB|GB|TB|PB|%)$/', $value, $m)) {
      // Fixed
      return [
        'fixed',
        $m[1],$m[2],
        0,'',
      ];
    } elseif (preg_match('/^([0-9]{1,})(KB|MB|GB|TB|PB|%)-([0-9]{1,})(KB|MB|GB|TB|PB|%)$/', $value)) {
      // Dynamic range
      return [
        'dynamic',
        $m[1],$m[2],
        $m[3],$m[4],
      ];
    } elseif (preg_match('/^0?\-$/', $value)) {
      // Dynamic range
      return [
        'remaining',
        0,'',
        0,'',
      ];
    } else {
      throw new SimplePluginError($this, sprintf(_('Invalid value "%s"'), $value));
    }
  }

  protected function updateAttributes ()
  {
    switch ($this->attributes[0]->getValue()) {
      case 'remaining':
        $this->attributes[1]->setVisible(FALSE);
        $this->attributes[2]->setVisible(FALSE);
        $this->attributes[3]->setVisible(FALSE);
        $this->attributes[4]->setVisible(FALSE);
        break;
      case 'fixed':
        $this->attributes[1]->setVisible(TRUE);
        $this->attributes[2]->setVisible(TRUE);
        $this->attributes[3]->setVisible(FALSE);
        $this->attributes[4]->setVisible(FALSE);
        break;
      case 'dynamic':
        $this->attributes[1]->setVisible(TRUE);
        $this->attributes[2]->setVisible(TRUE);
        $this->attributes[3]->setVisible(TRUE);
        $this->attributes[4]->setVisible(TRUE);
        break;
    }
  }

  function check ()
  {
    if ($this->isDisabled()) {
      return;
    }
    foreach ($this->attributes as &$attribute) {
      if (!$attribute->isVisible()) {
        continue;
      }
      $error = $attribute->check();
      if (!empty($error)) {
        return $error;
      }
    }
    unset($attribute);
    if ($this->attributes[0]->getValue() == 'dynamic') {
      $mp = [
        '%'  => 1,
        'KB' => 1024 ** 0,
        'MB' => 1024 ** 1,
        'GB' => 1024 ** 2,
        'TB' => 1024 ** 3,
        'PB' => 1024 ** 4
      ];
      $res1 = $this->attributes[1]->getValue() * $mp[$this->attributes[2]->getValue()];
      $res2 = $this->attributes[3]->getValue() * $mp[$this->attributes[4]->getValue()];
      if ($res1 > $res2) {
        return new SimplePluginCheckError(
          $this,
          msgPool::toobig(_('Minimum partition size'), '\''._('Maximum partition size').'\'')
        );
      }
    }
  }

  function applyPostValue ()
  {
    parent::applyPostValue();
    $this->updateAttributes();
  }

  function setValue ($values)
  {
    parent::setValue($values);
    $this->updateAttributes();
  }

  function resetToDefault ()
  {
    parent::resetToDefault();
    $this->updateAttributes();
  }

  function writeValues (array $values)
  {
    if ($values[0] == 'remaining') {
      return '0-';
    }
    $value = $values[1].$values[2];
    if ($values[0] == 'dynamic') {
      $value .= '-'.$values[3].$values[4];
    }
    return $value;
  }
}

class FaiMountPointAttribute extends StringAttribute
{
  function __construct ($label, $description, $ldapName, $required = FALSE, $defaultValue = '/', $acl = '')
  {
    parent::__construct(
      $label, $description, $ldapName, $required,
      $defaultValue, $acl, '/^(\/|\-|swap)[a-z0-9%\/_.+-]*$/i', '/home'
    );
  }

  function setPostValue ($value)
  {
    if ($value == '') {
      $value = '-';
    }
    parent::setPostValue($value);
  }
}

class FaiRaidDevices extends OrderedArrayAttribute
{
  protected function getAttributeArrayValue ($key, $value)
  {
    $value = parent::getAttributeArrayValue($key, $value);
    return [
      $value[0],
      ($value[1] ? 'spare' : ''),
      ($value[2] ? 'missing' : '')
    ];
  }
}

class faiPartition extends simplePlugin
{
  static function plInfo (): array
  {
    return [
      'plShortName'     => _('Partition entry'),
      'plDescription'   => _('FAI partition entry'),
      'plCategory'      => ['fai'],
      /* No ACL, we use the one for FAIpartitions */
      'plProvidedAcls'  => []
    ];
  }

  static function getAttributesInfo (): array
  {
    return [
      'main' => [
        'name'  => _('Partition'),
        'attrs' => [
          new HiddenAttribute('FAIdiskType'),
          new HiddenAttribute('FAIpartitionNr', FALSE, 'undefined'),
          new SelectAttribute(
            _('Type'), _('Partition type'),
            'FAIpartitionType', TRUE,
            ['primary', 'logical'], '',
            [_('Primary'), _('Logical')]
          ),
          new StringAttribute(
            _('Name'), _('Partition name'),
            'cn', TRUE,
            '', 'FAIpartitions',
            '/[a-z0-9_-]/i'
          ),
          new StringAttribute(
            _('Description'), _('Short description'),
            'description', FALSE
          ),
          new FaiPartitionSizeAttribute(
            _('Size'), _('Size of this partition'),
            'FAIpartitionSize', FALSE
          ),
        ]
      ],
      'flags' => [
        'name'  => _('Flags'),
        'attrs' => [
          new CommaListAttribute(
            'raidDevices',
            new FaiRaidDevices(
              new CompositeAttribute(
                '', 'raidDevices_composite',
                [
                  new SelectAttribute(
                    '', _('Combined physical partitions in this RAID'),
                    'raidDevices_select', FALSE
                  ),
                  new BooleanAttribute(
                    _('Spare'), _('Spare Raid device'),
                    'raidDevices_spare', FALSE,
                    FALSE, '',
                    ':spare', ''
                  ),
                  new BooleanAttribute(
                    _('Missing'), _('Missing Raid device'),
                    'raidDevices_missing', FALSE,
                    FALSE, '',
                    ':missing', ''
                  )
                ],
                '/^([^:]+)(:spare|)(:missing|)$/',
                '%s%s%s',
                '',
                ''
              ),
              FALSE,
              [],
              TRUE
            )
          ),
          new SelectAttribute(
            _('Partition'), _('Partition to encrypt'),
            'cryptsetupPartition', FALSE
          ),
          new StringAttribute(
            _('Password'), _('Password to use for encryption. Leave empty to use a encryption key file instead.'),
            'cryptsetupPassword', FALSE
          ),
          new BooleanAttribute(
            _('Resize'), _('Resize existing partition'),
            'resize', FALSE
          ),
          new BooleanAttribute(
            _('Bootable'), _('Wether or not this partition can be booted'),
            'bootable', FALSE
          ),
          new SelectAttribute(
            _('Preserve'), _('Does the partition need to be preserved'),
            'preserve', FALSE,
            ['','always','reinstall'], '',
            [_('Never'),_('Always'),_('Reinstall')]
          ),
        ]
      ],
      'fs' => [
        'name'  => _('Filesystem'),
        'attrs' => [
          new SelectAttribute(
            _('Filesystem'), _('The filesystem this partition should be formatted with'),
            'FAIfsType', TRUE,
            ['swap','vfat','ext2','ext3','ext4','reiserfs','xfs','btrfs','-'], 'ext4',
            [_('swap'),_('fat32'),_('ext2'),_('ext3'),_('ext4'),_('reiser fs'),_('xfs'),_('btrfs'),_('LVM/RAID')]
          ),
          new FaiMountPointAttribute(
            _('Mount point'), _('Mount point for this partition'),
            'FAImountPoint', TRUE
          ),
        ]
      ],
      'options' => [
        'name'  => _('Options'),
        'attrs' => [
          new StringAttribute(
            _('Mount options'), _('Filesystem mount options'),
            'FAImountOptions', FALSE,
            'defaults'
          ),
          new StringAttribute(
            _('Create options'), _('Filesystem create options'),
            'FAIfsCreateOptions', FALSE
          ),
          new StringAttribute(
            _('Tune options'), _('Filesystem tune options'),
            'FAIfsTuneOptions', FALSE
          ),
        ]
      ]
    ];
  }

  function __construct ($object, $parent, $type)
  {
    parent::__construct(NULL, NULL, $parent);

    $this->attributesAccess['FAIfsType']->setManagedAttributes(
      ['disable' => ['swap' => ['FAImountPoint']]]
    );

    $this->FAIdiskType  = $type;

    if ($this->FAIdiskType == 'disk') {
      /* Check if we should be able to add primary partitions */
      $disablePrimary = FALSE;
      if (!$object || ($object['FAIpartitionType'] == 'logical')) {
        $types = ['logical' => [], 'primary' => []];
        foreach ($this->parent->partitions as $part) {
          $types[$part['FAIpartitionType']][$part['FAIpartitionNr']] = 1;
        }
        if (count($types['logical']) && (count($types['primary']) >= 3)) {
          $disablePrimary = TRUE;
        } elseif (count($types['logical']) >= 4) {
          $disablePrimary = TRUE;
        }
      }
      if ($disablePrimary) {
        $types = ['logical' => _('Logical')];
      } else {
        $types  = [
          'primary' => _('Primary'),
          'logical' => _('Logical')
        ];
      }
    } elseif ($this->FAIdiskType == 'raid') {
      $types  = [
        'raid0' => _('RAID 0'),
        'raid1' => _('RAID 1'),
        'raid5' => _('RAID 5'),
        'raid6' => _('RAID 6')
      ];
    } elseif ($this->FAIdiskType == 'cryptsetup') {
      $types  = [
        'luks'  => _('LUKS'),
        'swap'  => _('Swap'),
        'tmp'   => _('tmp'),
      ];
    } else {
      $types = [];
    }
    $this->attributesAccess['FAIpartitionType']->setChoices(array_keys($types), array_values($types));
    if (empty($types)) {
      $this->attributesAccess['FAIpartitionType']->setVisible(FALSE);
      $this->attributesAccess['FAIpartitionType']->setDisabled(TRUE);
    }

    // Load attributes from existing partition
    if ($object) {
      if ($type == 'raid') {
        $object['raidDevices'] = $object['FAIpartitionSize'];
        unset($object['FAIpartitionSize']);
      } elseif ($type == 'cryptsetup') {
        $object['cryptsetupPartition'] = $object['FAIpartitionSize'];
        unset($object['FAIpartitionSize']);
        if (preg_match('/^luks:"(.+)"$/', $object['FAIpartitionType'], $m)) {
          $object['FAIpartitionType']   = 'luks';
          $object['cryptsetupPassword'] = $m[1];
        }
      }
      foreach ($this->attributes as $attr) {
        if (isset($object[$attr])) {
          $this->$attr = $object[$attr];
        }
      }
    }

    if ($this->FAIdiskType != 'lvm') {
      $this->attributesAccess['cn']->setDisabled(TRUE);
    }

    if ($this->FAIdiskType == 'cryptsetup') {
      $this->attributesAccess['cryptsetupPartition']->setChoices($this->getPartitionList());
      $this->attributesAccess['cryptsetupPartition']->setRequired(TRUE);
      $this->attributesAccess['FAIpartitionSize']->setDisabled(TRUE);
      $this->attributesAccess['FAIpartitionSize']->setVisible(FALSE);
      $this->attributesInfo['flags']['name'] = _('Encryption settings');
      $this->attributesAccess['resize']->setDisabled(TRUE);
      $this->attributesAccess['resize']->setVisible(FALSE);
      $this->attributesAccess['preserve']->setDisabled(TRUE);
      $this->attributesAccess['preserve']->setVisible(FALSE);
      $this->attributesAccess['bootable']->setDisabled(TRUE);
      $this->attributesAccess['bootable']->setVisible(FALSE);
    } else {
      $this->attributesAccess['cryptsetupPartition']->setDisabled(TRUE);
      $this->attributesAccess['cryptsetupPartition']->setVisible(FALSE);
      $this->attributesAccess['cryptsetupPassword']->setDisabled(TRUE);
      $this->attributesAccess['cryptsetupPassword']->setVisible(FALSE);
    }

    if ($this->FAIdiskType == 'raid') {
      $this->attributesAccess['raidDevices']->attributes[0]->attribute->attributes[0]->setChoices($this->getPartitionList());
      $this->attributesAccess['FAIpartitionSize']->setDisabled(TRUE);
      $this->attributesAccess['FAIpartitionSize']->setVisible(FALSE);
      $this->attributesInfo['flags']['name'] = _('Combined physical partitions');
      $this->attributesAccess['resize']->setDisabled(TRUE);
      $this->attributesAccess['resize']->setVisible(FALSE);
      $this->attributesAccess['preserve']->setDisabled(TRUE);
      $this->attributesAccess['preserve']->setVisible(FALSE);
      $this->attributesAccess['bootable']->setDisabled(TRUE);
      $this->attributesAccess['bootable']->setVisible(FALSE);
    } else {
      $this->attributesAccess['raidDevices']->setDisabled(TRUE);
      $this->attributesAccess['raidDevices']->setVisible(FALSE);
    }
  }

  protected function loadAttributes ()
  {
    foreach ($this->attributesAccess as &$attribute) {
      $attribute->setAcl('FAIpartitions');
    }
    unset($attribute);
    parent::loadAttributes();
  }

  public function render (): string
  {
    $smarty = get_smarty();
    $acl    = $this->aclGetPermissions('FAIpartitions');
    $smarty->assign('FAIpartitionsACL', $acl);
    parent::render();

    $buttons = '<p class="plugbottom">
                  <input type="submit" name="PartitionSave" value="'.msgPool::saveButton().'">
                  <input type="submit" formnovalidate="formnovalidate" name="PartitionCancel" value="'.msgPool::cancelButton().'" >
                </p>';
    if ($this->displayPlugin) {
      return $this->header.$smarty->fetch($this->templatePath).$buttons;
    } else {
      return $this->header;
    }
  }

  public function update (): bool
  {
    parent::update();
    if ($this->FAIfsType == 'swap') {
      $this->FAImountPoint = 'swap';
    }
    return TRUE;
  }

  function check (): array
  {
    $errors = parent::check();

    if ($this->FAIdiskType == 'raid') {
      $raidDevices = $this->attributesAccess['raidDevices']->attributes[0]->getValue();
      if (count($raidDevices) < 2) {
        $errors[] = new SimplePluginCheckError(
          $this->attributesAccess['raidDevices'],
          htmlescape(_('Raid arrays must contain at least two partitions!'))
        );
      } elseif (($this->FAIpartitionType == 'raid0') && (count($this->raidDevices) != 2)) {
        $errors[] = new SimplePluginCheckError(
          $this->attributesAccess['raidDevices'],
          htmlescape(_('Raid 0 arrays can only be realized with a combination of two partitions!'))
        );
      }
    }

    return $errors;
  }

  function save (): array
  {
    $ret = [];
    foreach ($this->attributes as $attr) {
      $ret[$attr] = $this->$attr;
    }

    if ($this->FAIdiskType == 'raid') {
      $ret['FAIpartitionSize'] = $ret['raidDevices'];
      unset($ret['raidDevices']);
    } elseif ($this->FAIdiskType == 'cryptsetup') {
      $ret['FAIpartitionSize'] = $ret['cryptsetupPartition'];
      unset($ret['cryptsetupPartition']);
      if (!empty($ret['cryptsetupPassword'])) {
        $ret['FAIpartitionType'] .= ':"'.$ret['cryptsetupPassword'].'"';
      }
      unset($ret['cryptsetupPassword']);
    }

    return $ret;
  }

  /* Returns a list of all partitions that are useable
   *  for raid arrays.
   */
  function getPartitionList (): array
  {
    $may = $used = [];
    foreach ($this->parent->parent->disks as $disk) {
      // Skip ourselves
      if ($disk['cn'] == $this->parent->cn) {
        continue;
      }

      // Add partition from lvm combinations
      if ($disk['FAIdiskType'] == 'lvm') {
        $used = array_merge($used, $disk['FAIlvmDevice']);
      }

      foreach ($disk['partitions'] as $part) {
        // Add disks of raid arrays, to the used list.
        if ($disk['FAIdiskType'] == 'raid') {
          foreach (explode(',', $part['FAIpartitionSize']) as $rDevice) {
            $used[] = preg_replace('/:.*$/i', '', $rDevice);
          }
        }

        // Collect all available disks
        if ($disk['FAIdiskType'] == 'disk') {
          $may[] = $part['cn'];
        }
      }
    }

    // Check which of the available disks are unused.
    $ret = [];
    foreach ($may as $val) {
      if (!in_array($val, $used)) {
        $ret[$val] = $val;
      }
    }
    return $ret;
  }
}
