<?php
/*
Plugin Name: Simple LDAP Auth
Plugin URI: 
Description: This plugin enables use of LDAP provider for authentication
Version: 1.0
Author: k3a
Author URI: http://k3a.me
*/
// Thanks to nicwaller (https://github.com/nicwaller) for cas plugin I used as a reference!

// No direct call
if( !defined( 'YOURLS_ABSPATH' ) ) die();


// returns true if the environment is set up right
function ldapauth_environment_check() {
	$required_params = array(
		'LDAPAUTH_HOST', // ldap host
		//'LDAPAUTH_PORT', // ldap port
		'LDAPAUTH_BASE', // base ldap path
		//'LDAPAUTH_USERNAME_FIELD', // field to check the username against
	);

	foreach ($required_params as $pname) {
		if ( !defined( $pname ) ) {
			$message = 'Missing defined parameter '.$pname.' in plugin '. $thisplugname;
			error_log($message);
			return false;
		}
	}

	if ( !defined( 'LDAPAUTH_PORT' ) )
		define( 'LDAPAUTH_PORT', 389 );

	if ( !defined( 'LDAPAUTH_USERNAME_FIELD' ) )
		define( 'LDAPAUTH_USERNAME_FIELD', 'uid' );

	if ( !defined( 'LDAPAUTH_ALL_USERS_ADMIN' ) )
		define( 'LDAPAUTH_ALL_USERS_ADMIN', true );

	if ( !defined( 'LDAPAUTH_ADD_NEW' ) )
		define( 'LDAPAUTH_ADD_NEW', false );

	if ( !defined( 'LDAPAUTH_USERCACHE_TYPE' ) )
		define( 'LDAPAUTH_USERCACHE_TYPE', 1 );

		
	global $ldapauth_authorized_admins;
	if ( !isset( $ldapauth_authorized_admins ) ) {
		if ( !LDAPAUTH_ALL_USERS_ADMIN ) {
			error_log('Undefined $ldapauth_authorized_admins');
		}
		$ldapauth_authorized_admins = array();
	}

	return true;
}


yourls_add_filter( 'is_valid_user', 'ldapauth_is_valid_user' );

function ldapauth_shuffle_assoc($list) {
    if (!is_array($list)) return $list;

    $keys = array_keys($list);
    shuffle($keys);
    $random = array();
    foreach ($keys as $key) {
        $random[$key] = $list[$key];
    }
    return $random;
}

// return list of Active Directory Ldap servers that are associated with a site and service
// example for $site =  = '_ldap._tcp.corporate._sites.company.com'
function ldapauth_get_ad_servers_for_site() {
    $results = [];
    $ad_servers = dns_get_record(LDAPAUTH_DNS_SITES_AND_SERVICES, DNS_SRV, $authns, $addtl);
    foreach ($ad_servers as $ad_server) {
        array_push($results, $ad_server['target']);
    }
    $results = ldapauth_shuffle_assoc($results);  #randomize the order
    return $results;
}

// returns ldap connection
function ldapauth_get_ldap_connection() {
    if (defined('LDAPAUTH_DNS_SITES_AND_SERVICES')) {
        $connection = NULL;
        $ldap_servers = ldapauth_get_ad_servers_for_site();
        foreach ($ldap_servers as $ldap_server) {
            $ldap_address = LDAPAUTH_HOST . $ldap_server;
            try {
                $temp_conn = ldap_connect($ldap_address, LDAPAUTH_PORT);  # ldap_connect doesn't actually connect it just checks for plausiable parameters.  Only ldap_bind connects
                if ($temp_conn) {
                    $connection = $temp_conn;
                    break;
                } else {
                    error_log('Warning, unable to connect to: ' . $ldap_address . ' on port ' . LDAPAUTH_PORT .  '.  ' . ldap_error($temp_conn));
                }
            } catch (Exception $e) {
                error_log('Warning, unable to connect to: ' . $ldap_address . ' on port ' . LDAPAUTH_PORT . '.  ' . __FILE__, __FUNCTION__,$e->getMessage());
            }
        }

        if ($connection) {
            return $connection;
        } else {
            die("Cannot connect to LDAP for site and service " . LDAPAUTH_DNS_SITES_AND_SERVICES);
        }

    } else {
        return ldap_connect(LDAPAUTH_HOST, LDAPAUTH_PORT);
    }
}

