<?php

class ldapHelper extends WireData implements Module, ConfigurableModule
{
    protected $ldapSession = null;
    protected $host, $port, $defaultLoginDomain, $userDefaultRoles, $protocol, $debugMode, $useV3, $admin, $secret, $ldapSetting, $userdn;
    public $message;

    public static function getModuleInfo() {
        return array(
            'title' => __('LDAP Helper'),
            'version' => '006',
            'author' => 'Kreativmonkey',
            'summary' => __('Hendle LDAP Communication for Processwire'),
            'singular' => true,
            'autoload' => true
        );
    }

    public function ___install() {
        if (!function_exists('ldap_connect'))
            throw new WireException ('Please make sure that extension php_ldap is loaded.');
    }

    public function init() {
        if (isset ($this->data ['host'])) {
            $host = explode(':', str_replace(array(
                'ldap://',
                'ldaps://'
            ), array(
                '',
                ''
            ), $this->data ['host']));
            $this->protocol = (empty ($this->data ['useSSL'])) ? 'ldap://' : 'ldaps://';
            $this->host = $host [0];
            $this->port = empty ($host [1]) ? 389 : $host [1];
        }

        $this->debugMode = empty ($this->data ['debug']);

        if (isset ($this->data ['loginDomain']))
            $this->defaultLoginDomain = $this->data ['loginDomain'];

        if (isset ($this->data ['userdn']))
            $this->userdn = $this->data ['userdn'];

        if (isset ($this->data ['admin']))
            $this->admin = $this->data ['admin'];

        if (isset ($this->data ['secret']))
            $this->secret = $this->data ['secret'];

        if (isset ($this->data ['useV3']))
            $this->useV3 = $this->data ['useV3'];

        if (isset ($this->data ['ldapSetting']))
            $this->ldapSetting = $this->data ['ldapSetting'];

        $this->userDefaultRoles = new WireArray ();
        if (isset ($this->data ['userDefaultRoles'])) {
            foreach ($this->data ['userDefaultRoles'] as $x) {
                $role = $this->roles->get($x);
                $this->userDefaultRoles->add($role);
            }
        }

        $this->userDefaultRoles->add($this->roles->getGuestRole());
        $this->userDefaultRoles = $this->userDefaultRoles->unique();

        $this->pages->addHookBefore("delete", $this, "ldapHelperDeleteUser");
        $this->addHookAfter('Modules::saveModuleConfigData', $this, 'hookModuleSave');
        $this->session->addHookAfter('login', $this, 'ldapHelperLogin');
        $this->addHookBefore("Password::setPassword", $this, 'ldapHelperChangePw');
    }

    public function ldapHelperRegistradeUser(Array $user) {

      $username = $user["username"];

      // LDAP user anlegen
      $info["objectclass"][0] = "top";
      $info["objectclass"][1] = "person";
      $info["objectclass"][2] = "organizationalPerson";
      $info["objectclass"][3] = "inetorgperson";
      $info["cn"] = $user["firstname"];
      $info["sn"] = $user["lastname"];
      $info["givenName"] = $user["username"];
      $info["description"] = "ffmyk";

      $password = $this->makeSshaPassword($user["password"]);

      // If you have the plain text password instead, you could use:
      $info['userPassword'] = $password;

	     // Connect to LDAP Server
      if(!$connect = $this->ldapHelperConnect()) return false;

      // bind mit passendem dn für aktulisierenden Zugriff
      if(!$r= $this->ldapHelperWrite($connect)) return false;

      // Add User to the Database
      if(!$r= ldap_add($connect, "uid=$username,$this->userdn", $info)){
        $massage = ldap_err2str(ldap_errno($connect));
        $this->log->save('ldap_helper', "Create User failed $ldapMassage");

        return false;
      }
      $this->log->save('ldap_helper', "Create User: $username");

      // Verbindung schließen
      ldap_close($connect);

      // Registrierung erfolgreich
      return true;
    }

