<?php
/**
 * ActiveDirectoryCommonirectoryCommon.php
 *
 * Common code from AD auth modules
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 * @package    LibreNMS
 * @link       http://librenms.org
 * @copyright  2018 Tony Murray
 * @author     Tony Murray <murraytony@gmail.com>
 */

namespace LibreNMS\Authentication;

use LibreNMS\Config;

trait ActiveDirectoryCommon
{
    protected function getUseridFromSid($sid)
    {
        return preg_replace('/.*-(\d+)$/', '$1', $sid);
    }

    protected function sidFromLdap($sid)
    {
        $sidUnpacked = unpack('H*hex', $sid);
        $sidHex = array_shift($sidUnpacked);
        $subAuths = unpack('H2/H2/n/N/V*', $sid);
        if (PHP_INT_SIZE <= 4) {
            for ($i = 1; $i <= count($subAuths); $i++) {
                if ($subAuths[$i] < 0) {
                    $subAuths[$i] = $subAuths[$i] + 0x100000000;
                }
            }
        }
        $revLevel = hexdec(substr($sidHex, 0, 2));
        $authIdent = hexdec(substr($sidHex, 4, 12));
        return 'S-'.$revLevel.'-'.$authIdent.'-'.implode('-', $subAuths);
    }

    protected function getCn($dn)
    {
        $dn = str_replace('\\,', '~C0mmA~', $dn);
        preg_match('/[^,]*/', $dn, $matches, PREG_OFFSET_CAPTURE, 3);
        return str_replace('~C0mmA~', ',', $matches[0][0]);
    }

    protected function getDn($samaccountname)
    {
        $link_identifier = $this->getConnection();
        $attributes = ['dn'];
        $result = ldap_search(
            $link_identifier,
            Config::get('auth_ad_base_dn'),
            $this->groupFilter($samaccountname),
            $attributes
        );
        $entries = ldap_get_entries($link_identifier, $result);
        if ($entries['count'] > 0) {
            return $entries[0]['dn'];
        } else {
            return '';
        }
    }

    protected function userFilter($username)
    {
        // don't return disabled users
        $user_filter = "(&(samaccountname=$username)(!(useraccountcontrol:1.2.840.113556.1.4.803:=2))";

        $extra = Config::get('auth_ad_user_filter');
        if ($extra) {
            $user_filter .= $extra;
        }
        $user_filter .= ')';

        return $user_filter;
    }

    protected function groupFilter($groupname)
    {
        $group_filter = "(samaccountname=$groupname)";

        $extra = Config::get('auth_ad_group_filter');
        if ($extra) {
            $group_filter = "(&$extra$group_filter)";
        }

        return $group_filter;
    }

    protected function getFullname($username)
    {
        $connection = $this->getConnection();
        $attributes = ['name'];
        $result = ldap_search(
            $connection,
            Config::get('auth_ad_base_dn'),
            $this->userFilter($username),
            $attributes
        );
        $entries = ldap_get_entries($connection, $result);
        if ($entries['count'] > 0) {
            $membername = $entries[0]['name'][0];
        } else {
            $membername = $username;
        }

        return $membername;
    }

    public function getGroupList()
    {
        $ldap_groups   = array();

        // show all Active Directory Users by default
        $default_group = 'Users';

        if (Config::has('auth_ad_group')) {
            if (Config::get('auth_ad_group') !== $default_group) {
                $ldap_groups[] = Config::get('auth_ad_group');
            }
        }

        if (!Config::has('auth_ad_groups') && !Config::has('auth_ad_group')) {
            $ldap_groups[] = $this->getDn($default_group);
        }

        foreach (Config::get('auth_ad_groups') as $key => $value) {
            $ldap_groups[] = $this->getDn($key);
        }

        return $ldap_groups;
    }

    public function getUserlist()
    {
        $connection = $this->getConnection();

        $userlist = array();
        $ldap_groups = $this->getGroupList();

        foreach ($ldap_groups as $ldap_group) {
            $search_filter = "(&(memberOf:1.2.840.113556.1.4.1941:=$ldap_group)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))";
            if (Config::get('auth_ad_user_filter')) {
                $search_filter = "(&" . Config::get('auth_ad_user_filter') . $search_filter .")";
            }
            $attributes = array('samaccountname', 'displayname', 'objectsid', 'mail');
            $search = ldap_search($connection, Config::get('auth_ad_base_dn'), $search_filter, $attributes);
            $results = ldap_get_entries($connection, $search);

            foreach ($results as $result) {
                if (isset($result['samaccountname'][0])) {
                    $userlist[$result['samaccountname'][0]] = $this->userFromAd($result);
                }
            }
        }

        return array_values($userlist);
    }

    /**
     * Generate a user array from an AD LDAP entry
     * Must have the attributes: objectsid, samaccountname, displayname, mail
     * @internal
     *
     * @param $entry
     * @return array
     */
    protected function userFromAd($entry)
    {
        return array(
            'user_id' => $this->getUseridFromSid($this->sidFromLdap($entry['objectsid'][0])),
            'username' => $entry['samaccountname'][0],
            'realname' => $entry['displayname'][0],
            'email' => isset($entry['mail'][0]) ? $entry['mail'][0] : null,
            'descr' => '',
            'level' => $this->getUserlevel($entry['samaccountname'][0]),
            'can_modify_passwd' => 0,
        );
    }

    public function getUser($user_id)
    {
        $connection = $this->getConnection();
        $domain_sid = $this->getDomainSid();

        $search_filter = "(&(objectcategory=person)(objectclass=user)(objectsid=$domain_sid-$user_id))";
        $attributes = array('samaccountname', 'displayname', 'objectsid', 'mail');
        $search = ldap_search($connection, Config::get('auth_ad_base_dn'), $search_filter, $attributes);
        $entry = ldap_get_entries($connection, $search);

        if (isset($entry[0]['samaccountname'][0])) {
            return $this->userFromAd($entry[0]);
        }

        return array();
    }

    protected function getDomainSid()
    {
        $connection = $this->getConnection();

        // Extract only the domain components
        $dn_candidate = preg_replace('/^.*?DC=/i', 'DC=', Config::get('auth_ad_base_dn'));

        $search = ldap_read(
            $connection,
            $dn_candidate,
            '(objectClass=*)',
            array('objectsid')
        );
        $entry = ldap_get_entries($connection, $search);
        return substr($this->sidFromLdap($entry[0]['objectsid'][0]), 0, 41);
    }

    /**
     * Provide a connected and bound ldap connection resource
     *
     * @return resource
     */
    abstract protected function getConnection();
}