// returns true/false
function ldapauth_is_valid_user( $value ) {
	global $yourls_user_passwords;
	global $ydb;

	// Always check & set early
	if ( !ldapauth_environment_check() ) {
		die( 'Invalid configuration for YOURLS LDAP plugin. Check PHP error log.' );
	}

	if( LDAPAUTH_USERCACHE_TYPE == 1) {
		$ldapauth_usercache = $ydb->option['ldapauth_usercache'];
	}
	
	// no point in continuing if the user has already been validated by core
	if ($value) {
		ldapauth_debug("Returning from ldapauth_is_valid_user as user is already validated");
		return $value;
	}
	
	// session is only needed if we don't use usercache
	if (empty(LDAPAUTH_USERCACHE_TYPE)) {
		@session_start();
	}

	if ( empty(LDAPAUTH_USERCACHE_TYPE) && isset( $_SESSION['LDAPAUTH_AUTH_USER'] ) ) {
		// already authenticated...
		$username = $_SESSION['LDAPAUTH_AUTH_USER'];
		// why is this checked here, but not before the cookie is set?
		if ( ldapauth_is_authorized_user( $username ) ) { 
		if( !isset($yourls_user_passwords[$username]) ) {
			// set a dummy password to work around the "Stealing cookies" problem
			// we prepend with 'phpass:' to avoid YOURLS trying to auto-encrypt it and
			// write it to user/config.php
			ldapauth_debug('Setting dummy entry in $yourls_user_passwords for user ' . $username);
			$yourls_user_passwords[$username]='phpass:ThereIsNoPasswordButHey,WhoCares?';
		}
			yourls_set_user( $_SESSION['LDAPAUTH_AUTH_USER'] );
			return true;
		} else {
			return $username.' is not admin user.';
		}
	} else if ( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] )
			&& !empty( $_REQUEST['username'] ) && !empty( $_REQUEST['password']  ) ) {

		// try to authenticate
		$ldapConnection = ldapauth_get_ldap_connection();
		if (!$ldapConnection) die("Cannot connect to LDAP " . LDAPAUTH_HOST);
		ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3);
		//ldap_set_option($ldapConnection, LDAP_OPT_REFERRALS, 0);
		
		// should we to try and bind using the credentials being logged in with?
		if (defined('LDAPAUTH_BIND_WITH_USER_TEMPLATE')) {
			$bindRDN = sprintf(LDAPAUTH_BIND_WITH_USER_TEMPLATE, $_REQUEST['username']);
			if (!($ldapSuccess = @ldap_bind($ldapConnection, $bindRDN, $_REQUEST['password']))) {
				error_log('Couldn\'t bind to LDAP server with user ' . $bindRDN);
				return $value;
			}
		} 
		
		// Check if using a privileged user account to search - only if not already bound with current user
		if (defined('LDAPAUTH_SEARCH_USER') && defined('LDAPAUTH_SEARCH_PASS') && empty($ldapSuccess)) {
			if (!@ldap_bind($ldapConnection, LDAPAUTH_SEARCH_USER, LDAPAUTH_SEARCH_PASS)) {
				die('Couldn\'t bind search user ' . LDAPAUTH_SEARCH_USER);
			}
		}

		// Limit the attrs to the ones we need
		$attrs = array('dn', LDAPAUTH_USERNAME_FIELD);
		if (defined('LDAPAUTH_GROUP_ATTR'))
			array_push($attrs, LDAPAUTH_GROUP_ATTR);
		
		$searchDn = ldap_search($ldapConnection, LDAPAUTH_BASE, LDAPAUTH_USERNAME_FIELD . "=" . $_REQUEST['username'], $attrs );
		if (!$searchDn) return $value;
		$searchResult = ldap_get_entries($ldapConnection, $searchDn);
		if (!$searchResult) return $value;
		$userDn = $searchResult[0]['dn'];
		if (!$userDn && !$ldapSuccess) return $value;	
		if (empty($ldapSuccess)) { // we don't need to do this if we already bound using username and LDAPAUTH_BIND_WITH_USER_TEMPLATE
		  $ldapSuccess = @ldap_bind($ldapConnection, $userDn, $_REQUEST['password']);
		}
	
		// success?
		if ($ldapSuccess)
		{
			// are we checking group auth?
			if (defined('LDAPAUTH_GROUP_ATTR') && defined('LDAPAUTH_GROUP_REQ')) {
				if (!array_key_exists(LDAPAUTH_GROUP_ATTR, $searchResult[0])) die('Not in any LDAP groups');
				
				$in_group = false;
				$groups_to_check = explode(";", strtolower(LDAPAUTH_GROUP_REQ)); // This is now an array
				
				foreach($searchResult[0][LDAPAUTH_GROUP_ATTR] as $grps) {
			 		if (in_array(strtolower($grps), $groups_to_check)) { $in_group = true; break;  }
				}
			
				if (!$in_group) die('Not in admin group');
			}
			
			// attribute index returned by ldap_get_entries is lowercased (http://php.net/manual/en/function.ldap-get-entries.php)
			$username = $searchResult[0][strtolower(LDAPAUTH_USERNAME_FIELD)][0];
			yourls_set_user($username);
			
			if (LDAPAUTH_ADD_NEW && !array_key_exists($username, $yourls_user_passwords)) {
				ldapauth_create_user( $username, $_REQUEST['password'] );
			}
			
			if (LDAPAUTH_USERCACHE_TYPE == 1) {
				// store the current user credentials in our cache. This cuts down calls to the LDAP 
				// server, and allows API keys to work with LDAP users
				$ldapauth_usercache[$username] = 'phpass:' . ldapauth_hash_password($_REQUEST['password']);
				yourls_update_option('ldapauth_usercache', $ldapauth_usercache);
			}

			$yourls_user_passwords[$username] = ldapauth_hash_password($_REQUEST['password']);
			if (empty(LDAPAUTH_USERCACHE_TYPE)) {
				$_SESSION['LDAPAUTH_AUTH_USER'] = $username;
			}
			return true;
		} else {
			error_log("No LDAP success");
		}
	}

	return $value;
}