    public function ldapHelperLogin(HookEvent $event){
      if($event->return) return;

      $username = $event->arguments[0];
      $password = $event->arguments[1];

      if(!$con = $this->ldapHelperConnect()) return; // Connection Failed
      $user_search = ldap_search($con,"dc=ffmyk,dc=de","(|(uid=$username))");
      $user_entry = ldap_first_entry($con, $user_search);
      $user_dn = ldap_get_dn($con, $user_entry);

      if(@ldap_bind($con, $user_dn, $password)){
        $wireUserName = $this->sanitizer->pageName($username);

        $user = $this->users->get("name=$wireUserName");
        if($user instanceof NullPage){
          // Search the LDAP Entry
          $first_name = ldap_get_values($con, $user_entry, "cn");
          $last_name = ldap_get_values($con, $user_entry, "sn");

          // Add ldap user to ProcessWire
          $new_user = new User();
          $new_user->of(false);
          $new_user->name = $wireUserName;
          $new_user->firstname = $first_name;
          $new_user->lastname = $last_name;
          $new_user->pass = $password;
          $new_user->addRole("user");
          $new_user->admin_theme = "AdminThemeReno";
          $new_user->save();
          $new_user->of(true);
        } else {
          // Set new Password
          $user->of(false);
          $user->pass = $password;
          if($user->isChanged('pass')){
             $user->save();
             $this->message($this->_('Password changed success.'));
           }
           $user->of(true);
        }

        $user = $this->session->login($wireUserName, $password);
        $event->return = $user;

        $this->message($this->_('Logged in via LDAP.'));

        return;
      } else {
        $this->message("Bitte Registriere dich.");
      }
    }

    /**
     * Change the Password for the LDAP User
     * $username = Ldap uid
     * $newPassword = the new Password to set for the User
     * $newPasswordCnt = double check the new Password
     *
     * return
     * string
     */
    public function ldapHelperChangePw($username, $newPassword, $newPasswordCnt) {
      $user = wire('users')->get("name=$username");
      $dn = $this->userdn;

      if($user instanceof NullPage) return "User dosn't exist.";
      if(empty($newPassword) | empty($newPasswordCnt)) {
        $this->message = "Not enouth values.";
        return false;
      }

      // check if new Password match
      if($newPassword != $newPasswordCnt) {
        $this->message = "Password doesn't match.";
        return false;
      }

      // Connect to LDAP Server
      if(!$con = $this->ldapHelperConnect()) {
        $this->message = "Verbindungsprobleme, bitte versuche es später noch einmal.";
        return false;
      }

      // Search for the user in the ldap Directory
      $user_search = ldap_search($con,$dn,"(|(uid=$username))"); // Ressource ID:
      $user_get = ldap_get_entries($con,$user_search); // array with all entries
      $user_entry = ldap_first_entry($con, $user_search); // array with ressource
      $user_dn = ldap_get_dn($con, $user_entry); // uid=$userid,ou=People,dc=ffmyk,dc=de
      $user_id = $user_get[0]["uid"][0]; // the user ID
      $user_givenName = $user_get[0]["givenname"][0]; // Given name from user
      $user_search_array = array("*", "ou", "uid");
      $user_search_filter = "(|(uid=$user_id))";
      $user_search_opt = ldap_search($con, $user_dn, $user_search_filter, $user_search_array); // array ressource
      $user_get_opt = ldap_get_entries($con,$user_search_opt); // array with the entrie

      $encoded_password = $this->makeSshaPassword($newPassword);

      $entry = array();
      $entry["userPassword"] = "$encoded_password";

      // bind mit passendem dn für aktulisierenden Zugriff
      if(!$r= $this->ldapHelperWrite($con)) {
        $this->message = "Verbindungsprobleme.";
        return false;
      }

      if(ldap_modify($con,$user_dn,$entry) === false){
        $error = ldap_error($con);
        $errno = ldap_errno($con);
        $this->log->save('ldap_helper', "$username - Your password cannot be change, please contact the administrator.");
        $this->log->save('ldap_helper', "$errno - $error");
        $this->message = "Dein Password konnte nicht gespeichert werden. Bitte kontaktiere den Administrator";
        return false;
      } else {
        $this->message = "Dein Password wurde geändert, du kannst dich nun mit dem neuen Password einloggen.";
        return true;
      }
    }

