<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)

  Copyright (C) 2012-2019 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 configInLdap extends simplePlugin
{
  static function plInfo (): array
  {
    return [
      'plShortName'     => _('Configuration'),
      'plTitle'         => _('FusionDirectory configuration'),
      'plDescription'   => _('Configuration screen of FusionDirectory'),
      'plIcon'          => 'geticon.php?context=categories&icon=settings&size=48',
      'plObjectClass'   => ['fusionDirectoryConf'],
      'plObjectType'    => [
        'configuration' => [
          'name'      => _('FusionDirectory configuration'),
          'filter'    => 'objectClass=fusionDirectoryConf',
          'tabClass'  => 'configInLdapTabs',
          'icon'      => 'geticon.php?context=categories&icon=settings&size=16',
          'mainAttr'  => FALSE,
          'ou'        => preg_replace('/^[^,]+,/', '', CONFIGRDN)
        ]
      ],
      'plSection'       => ['conf' => ['name' => _('Configuration'), 'priority' => 20]],
      'plManages'       => ['configuration'],
      'plPriority'      => 0,

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

  static function getAttributesInfo (): array
  {
    global $config;

    return [
      'look_n_feel' => [
        'name'  => _('Look and feel'),
        'attrs' => [
          new SelectAttribute(
            _('Language'), _('Language of the application. If \'automatic\' or not available, the one asked by the browser will be used. This setting can be overriden per user.'),
            'fdLanguage', FALSE,
            ['']
          ),
          new SelectAttribute(
            _('Theme'), _('Theme to be used'),
            'fdTheme', TRUE,
            static::get_themes(),
            'breezy'
          ),
          new SelectAttribute(
            _('Timezone'), _('Timezone to be used'),
            'fdTimezone', TRUE,
            ['America/New_York']
          ),
          new HiddenAttribute('fusionConfigMd5'),
          new HiddenAttribute('fdIncrementalModifierStates'),
        ]
      ],
      'core_settings' => [
        'name'  => _('Core settings'),
        'attrs' => [
          new IntAttribute(
            _('LDAP size limit'), _('Defines the number of entries to get from LDAP by default.'),
            'fdLdapSizeLimit', FALSE,
            0 /*min*/, FALSE /*no max*/, 200
          ),
          new SelectAttribute(
            _('Edit locking'),
            _('Check if a entry currently being edited has been modified outside of FusionDirectory in the meantime.'),
            'fdModificationDetectionAttribute', FALSE,
            ['', 'entryCSN', 'contextCSN'], 'entryCSN'
          ),
          new BooleanAttribute(
            _('Enable logging'),
            _('Event logging on FusionDirectory side.'),
            'fdLogging', FALSE,
            TRUE
          ),
          new BooleanAttribute(
            _('Schema validation'),
            _('Enables schema checking during login.'),
            'fdSchemaCheck', FALSE,
            TRUE
          ),
          new BooleanAttribute(
            _('Wildcard foreign keys'), _('Enables wildcard searches like member=* when moving a whole department. This will open all existing groups and roles to make sure foreign keys are respected. Slow on big trees.'),
            'fdWildcardForeignKeys', FALSE,
            TRUE
          ),
        ]
      ],
      'password' => [
        'name'  => _('Password settings'),
        'attrs' => [
          new SetAttribute(
            new SelectAttribute(
              _('Allowed password hashes'), _('Password hashes which may be used for user passwords'),
              'fdPasswordAllowedHashes', TRUE,
              ['ssha']
            )
          ),
          new SelectAttribute(
            _('Password default hash'), _('Default hash to be used'),
            'fdPasswordDefaultHash', TRUE,
            ['ssha']
          ),
          new BooleanAttribute(
            _('Force default hash'), _('Force the use of the default password hash'),
            'fdForcePasswordDefaultHash'
          ),
          new IntAttribute(
            _('Password minimum length'), _('Minimum length of user passwords'),
            'fdPasswordMinLength', FALSE,
            0 /*min*/, FALSE /*no max*/
          ),
          new IntAttribute(
            _('Password minimum differs'), _('Minimum number of different characters from last password'),
            'fdPasswordMinDiffer', FALSE,
            0 /*min*/, FALSE /*no max*/
          ),
          new BooleanAttribute(
            _('Use account expiration'),
            _('Enables shadow attribute tests during the login to FusionDirectory and forces password renewal or account locking'),
            'fdHandleExpiredAccounts'
          ),
          new StringAttribute(
            _('SASL Realm'), _('SASL Realm'),
            'fdSaslRealm'
          ),
          new StringAttribute(
            _('SASL Exop'), _('Attribute to be stored in the userPassword attribute'),
            'fdSaslExop'
          ),
        ]
      ],
      'login' => [
        'name'  => _('Login and session'),
        'attrs' => [
          new SelectAttribute(
            _('Login attribute'),
            _('Which LDAP attribute should be used as the login name during login.'),
            'fdLoginAttribute', TRUE,
            ['uid', 'mail', 'uid,mail'], 'uid',
            ['uid', 'mail', 'both']
          ),
          new BooleanAttribute(
            _('Enforce encrypted connections'),
            _('Enables PHP security checks to force encrypted access (https) to the web interface.'),
            'fdForceSSL'
          ),
          new BooleanAttribute(
            _('Warn if session is not encrypted'),
            _('will display a warning to the user when http is used instead of https.'),
            'fdWarnSSL', FALSE,
            TRUE
          ),
          new IntAttribute(
            _('Session lifetime'), _('Defines when a session will expire in seconds (0 to disable).'),
            'fdSessionLifeTime', TRUE,
            0 /*min*/, FALSE /*no max*/, 1800
          ),
          new SelectAttribute(
            _('Login method'),
            _('Which login method should be used for connecting to FusionDirectory'),
            'fdLoginMethod', TRUE
          ),
          new StringAttribute(
            _('Header name'), _('Name of the header containing user identifier.'),
            'fdHttpHeaderAuthHeaderName', FALSE,
            'AUTH_USER'
          ),
        ]
      ],
      'ssl' => [
        'name'  => _('SSL'),
        'attrs' => [
          new TrimmedStringAttribute(
            _('Key path'), _('Path to FusionDirectory private key. Unused for now.'),
            'fdSslKeyPath', FALSE,
            '/etc/ssl/private/fd.key'
          ),
          new TrimmedStringAttribute(
            _('Certificate path'), _('Path to FusionDirectory certificate. Unused for now.'),
            'fdSslCertPath', FALSE,
            '/etc/ssl/certs/fd.cert'
          ),
          new TrimmedStringAttribute(
            _('CA certificate path'), _('Path to the CA certificate. Used for validating Argonaut Server host.'),
            'fdSslCaCertPath', FALSE,
            '/etc/ssl/certs/ca.cert'
          ),
        ]
      ],
      'cas' => [
        'name'  => _('CAS'),
        'attrs' => [
          new TrimmedStringAttribute(
            _('CA certificate path'), _('Path to the CA certificate of the CAS server'),
            'fdCasServerCaCertPath', FALSE,
            '/etc/ssl/certs/ca.cert'
          ),
          new StringAttribute(
            _('Host'), _('Host of the CAS server'),
            'fdCasHost', FALSE,
            'localhost'
          ),
          new IntAttribute(
            _('Port'), _('Port the CAS server is listening on'),
            'fdCasPort', FALSE,
            0 /*min*/, FALSE /*no max*/, 443
          ),
          new StringAttribute(
            _('CAS context'), _('CAS context to be used'),
            'fdCasContext', FALSE,
            '/cas'
          ),
          new BooleanAttribute(
            _('Verbose error'), _('Activate verbose errors in phpCAS. Avoid in production.'),
            'fdCasVerbose', FALSE
          ),
          new BooleanAttribute(
            _('Library CAS 1.6'), _('Activate if library CAS >= 1.6 is being used.'),
            'fdCasLibraryBool', FALSE
          ),
          new StringAttribute(
            _('Client service'), _('The client service name'),
            'fdCasClientServiceName', FALSE
          ),
        ]
      ],
      'people_and_group' => [
        'name'  => _('People and group storage'),
        'class' => ['critical'],
        'attrs' => [
          new SelectAttribute(
            _('People DN attribute'), _('Attribute to use at the beginning of the user dn'),
            'fdAccountPrimaryAttribute', TRUE,
            ['uid', 'cn']
          ),
          new StringAttribute(
            _('CN pattern'), _('The pattern to use to build the common name field'),
            'fdCnPattern', TRUE,
            '%givenName% %sn%'
          ),
          new BooleanAttribute(
            _('Mandatory first name'),
            _('Whether first name (givenName) should be a mandatory field on users'),
            'fdGivenNameRequired', FALSE,
            TRUE
          ),
          new BooleanAttribute(
            _('Strict naming policy'),
            _('Enables strict checking of user and group names'),
            'fdStrictNamingRules', FALSE,
            TRUE
          ),
          new StringAttribute(
            _('Users RDN'), _('The branch where users are stored.'),
            'fdUserRDN', TRUE,
            'ou=people'
          ),
          new StringAttribute(
            _('ACL role RDN'), _('The branch where ACL roles are stored.'),
            'fdAclRoleRDN', TRUE,
            'ou=aclroles'
          ),
          new BooleanAttribute(
            _('Restrict role members'), _('When enabled only users from the same branch or members of groups from the same branch can be added to a role.'),
            'fdRestrictRoleMembers'
          ),
          new BooleanAttribute(
            _('Separate address fields'), _('Expose street, postOfficeBox and postalCode fields instead of postalAddress.'),
            'fdSplitPostalAddress'
          ),
          new PostalAddressAttribute(
            _('Postal address pattern'), _('When using separate address fields, you can use a pattern to fill postalAddress field.'),
            'fdPostalAddressPattern'
          ),
          new IntAttribute(
            _('Avatar max size'), _('Maximum user picture width and height in pixels. Bigger uploaded pictures will be resized.'),
            'fdMaxAvatarSize', FALSE,
            1, FALSE, 200
          ),
        ]
      ],
      'debug' => [
        'name'  => _('Debugging'),
        'attrs' => [
          new BooleanAttribute(
            _('Display PHP errors'),
            _('Shows PHP errors in the upper part of the screen. This should be disabled in production deployments, because it may contain passwords.'),
            'fdDisplayErrors'
          ),
          new IntAttribute(
            _('Maximum LDAP query time'), _('Stop LDAP actions if there is no answer within the specified number of seconds.'),
            'fdLdapMaxQueryTime', FALSE,
            0 /*min*/, FALSE /*no max*/
          ),
          new BooleanAttribute(
            _('Log LDAP statistics'),
            _('Track LDAP timing statistics to the syslog. This may help to find indexing problems or bad search filters.'),
            'fdLdapStats'
          ),
          new DebugLevelAttribute(
            new SelectAttribute(
              _('Debug level'),
              _('Display certain information on each page load.'),
              'fdDebugLevel', FALSE,
              [DEBUG_TRACE,  DEBUG_LDAP, DEBUG_DB,   DEBUG_SHELL,  DEBUG_POST,
                    DEBUG_SESSION,  DEBUG_ACL,  DEBUG_SI, DEBUG_MAIL],
                    DEBUG_TRACE,
              ['Trace',      'LDAP',     'Database', 'Shell',      'POST',
                    'SESSION',      'ACL',      'SI',     'Mail']
            )
          ),
          new BooleanAttribute(
            _('Log debug messages'),
            _('Sends debug output to syslog as well'),
            'fdDebugLogging'
          ),
        ]
      ],
      'miscellaneous' => [
        'name'  => _('Miscellaneous'),
        'attrs' => [
          new BooleanAttribute(
            _('Display summary in listings'),
            _('Determines whether a status bar will be shown on the bottom of lists, displaying a short summary of type and number of elements in the list.'),
            'fdListSummary', FALSE,
            TRUE
          ),
          new BooleanAttribute(
            _('Show ACL tab on all objects'),
            _('For very specific ACL rights setting where you might need to give right on a single object.'),
            'fdAclTabOnObjects'
          ),
          new SetAttribute(
            new StringAttribute(
              _('Available department categories'), _('Available categories in the departments dropdown'),
              'fdDepartmentCategories', FALSE
            ),
            []
          ),
          new OrderedArrayAttribute(
            new PipeSeparatedCompositeAttribute(
              _('Use this to hide some menu entry to specific groups of users'),
              'fdPluginsMenuBlacklist',
              [
                new SelectAttribute(
                  '', _('Group or role'),
                  'blacklistGroup', TRUE,
                  []
                ),
                new SelectAttribute(
                  '', _('Plugin to blacklist'),
                  'blacklistPlugin', TRUE,
                  []
                ),
              ],
              '',
              _('Plugin menu blacklist')
            ),
            // no order
            FALSE,
            []
          ),
          // Needed here for ACLs
          new HiddenAttribute('fdManagementConfig'),
          new IntAttribute(
            _('ACL target filter limit'), _('Defines the maximum number of entries an ACL target filter is allowed to return'),
            'fdAclTargetFilterLimit', FALSE,
            0 /*min*/, FALSE /*no max*/, 100
          ),
        ]
      ],
    ];
  }

  function __construct ($dn = NULL, $object = NULL, $parent = NULL, $mainTab = FALSE, $attributesInfo = NULL)
  {
    global $config;
    $attributesInfo = static::getAttributesInfo();
    /* Languages */
    $languages = Language::getList(TRUE);
    asort($languages);
    $languages = array_merge(["" => _("Automatic")], $languages);
    $attributesInfo['look_n_feel']['attrs'][0]->setChoices(array_keys($languages), array_values($languages));
    /* Timezones */
    $attributesInfo['look_n_feel']['attrs'][2]->setChoices(timezone::_get_tz_zones());
    /* Password methods */
    $methods = passwordMethod::get_available_methods();
    $methods = $methods['name'];
    if (!in_array('sasl', $methods)) {
      $methods[] = 'sasl';
    }
    $attributesInfo['password']['attrs'][0]->attribute->setChoices($methods);
    $attributesInfo['password']['attrs'][0]->setDefaultValue($methods);
    $attributesInfo['password']['attrs'][0]->resetToDefault();
    $attributesInfo['password']['attrs'][1]->setChoices($methods);
    /* Login methods */
    $methods = LoginMethod::getMethods();
    $attributesInfo['login']['attrs'][4]->setChoices(array_keys($methods), array_values($methods));

    $groupsAndRoles = array_merge(
      array_map(
        function ($group)
        {
          return sprintf(_('Group %s'), $group);
        },
        objects::ls('ogroup')
      ),
      array_map(
        function ($role)
        {
          return sprintf(_('Role %s'), $role);
        },
        objects::ls('role')
      )
    );
    $attributesInfo['miscellaneous']['attrs'][3]->attribute->attributes[0]->setChoices(
      array_keys($groupsAndRoles),
      array_values($groupsAndRoles)
    );
    $menuPlugins = [];
    $plist = session::get('plist');
    foreach ($config->data['SECTIONS'] as $section => $section_infos) {
      foreach ($config->data['MENU'][$section] as $info) {
        if (isset($info['CLASS'])) {
          list ($plHeadline, , , ) = $plist->get_infos($info['CLASS']);
          $menuPlugins[$info['CLASS']] = $plHeadline;
        }
      }
    }
    asort($menuPlugins);
    $attributesInfo['miscellaneous']['attrs'][3]->attribute->attributes[1]->setChoices(
      array_keys($menuPlugins),
      array_values($menuPlugins)
    );

    try {
      parent::__construct($dn, $object, $parent, $mainTab, $attributesInfo);
    } catch (NonExistingLdapNodeException $e) {
      parent::__construct('new', $object, $parent, $mainTab, $attributesInfo);
      $this->dn = $dn;
    }

    $this->fusionConfigMd5 = md5_file(CACHE_DIR."/".CLASS_CACHE);

    $this->attributesAccess['fdForceSSL']->setManagedAttributes(
      [
        'disable' => [
          TRUE => [
            'fdWarnSSL',
          ]
        ]
      ]
    );
    $this->attributesAccess['fdSplitPostalAddress']->setManagedAttributes(
      [
        'disable' => [
          FALSE => [
            'fdPostalAddressPattern',
          ]
        ]
      ]
    );

    // CAS boolean case to allow the use of CAS library >= 1.6
    $this->attributesAccess['fdCasLibraryBool']->setManagedAttributes(
      [
        'disable' => [
          FALSE => [
            'fdCasClientServiceName',
          ]
        ]
      ]
    );

    $this->attributesAccess['fdLoginMethod']->setManagedAttributes(
      [
        'multiplevalues' => [
          'noncas'    => [
            'LoginPost',
            'LoginHTTPAuth',
            'LoginHTTPHeader',
          ],
          'nonheader' => [
            'LoginPost',
            'LoginCAS',
            'LoginHTTPAuth'
          ],
        ],
        'disable' => [
          'noncas' => [
            'fdCasServerCaCertPath',
            'fdCasHost',
            'fdCasPort',
            'fdCasContext',
            'fdCasVerbose',
            'fdCasClientServiceName',
            'fdCasLibraryBool'
          ],
          'nonheader' => [
            'fdHttpHeaderAuthHeaderName',
          ]
        ]
      ]
    );
    if (empty($this->attrs['fdLoginMethod'][0])) {
      // Reading OBSOLETE attributes from FD<1.4 to ease migration
      if (isset($this->attrs['fdHttpAuthActivated'][0]) && ($this->attrs['fdHttpAuthActivated'][0] == 'TRUE')) {
        $this->fdLoginMethod = 'LoginHTTPAuth';
      } elseif (isset($this->attrs['fdCasActivated'][0]) && ($this->attrs['fdCasActivated'][0] == 'TRUE')) {
        $this->fdLoginMethod = 'LoginCAS';
      } elseif (isset($this->attrs['fdHttpHeaderAuthActivated'][0]) && ($this->attrs['fdHttpHeaderAuthActivated'][0] == 'TRUE')) {
        $this->fdLoginMethod = 'LoginHTTPHeader';
      }
    }

    $this->attributesAccess['fdPasswordDefaultHash']->setChoices(
      $this->attributesAccess['fdPasswordAllowedHashes']->getValue()
    );
  }

  function compute_dn (): string
  {
    return $this->dn;
  }

  function check (): array
  {
    $messages = parent::check();
    if (($this->fdPasswordDefaultHash == 'sasl') && ($this->fdSaslRealm == '') && ($this->fdSaslExop == '')) {
      $messages[] = new SimplePluginCheckError(
        $this,
        htmlescape(_('You need to fill saslRealm or saslExop in the configuration screen in order to use SASL'))
      );
    }
    if ($this->attributesAccess['fdLanguage']->hasChanged() && ($this->fdLanguage != '') && !Language::isAvailable($this->fdLanguage)) {
      $messages[] = new SimplePluginCheckError(
        $this->attributesAccess['fdLanguage'],
        htmlescape(sprintf(_('It seems the selected language "%s" is not installed on the system. Please install it or select an other one.'), $this->fdLanguage))
      );
    }

    if (($this->fdLdapSizeLimit !== '') && ($this->fdLdapSizeLimit > 0)) {
      $error = ldapSizeLimit::checkMaxInputVars($this->fdLdapSizeLimit);
      if ($error !== FALSE) {
        $messages[] = new SimplePluginCheckError(
          $this->attributesAccess['fdLdapSizeLimit'],
          $error->getHtmlMessage(),
          $error->getCode(),
          $error
        );
      }
    }

    return $messages;
  }

  public function update (): bool
  {
    $res = parent::update();

    $this->attributesAccess['fdPasswordDefaultHash']->setChoices(
      $this->attributesAccess['fdPasswordAllowedHashes']->getValue()
    );

    return $res;
  }

  static function get_themes ()
  {
    $themesdir  = '../ihtml/themes/';
    $themes     = array_keys(session::get(IconTheme::$session_var));
    if ($dir = opendir("$themesdir")) {
      while (($file = readdir($dir)) !== FALSE) {
        if (is_dir("$themesdir/$file") && !preg_match("/^\./", $file)) {
          $themes[] = $file;
        }
      }
    }
    return array_unique($themes);
  }

  static function mainInc ($classname = NULL, $entry_dn = NULL, $tabs = TRUE, $edit_mode = TRUE, $objectType = FALSE)
  {
    global $config;

    if ($classname === NULL) {
      $classname = get_called_class();
    }

    if ($entry_dn === NULL) {
      $entry_dn = CONFIGRDN.$config->current['BASE'];
    }

    parent::mainInc($classname, $entry_dn, $tabs, $edit_mode, $objectType);
  }
}