function ldapauth_is_authorized_user( $username ) {	
	// by default, anybody who can authenticate is also
	// authorized as an administrator.
	if ( LDAPAUTH_ALL_USERS_ADMIN ) {
		return true;
	}

	// users listed in config.php are admin users. let them in.
	global $ldapauth_authorized_admins;
	if ( in_array( $username, $ldapauth_authorized_admins ) ) {
		return true;
	}

	// not an admin user
	return false;
}

yourls_add_action( 'logout', 'ldapauth_logout_hook' );

function ldapauth_logout_hook( $args ) {
	if (empty(LDAPAUTH_USERCACHE_TYPE)) {
	      unset($_SESSION['LDAPAUTH_AUTH_USER']);
	      setcookie('PHPSESSID', '', 0, '/');
	}
}

/* This action, called as early as possible, retrieves our cache of LDAP users and 
 * merges it with $yourls_user_passwords. This enables core to do the authorisation
 * of previously seen LDAP users, and also means that API signatures for LDAP users 
 * will work. Users that exist in both users/config.php and LDAP will need to use 
 * their LDAP passwords
 */

yourls_add_action ('plugins_loaded', 'ldapauth_merge_users');
function ldapauth_merge_users() {
	global $ydb;
	global $yourls_user_passwords;
	if ( !ldapauth_environment_check() ) {
		die( 'Invalid configuration for YOURLS LDAP plugin. Check PHP error log.' );
	}
	if(LDAPAUTH_USERCACHE_TYPE==1 && isset($ydb->option['ldapauth_usercache'])) {
		ldapauth_debug("Merging text file users and cached LDAP users");
		$yourls_user_passwords = array_merge($yourls_user_passwords, $ydb->option['ldapauth_usercache']);
	}
}
/**
 * Create user in config file
 * Code reused from yourls_hash_passwords_now()
 */
function ldapauth_create_user( $user, $new_password ) {
	$configdata = file_get_contents( YOURLS_CONFIGFILE );
	if ( $configdata == FALSE )	{
		die('Couldn\'t read the config file');
	}
	
	if (!is_writable(YOURLS_CONFIGFILE))
		die('Can\'t write to config file');
		
	$pass_hash = ldapauth_hash_password($new_password);
	$user_line = "\t'$user' => 'phpass:$pass_hash' /* Password encrypted by YOURLS */,";
	
	// Add the user on a new line after the start of the passwords array
	$new_contents = preg_replace('/(yourls_user_passwords\s=\sarray\()/',  '$0 ' . PHP_EOL . $user_line, $configdata, -1, $count);
	
	if ($count === 0) {
		die('Couldn\'t add user, plugin may not be compatible with YourLS version');
	} else if ($count > 1) {
		die('Added user more than once. Check config file.');
	}
		
	$success = file_put_contents( YOURLS_CONFIGFILE, $new_contents );
	if ( $success === false ) {
		die('Unable to save config file');
	}
	
	return $pass_hash;
}
/**
 * Hashes password the same way as yourls_hash_passwords_now()
 **/
function ldapauth_hash_password ($password) {
	$pass_hash = yourls_phpass_hash( $password );
	// PHP would interpret $ as a variable, so replace it in storage.
	$pass_hash = str_replace( '$', '!', $pass_hash );
	
	return $pass_hash;
}
function ldapauth_debug ($msg) {
	if (defined('LDAPAUTH_DEBUG') && LDAPAUTH_DEBUG) { 
		error_log("yourls_ldap_auth: " . $msg);
	}
}