    public function ldapHelperDeleteUser(HookEvent $event) {
        $page = $event->arguments("page");

        if (!$page->id && !$page->template->name === "user") return; // Return if User is not deleted
        $username = $page->name;

        // bind mit passendem dn für aktualisierenden Zugriff
        $connect = $this->ldapHelperConnect();
        $r= $this->ldapHelperWrite($connect);

        $user_search = ldap_search($connect,"dc=ffmyk,dc=de","(|(uid=$user))");
        $user_entry = ldap_first_entry($connect, $user_search);

        // Delete User from LDAP
        $dn= ldap_get_dn($connect, $user_entry);
        if(ldap_delete( $connect , $dn )){
          $this->message("Delete LDAP User: $username");
          $this->log->save('ldap_helper', "Delete User: $username");
        } else {
          $errno = ldap_errno($connect); // Error code
          $error = ldap_error($connect); // Error text
          $this->message("Delete LDAP User failed $error ($errno)");
          $this->log->save('ldap_helper', "Delete User failed $error ($errno)");
        }

        ldap_close($connect);
    }

    protected function makeSshaPassword($password) {
      mt_srand((double)microtime()*1000000);
      $salt = pack("CCCC", mt_rand(), mt_rand(), mt_rand(), mt_rand());
      $hash = "{SSHA}" . base64_encode(pack("H*", sha1($password . $salt)) . $salt);
      return $hash;
    }

    protected function ldapHelperConnect() {
      if ($this->debugMode) ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7); // LDAP DebugMode
      if(!$connection = ldap_connect($this->host, $this->port)) return $this->log->save('LDAP', printf(__("Failed to connect LDAP %d pages."), ldap_err2str(ldap_errno($connection)))); // Annahme: der LDAP Server befindet sich auf diesem Host
      if ($this->useV3) ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3); // Set LDAP_PROTOCOL_VERSION
      return $connection;
    }

    protected function ldapHelperWrite($ds) {
      if(empty($ds)) return;
      return ldap_bind($ds,"$this->admin", "$this->secret");
    }

    public function validateConfiguration() {
        $connection = $this->ldapHelperConnect();
        if (!$connection) {
            return;
        }
        $this->message($this->_('Successfully connected to LDAP server.'));
        $bind = $this->ldapHelperWrite($connection);
        $ldapMessage = ldap_err2str(ldap_errno($connection));
        $this->message("Bind $ldapMessage");
    }

    public function hookModuleSave(HookEvent $event) {
       $className = $event->arguments [0];
       if ($className != get_class($this)) return;
       $this->validateConfiguration();
     }

    static public function getModuleConfigInputfields(array $data) {
        $inputfields = new InputfieldWrapper ();

        $hostField = wire('modules')->get('InputfieldText');
        $hostField->name = 'host';
        $hostField->columnWidth = 80;
        $hostField->label = __('LDAP Server');
        $hostField->required = 1;
        if (isset ($data ['host']))
            $hostField->value = $data ['host'];
        $hostField->description = __('The hostname of your LDAP server. This can be either an ip address or a domain name. Supply a custom port (different than 389) separated with a colon. Examples: 10.0.0.1, controller.domain.com, controller.domain.com:388');
        $inputfields->add($hostField);

        $useSSLField = wire('modules')->get('InputfieldCheckbox');
        $useSSLField->name = 'useSSL';
        $useSSLField->columnWidth = 20;
        $useSSLField->label = __('Use SSL?');
        $useSSLField->description = __('Connects to the LDAP Server via SSL.');
        if (isset ($data ['useSSL']) && $data ['useSSL'] == 1)
            $useSSLField->checked = 1;
        $inputfields->add($useSSLField);

        $defaultLoginDomainField = wire('modules')->get('InputfieldText');
        $defaultLoginDomainField->name = 'loginDomain';
        $defaultLoginDomainField->label = __('Default Login Domain');
        $defaultLoginDomainField->columnWidth = 40;
        $defaultLoginDomainField->required = 1;
        if (isset ($data ['loginDomain']))
            $defaultLoginDomainField->value = $data ['loginDomain'];
        $defaultLoginDomainField->description = __('This is the domain name used by default if the user does not supply a domain name. It will be added to the username, e.g. username@domainname.com');
        $inputfields->add($defaultLoginDomainField);

        $adminLoginField = wire('modules')->get('InputfieldText');
        $adminLoginField->name = 'userdn';
        $adminLoginField->columnWidth = 40;
        $adminLoginField->label = __('LDAP User DN');
        $adminLoginField->required = 1;
        if (isset ($data ['userdn']))
            $adminLoginField->value = $data ['userdn'];
        $adminLoginField->description = __('The DN for the user. Example: ou=Person,dc=domain,dc=com');
        $inputfields->add($adminLoginField);

        $useV3Field = wire('modules')->get('InputfieldCheckbox');
        $useV3Field->name = 'useV3';
        $useV3Field->columnWidth = 20;
        $useV3Field->label = __('Use Protocol V3?');
        $useV3Field->description = __('Use LDAP Protocol V3.');
        if (isset ($data ['useV3']) && $data ['useV3'] == 1)
            $useV3Field->checked = 1;
        $inputfields->add($useV3Field);

        $adminLoginField = wire('modules')->get('InputfieldText');
        $adminLoginField->name = 'admin';
        $adminLoginField->columnWidth = 50;
        $adminLoginField->label = __('LDAP Admin DN');
        $adminLoginField->required = 1;
        if (isset ($data ['admin']))
            $adminLoginField->value = $data ['admin'];
        $adminLoginField->description = __('An LDAP account with access to add/edit/delet users. Example: cn=accountmanager,dc=domain,dc=com');
        $inputfields->add($adminLoginField);

        $secretField = wire('modules')->get('InputfieldText');
        $secretField->attr ('type','password');
        $secretField->name = 'secret';
        $secretField->columnWidth = 50;
        $secretField->label = __('LDAP Admin Secret');
        $secretField->description = __('Needet to use with LDAP Admin to add/edit/delete users.');
        if (isset ($data ['secret']))
            $secretField->value = $data ['secret'];
        $inputfields->add($secretField);

        $ldapSettingField = wire('modules')->get('InputfieldTextarea');
        $ldapSettingField->name = 'ldapSetting';
        $ldapSettingField->columnWidth = 50;
        $ldapSettingField->label = __('Entry Setup');
        $ldapSettingField->description = __('The Entry Setup for LDAP Users. Includes objectClass, uid, cn... one per Line. Set Variables @variable@.');
        if (isset ($data ['ldapSetting']))
            $ldapSettingField->value = $data ['ldapSetting'];
        $inputfields->add($ldapSettingField);

        $userDefaultRolesField = wire('modules')->get('InputfieldPageListSelectMultiple');
        $userDefaultRolesField->name = 'userDefaultRoles';
        $userDefaultRolesField->label = __('Default Roles for new Users');
        $userDefaultRolesField->description = __('These user roles will be applied to all new LDAP users. Please note that the guest role is applied automatically.');
        $userDefaultRolesField->parent_id = wire('roles')->getGuestRole()->parent_id;
        if (isset ($data ['userDefaultRoles']))
            $userDefaultRolesField->value = $data ['userDefaultRoles'];
        $inputfields->add($userDefaultRolesField);

        $debugField = wire('modules')->get('InputfieldCheckbox');
        $debugField->name = 'debug';
        $debugField->collapsed = Inputfield::collapsedYes;
        $debugField->label = __('Debug Mode');
        $debugField->description = __('Turns on the debug mode so you can see the output of PHP\'s ldap module in the apache log.');
        if (isset ($data ['debug']) && $data ['debug'] == 1)
            $debugField->checked = 1;
        $inputfields->add($debugField);

        return $inputfields;
    }
}
