From c4a6c2c3d0509d2d1a27a095b87073b1975e2527 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Mon, 21 Nov 2011 13:01:44 +0000
Subject: [PATCH] ldap: Common base authproc filter + new filter.

This patch adds a common "base filter" for creating authproc filters
that access a LDAP directory, and it adds a filter
(ldap:AttributeAddUsersGroups) for adding group information.

Thanks to Ryan Panning for implementing this!

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2986 44740490-163a-0410-bde0-09ae8108e29a
---
 lib/SimpleSAML/Auth/LDAP.php                  | 111 +++++-
 modules/ldap/docs/ldap.txt                    | 324 +++++++++++++++++-
 .../lib/Auth/Process/AttributeAddFromLDAP.php | 234 ++++---------
 .../Auth/Process/AttributeAddUsersGroups.php  | 295 ++++++++++++++++
 modules/ldap/lib/Auth/Process/BaseFilter.php  | 277 +++++++++++++++
 5 files changed, 1076 insertions(+), 165 deletions(-)
 create mode 100644 modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php
 create mode 100644 modules/ldap/lib/Auth/Process/BaseFilter.php

diff --git a/lib/SimpleSAML/Auth/LDAP.php b/lib/SimpleSAML/Auth/LDAP.php
index 828f97fa5..e1cc2282a 100644
--- a/lib/SimpleSAML/Auth/LDAP.php
+++ b/lib/SimpleSAML/Auth/LDAP.php
@@ -48,9 +48,10 @@ class SimpleSAML_Auth_LDAP {
 	 * @param bool $enable_tls
 	 * @param bool $debug
 	 * @param int $timeout
+	 * @param int $port
 	 */
 	// TODO: Flesh out documentation.
-	public function __construct($hostname, $enable_tls = TRUE, $debug = FALSE, $timeout = 0) {
+	public function __construct($hostname, $enable_tls = TRUE, $debug = FALSE, $timeout = 0, $port = 389) {
 
 		// Debug.
 		SimpleSAML_Logger::debug('Library - LDAP __construct(): Setup LDAP with ' .
@@ -72,7 +73,7 @@ class SimpleSAML_Auth_LDAP {
 		 * Prepare a connection for to this LDAP server. Note that this function
 		 * doesn't actually connect to the server.
 		 */
-		$this->ldap = @ldap_connect($hostname);
+		$this->ldap = @ldap_connect($hostname, $port);
 		if ($this->ldap == FALSE)
 			throw new $this->makeException('Library - LDAP __construct(): Unable to connect to \'' . $hostname . '\'', ERR_INTERNAL);
 
@@ -281,6 +282,85 @@ class SimpleSAML_Auth_LDAP {
 	}
 
 
+	/**
+	 * This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory()
+	 * method, but could be used for other LDAP search needs. It will search LDAP and return all the entries.
+	 *
+	 * @throws Exception
+	 * @param string|array $bases
+	 * @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter, or a raw filter string
+	 * @param string|array $attributes Array of attributes requested from LDAP
+	 * @param bool $and If multiple filters defined, then either bind them with & or |
+	 * @param bool $escape Weather to escape the filter values or not
+	 * @return array
+	 */
+	public function searchformultiple($bases, $filters, $attributes = array(), $and = TRUE, $escape = TRUE) {
+
+		// Escape the filter values, if requested
+		if ($escape) {
+			$filters = $this->escape_filter_value($filters, FALSE);
+		}
+
+		// Build search filter
+		$filter = '';
+		if (is_array($filters)) {
+			foreach ($filters as $attribute => $value) {
+				$filter .= "($attribute=$value)";
+			}
+			if (count($filters) > 1) {
+				$filter = ($and ? '(&' : '(|') . $filter . ')';
+			}
+		} elseif (is_string($filters)) {
+			$filter = $filters;
+		}
+
+		// Verify filter was created
+		if ($filter == '' || $filter == '(=)') {
+			throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
+		}
+
+		// Verify at least one base was passed
+		$bases = (array) $bases;
+		if (empty($bases)) {
+			throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
+		}
+
+		// Search each base until result is found
+		$result = FALSE;
+		foreach ($bases as $base) {
+			$result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
+			if ($result !== FALSE) break;
+		}
+
+		// Verify that a result was found in one of the bases
+		if ($result === FALSE) {
+			throw $this->makeException(
+				'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) [' .
+				implode('; ', $bases) . '] with filter [' . $filter . ']. LDAP error [' .
+				ldap_error($this->ldap) . ']'
+			);
+		} elseif (@ldap_count_entries($this->ldap, $result) < 1) {
+			throw $this->makeException(
+				'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) [' .
+				implode('; ', $bases) . '] with filter [' . $filter . ']',
+				ERR_NO_USER
+			);
+		}
+
+		// Get all results
+		$results = ldap_get_entries($this->ldap, $result);
+		if ($results === FALSE) {
+			throw $this->makeException(
+				'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
+			);
+		}
+
+		// Remove the count and return
+		unset($results['count']);
+		return $results;
+	}
+
+
 	/**
 	 * Bind to LDAP with a specific DN and password. Simple wrapper around
 	 * ldap_bind() with some additional logging.
@@ -349,6 +429,33 @@ class SimpleSAML_Auth_LDAP {
 	}
 
 
+	/**
+	 * Applies an LDAP option to the current connection.
+	 *
+	 * @throws Exception
+	 * @param $option
+	 * @param $value
+	 * @return void
+	 */
+	public function setOption($option, $value) {
+
+		// Attempt to set the LDAP option
+		if (!@ldap_set_option($this->ldap, $option, $value)) {
+			throw $this->makeException(
+				'ldap:LdapConnection->setOption : Failed to set LDAP option [' .
+				$option . '] with the value [' . $value . '] error: ' . ldap_error($this->ldap),
+				ERR_INTERNAL
+			);
+		}
+
+		// Log debug message
+		SimpleSAML_Logger::debug(
+			'ldap:LdapConnection->setOption : Set the LDAP option [' .
+			$option . '] with the value [' . $value . ']'
+		);
+	}
+
+
 	/**
 	 * Search a given DN for attributes, and return the resulting associative
 	 * array.
diff --git a/modules/ldap/docs/ldap.txt b/modules/ldap/docs/ldap.txt
index 2cdf47754..946c3737d 100644
--- a/modules/ldap/docs/ldap.txt
+++ b/modules/ldap/docs/ldap.txt
@@ -2,7 +2,8 @@ LDAP module
 ===========
 
 The LDAP module provides a method for authenticating users against a
-LDAP server. There are two seperate authentication modules:
+LDAP server. There are two separate authentication modules and two
+authentication process filters:
 
 
 `ldap:LDAP`
@@ -11,6 +12,11 @@ LDAP server. There are two seperate authentication modules:
 `ldap:LDAPMulti`
 : Allow the user to chose one LDAP server to authenticate against.
 
+`ldap:AttributeAddFromLDAP`
+: Adds an attribute value from LDAP to the request
+
+`ldap:AttributeAddUsersGroups`
+: Add a attribute in the request with all the users group memberships
 
 `ldap:LDAP`
 -----------
@@ -220,3 +226,319 @@ All options from the `ldap:LDAP` configuration can be used in each
 group, and you should refer to the documentation for that module for
 more information about available options.
 
+
+`ldap:AttributeAddFromLDAP`
+------------------------------
+
+Filter to add attributes to the identity by executing a query against
+an LDAP directory. In addition to all the configuration options available
+in the ldap:AttributeAddUsersGroups filter (below), these are the filter
+specific configuration options:
+
+
+	50 = array(
+		'class' => 'ldap:AttributeAddFromLDAP',
+
+
+		/**
+		 * The attribute name used when adding the LDAP values
+		 * to the request attributes.
+		 *
+		 * Default: NULL
+		 * Require: Yes
+		 */
+		'attribute.new' => 'my_ldap_attrib',
+
+
+		/**
+		 * When searching LDAP, this is the attribute to retrieve
+		 * and add to the request attributes.
+		 *
+		 * Default: NULL
+		 * Require: Yes
+		 */
+		'search.attribute' => 'displayName',
+
+
+		/**
+		 * The attribute name used when adding the LDAP values
+		 * to the request attributes.
+		 *
+		 * Note: Variable substitution will be performed on this option.
+		 *       Any attribute in the identity can be substituted by surrounding
+		 *       it with percent symbols (%). For instance %cn% would be replaced
+		 *       with the CN of the user.
+		 *
+		 * Default: NULL
+		 * Require: Yes
+		 */
+		'search.filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)',
+	);
+
+
+### Backwards Compatability ###
+
+The filter option names have recently changed, however the old config names will be
+converted to the new names automatically. That way any existing/older config's
+should still work. Below are the old config names and their new names:
+
+array(
+	'ldap_host'             => 'ldap.hostname',
+	'ldap_port'             => 'ldap.port',
+	'ldap_bind_user'        => 'ldap.username',
+	'ldap_bind_pwd'         => 'ldap.password',
+	'userid_attribute'      => 'attribute.username',
+	'ldap_search_base_dn'   => 'ldap.basedn',
+	'ldap_search_filter'    => 'search.filter',
+	'ldap_search_attribute' => 'search.attribute',
+	'new_attribute_name'    => 'attribute.new'
+)
+
+
+### Example ###
+
+This is the most basic configuration possible. It will look at the
+authsource for all LDAP connection information and queries LDAP for
+the specific attribute.
+
+	50 => array(
+		'class' => 'ldapAttributeAddUsersGroups',
+		'authsource' => 'example-ldap',
+		'attribute.new' => 'my_ldap_attribute',
+		'search.attribute' => 'displayName'
+		'search.filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)'
+	)
+
+If no authsource is available then you can specify the connection info
+using the filter configuration. Note: All of the options below are not
+required, see the config options for ldap:AttributeAddUsersGroups below.
+
+	50 => array(
+		'class' => 'ldapAttributeAddUsersGroups',
+		'ldap.hostname' => 'ldap.example.org',
+		'ldap.username' => 'CN=LDAP User,CN=Users,DC=example,DC=org',
+		'ldap.password' => 'Abc123',
+		'ldap.basedn' => 'DC=example,DC=org',
+		'attribute.new' => 'my_ldap_attribute',
+		'search.attribute' => 'displayName'
+		'search.filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)'
+	)
+
+
+
+
+`ldap:AttributeAddUsersGroups`
+------------------------------
+
+This filter will add the logged in users LDAP group memberships to
+a specified request attribute. Although most LDAP products have a
+memberOf attribute which only lists the direct membership relations,
+this filter checks those relation for "sub" groups, recursively
+checking the hierarchy for all groups the user would technically be
+a member of. This can be helpful for other filters to know. Below is
+a listing of all configuration options and their details.
+
+
+	50 => array(
+		'class' => 'ldap:AttributeAddUsersGroups',
+
+
+		/**
+		 * LDAP connection settings can be retrieved from an ldap:LDAP
+		 * authsource. Specify the authsource name here to pull that
+		 * data from the authsources.php file in the config folder.
+		 *
+		 * Note: ldap:LDAPMulti is not supported as the SimpleSAMLphp
+		 *       framework does not pass any information about which
+		 *       LDAP source the user selected.
+		 *
+		 * Default: NULL
+		 * Require: No
+		 */
+		'authsource' => NULL,
+		'authsource' => 'example-ldap',
+
+
+		/**
+		 * This is the attribute name which the users groups will be
+		 * added to. If the attribute exists in the request then the
+		 * filter will attempt to add them.
+		 *
+		 * Default: 'groups'
+		 * Required: No
+		 */
+		'attribute.groups' => 'groups',
+
+
+		/**
+		 * The base DN used to search LDAP. May not be needed if searching
+		 * LDAP using the standard method, meaning that no Product is specified.
+		 * Can be listed as a single string for one base, else an array of
+		 * strings for multiple bases.
+		 *
+		 * Default: ''
+		 * Required: No
+		 * AuthSource: search.base
+		 */
+		'ldap.basedn' => '',
+		'ldap.basedn' => 'DC=example,DC=org',
+		'ldap.basedn' => array(
+			'OU=Staff,DC=example,DC=org',
+			'OU=Students,DC=example,DC=org'
+		),
+
+
+		/**
+		 * Set to TRUE to enable LDAP debug level. Passed to
+		 * the LDAP connection class.
+		 *
+		 * Default: FALSE
+		 * Required: No
+		 * AuthSource: debug
+		 */
+		'ldap.debug' => FALSE,
+		'ldap.debug' => TRUE,
+
+
+		/**
+		 * Set to TRUE to force the LDAP connection to use TLS.
+		 *
+		 * Note: If ldaps:// is specified in the hostname then it
+		 *       will automatically use TLS.
+		 *
+		 * Default: FALSE
+		 * Required: No
+		 * AuthSource: enable_tls
+		 */
+		'ldap.enable_tls' => FALSE,
+		'ldap.enable_tls' => TRUE,
+
+
+		/**
+		 * This is the hostname string of LDAP server(s) to try
+		 * and connect to. It should be the same format as the
+		 * LDAP authsource hostname as it is passed to that class.
+		 *
+		 * Note: Multiple servers are separated by a space.
+		 *
+		 * Default: NULL
+		 * Required: Yes, unless authsource is used
+		 * AuthSource: hostname
+		 */
+		'ldap.hostname' => 'ldap.example.org',
+		'ldap.hostname' => 'ad1.example.org ad2.example.org',
+
+
+		/**
+		 * This is the password used to bind to LDAP.
+		 *
+		 * Default: NULL
+		 * Required: No, only if required for binding.
+		 * AuthSource: search.password OR priv.password
+		 */
+		'ldap.password' => 'Abc123',
+
+
+		/**
+		 * By specifying the directory service product name, the number
+		 * of LDAP queries can be dramatically reduced. The reason is
+		 * that most products have a special query to recursively search
+		 * group membership.
+		 *
+		 * Note: Only ActiveDirectory is currently supported.
+		 *
+		 * Default: ''
+		 * Required: No
+		 */
+		'ldap.product' => '',
+		'ldap.product' => 'ActiveDirectory',
+
+
+		/**
+		 * The LDAP timeout value passed to the LDAP connection class.
+		 *
+		 * Default: 0
+		 * Required: No
+		 * AuthSource: timeout
+		 */
+		'ldap.timeout' => 0,
+		'ldap.timeout' => 30,
+
+
+		/**
+		 * This is the username used to bind to LDAP with.
+		 * More than likely will need to be in the DN of
+		 * user binding to LDAP.
+		 *
+		 * Default: NULL
+		 * Required: No, only if required for binding.
+		 * AuthSource: search.username OR priv.username
+		 */
+		'ldap.username' => 'CN=LDAP User,CN=Users,DC=example,DC=org',
+
+
+		/**
+		 * The following attribute.* and type.* configuration options
+		 * define the LDAP schema and should only be defined/modified
+		 * if the schema has been modified or the LDAP product used
+		 * uses other attribute names. By default, the schema is setup
+		 * for ActiveDirectory.
+		 *
+		 * Defaults: Listed Below
+		 * Required: No
+		 */
+		'attribute.dn' => 'distinguishedName',
+		'attribute.groups' => 'groups', // Also noted above
+		'attribute.member' => 'member',
+		'attribute.memberof' => 'memberOf',
+		'attribute.groupname' => 'name',
+		'attribute.type' => 'objectClass',
+		'attribute.username' => 'sAMAccountName',
+
+
+		/**
+		 * As mentioned above, these can be changed if the LDAP schema
+		 * has been modified. These list the Object/Entry Type for a given
+		 * DN, in relation to the 'attribute.type' config option above.
+		 * These are used to determine the type of entry.
+		 *
+		 * Defaults: Listed Below
+		 * Required: No
+		 */
+		'type.group' => 'group',
+		'type.user' => 'user',
+	)
+
+
+### Example ###
+
+This is the most basic configuration possible. It will look at the
+authsource for all LDAP connection information and manually search
+the hierarchy for the users group memberships.
+
+	50 => array(
+		'class' => 'ldapAttributeAddUsersGroups',
+		'authsource' => 'example-ldap'
+	)
+
+By making one small change we can optimize the filter to use better
+group search methods and eliminate un-needed LDAP queries.
+
+	50 => array(
+		'class' => 'ldapAttributeAddUsersGroups',
+		'authsource' => 'example-ldap',
+		'ldap.product' => 'ActiveDirectory'
+	)
+
+If no authsource is available then you can specify the connection info
+using the filter configuration. Note: All of the options below are not
+required, see the config info above for details.
+
+	50 => array(
+		'class' => 'ldapAttributeAddUsersGroups',
+		'ldap.hostname' => 'ldap.example.org',
+		'ldap.username' => 'CN=LDAP User,CN=Users,DC=example,DC=org',
+		'ldap.password' => 'Abc123',
+		'ldap.basedn' => 'DC=example,DC=org'
+	)
+
diff --git a/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php b/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php
index fc08fb3c7..c731a2f06 100644
--- a/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php
+++ b/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php
@@ -1,101 +1,53 @@
 <?php
-// +---------------------------------------------------+
-// | PHP Version: 5.2.x                                |
-// +---------------------------------------------------+
-// | simpleSAMLphp Auth Proc for adding additional     |
-// | identity attributes from a secondary LDAP query   |
-// +---------------------------------------------------+
-// |                                                   |
-// | This Auth Proc needs the following configuration  |
-// | directives set in order to function properly.     |
-// |                                                   |
-// | 'ldap_host' the hostname of the LDAP server       |
-// |                                                   |
-// | 'ldap_port' the port for the LDAP process         |
-// |                                                   |
-// | 'ldap_bind_user' the user to bind as (optional)   |
-// |                                                   |
-// | 'ldap_bind_pwd' the password for the bind user    |
-// |                 required if ldap_bind_user is     |
-// |                 specified                         |
-// |                                                   |
-// | 'userid_attribute' the attribute you will use to  |
-// |    filter results                                 |
-// |                                                   |
-// | 'ldap_search_base_dn' the search base             |
-// |                                                   |
-// | 'ldap_search_filter' the search filter.           |
-// | NOTE: Variable substitution will be performed on  |
-// |      ldap_search_filter. Any attribute in the     |
-// |      identity can be substituted by surrounding   |
-// |      it with percent symbols (%). For instance    |
-// |      %cn% would be replaced with the cn of the    |
-// |      user.                                        |
-// |                                                   |
-// | 'ldap_search_attribute' the name of the attribute |
-// |      in the search results that you want to add   |
-// |      to the identity attributes                   |
-// |                                                   |
-// | 'new_attribute_name' the name you want the newly  |
-// |      added attribute to be called when it's added |
-// |                                                   |
-// | EXAMPLE                                           |
-// |                                                   |
-// | 'authproc' => array(
-// |    50 => array(
-// |    'class' => 'ldap:AttributeAddFromLDAP',
-// |    'ldap_host' => 'ldap.example.org',
-// |    'ldap_port' => '389',
-// |    'ldap_bind_user' => 'ldap_bind_user',
-// |    'ldap_bind_pwd' => 'ldap_bind_pwd',
-// |    'userid_attribute' => 'cn',
-// |    'ldap_search_base_dn' => 'cn=security_tags,dc=example,dc=org',
-// |    'ldap_search_filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)',
-// |    'ldap_search_attribute' => 'displayname',
-// |    'new_attribute_name' => 'security_tags',
-// |    ),
-// | ),
-// |                                                   |
-// | This will cause the Auth Proc to query the LDAP   |
-// | looking for all the security tags that the        |
-// | current user is assigned. It will take the value  |
-// | contained in displayname and put it into a mutli- |
-// | value attribute called security_tags              |
-// |                                                   |
-// +---------------------------------------------------+
-// | Author: Steve Moitozo II <steve_moitozo@jaars.org>|
-// | Created: 20100513                                 |
-// +---------------------------------------------------+
-// | 20100920 Steve Moitozo II                         |
-// |    - incorporated feedback from Olav Morken to    |
-// |    prep code for inclusion in SimpleSAMLphp distro|
-// |    - moved call to ldap_set_options() inside test |
-// |    for $ds                                        |
-// |    - added the output of ldap_error() to the      |
-// |    exceptions                                     |
-// |    - reduced some of the nested ifs               |
-// |    - added support for multiple values            |
-// |    - added support for anonymous binds            |
-// |    - added escaping of search filter and attribute|
-// +---------------------------------------------------+
-
 
 /**
  * Filter to add attributes to the identity by executing a query against an LDAP directory
  *
+ * Original Author: Steve Moitozo II <steve_moitozo@jaars.org>
+ * Created: 20100513
+ * Updated: 20100920 Steve Moitozo II
+ *          - incorporated feedback from Olav Morken to prep code for inclusion in SimpleSAMLphp distro
+ *          - moved call to ldap_set_options() inside test for $ds
+ *          - added the output of ldap_error() to the exceptions
+ *          - reduced some of the nested ifs
+ *          - added support for multiple values
+ *          - added support for anonymous binds
+ *          - added escaping of search filter and attribute
+ * Updated: 20111118 Ryan Panning
+ *          - Updated the class to use BaseFilter which reuses LDAP connection features
+ *          - Added conversion of original filter option names for backwards-compatibility
+ *          - Updated the constructor to use the new config method
+ *          - Updated the process method to use the new config variable names
  *
- * @author Steve Moitozo, JAARS, Inc.
+ * @author Steve Moitozo, JAARS, Inc., Ryan Panning
  * @package simpleSAMLphp
  * @version $Id$
  */
-class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_ProcessingFilter {
+class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends sspmod_ldap_Auth_Process_BaseFilter {
+
 
 	/**
-	 * The configuration.
+	 * Name of the attribute to add LDAP values to
 	 *
-	 * Associative array of strings.
+	 * @var string
 	 */
-	private $config = array();
+	protected $new_attribute;
+
+
+	/**
+	 * LDAP attribute to add to the request attributes
+	 *
+	 * @var string
+	 */
+	protected $search_attribute;
+
+
+	/**
+	 * LDAP search filter to use in the LDAP query
+	 *
+	 * @var string
+	 */
+	protected $search_filter;
 
 
 	/**
@@ -105,51 +57,38 @@ class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_Proc
 	 * @param mixed $reserved  For future use.
 	 */
 	public function __construct($config, $reserved) {
-		parent::__construct($config, $reserved);
 
-		assert('is_array($config)');
-
-		$reqConfigVars = array(
-			'ldap_host',
-			'ldap_port',
-			'ldap_bind_user',
-			'ldap_bind_pwd',
-			'userid_attribute',
-			'ldap_search_base_dn',
-			'ldap_search_filter',
-			'ldap_search_attribute',
-			'new_attribute_name'
+		// For backwards compatibility, check for old config names
+		if (isset($config['ldap_host']))             $config['ldap.hostname']      = $config['ldap_host'];
+		if (isset($config['ldap_port']))             $config['ldap.port']          = $config['ldap_port'];
+		if (isset($config['ldap_bind_user']))        $config['ldap.username']      = $config['ldap_bind_user'];
+		if (isset($config['ldap_bind_pwd']))         $config['ldap.password']      = $config['ldap_bind_pwd'];
+		if (isset($config['userid_attribute']))      $config['attribute.username'] = $config['userid_attribute'];
+		if (isset($config['ldap_search_base_dn']))   $config['ldap.basedn']        = $config['ldap_search_base_dn'];
+		if (isset($config['ldap_search_filter']))    $config['search.filter']      = $config['ldap_search_filter'];
+		if (isset($config['ldap_search_attribute'])) $config['search.attribute']   = $config['ldap_search_attribute'];
+		if (isset($config['new_attribute_name']))    $config['attribute.new']      = $config['new_attribute_name'];
+
+		// Remove the old config names
+		unset(
+			$config['ldap_host'],
+			$config['ldap_port'],
+			$config['ldap_bind_user'],
+			$config['ldap_bind_pwd'],
+			$config['userid_attribute'],
+			$config['ldap_search_base_dn'],
+			$config['ldap_search_filter'],
+			$config['ldap_search_attribute'],
+			$config['new_attribute_name']
 		);
 
-		foreach($config as $name => $values){
-			if(!is_string($name)){
-				throw new Exception('Invalid attribute name: ' . var_export($name, TRUE));
-			}
-
-			// make sure the name is in the list of required config variables
-			if(in_array($name,$reqConfigVars)){
-
-
-				if(is_array($values)){
-					throw new Exception('Configuration parameters must not contain arrays. The value for parameter "'.$name.'" is an array.');
-				}
-
-				$this->config[$name] = $values;
-
-			}else{
-				// unknown config variable, skipping
-				throw new Exception('Unknown configuration variable "'.$name.'"');
-			}
-
-
-		}
+		// Now that we checked for BC, run the parent constructor
+		parent::__construct($config, $reserved);
 
-		$configVarsSet = array_keys($this->config);
-		foreach($reqConfigVars as $configVar){
-			if(!in_array($configVar, $configVarsSet)){
-				throw new Exception('Please provide a value for configuration parameter "'.$configVar.'".');
-			}
-		}
+		// Get filter specific config options
+		$this->new_attribute    = $this->config->getString('attribute.new');
+		$this->search_attribute = $this->config->getString('search.attribute');
+		$this->search_filter    = $this->config->getString('search.filter');
 	}
 
 
@@ -163,9 +102,10 @@ class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_Proc
 		assert('array_key_exists("Attributes", $request)');
 
 		$attributes =& $request['Attributes'];
+		$map =& $this->attribute_map;
 
-		if(!isset($attributes[$this->config['userid_attribute']])){
-			throw new Exception('The user\'s identity does not have an attribute called "'.$this->config['userid_attribute'].'"');
+		if(!isset($attributes[$map['username']])){
+			throw new Exception('The user\'s identity does not have an attribute called "'.$map['username'].'"');
 		}
 
 
@@ -183,53 +123,23 @@ class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_Proc
 		}
 
 		// merge the attributes into the ldap_search_filter
-		$merged_ldap_search_filter = str_replace($arrSearch, $arrReplace, $this->config['ldap_search_filter']);
-
-
-		// connect to the LDAP directory
-		$ds = ldap_connect($this->config['ldap_host'], $this->config['ldap_port']);
-
-		if(!$ds){
-			throw new Exception('Failed to initialize LDAP connection parameters ('.ldap_error(NULL).')');
-		}
-
-		ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
-
-		// if we're supposed to bind as a specified user
-		if((isset($this->config['ldap_bind_user']) && $this->config['ldap_bind_user']) && 
-		   (isset($this->config['ldap_bind_pwd'])  && $this->config['ldap_bind_pwd'])){
-
-			// bind to the directory as the specified user
-			if(!ldap_bind($ds, $this->config['ldap_bind_user'], $this->config['ldap_bind_pwd'])){
-				throw new Exception($this->config['ldap_bind_user'].' failed to bind against '.$this->config['ldap_host'].' ('.ldap_error($ds).')');
-			}
-
-		}else{	// bind to the directory anonymously
-
-			if(!ldap_bind($ds)){
-				throw new Exception('Failed to anonymously bind against '.$this->config['ldap_host'].' ('.ldap_error($ds).')');
-			}
-
-		}
+		$filter = str_replace($arrSearch, $arrReplace, $this->search_filter);
 
 		// search for matching entries
-		$sr = ldap_search($ds, $this->config['ldap_search_base_dn'], $merged_ldap_search_filter, array($this->config['ldap_search_attribute']));
-		$entries = ldap_get_entries($ds, $sr);
+		$entries = $this->getLdap()->searchformultiple($this->base_dn, $filter, (array) $this->search_attribute, TRUE, FALSE);
 
 		// handle [multiple] values
 		if(is_array($entries) && is_array($entries[0])){
 			$results = array();
 			foreach($entries as $entry){
-				$entry = $entry[strtolower($this->config['ldap_search_attribute'])];
+				$entry = $entry[strtolower($this->search_attribute)];
 				for($i = 0; $i < $entry['count']; $i++){
 					$results[] = $entry[$i];
 				}
 			}
-			$attributes[$this->config['new_attribute_name']] = array_values($results);
+			$attributes[$this->new_attribute] = array_values($results);
 		}
 
-		ldap_unbind($ds);
-
 	}
 
 }
diff --git a/modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php b/modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php
new file mode 100644
index 000000000..06872b140
--- /dev/null
+++ b/modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php
@@ -0,0 +1,295 @@
+<?php
+
+/**
+ * Does a reverse membership lookup on the logged in user,
+ * looking for groups it is a member of and adds them to
+ * a defined attribute, in DN format.
+ *
+ * @author Ryan Panning <panman@traileyes.com>
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_ldap_Auth_Process_AttributeAddUsersGroups extends sspmod_ldap_Auth_Process_BaseFilter {
+
+
+	/**
+	 * This is run when the filter is processed by SimpleSAML.
+	 * It will attempt to find the current users groups using
+	 * the best method possible for the LDAP product. The groups
+	 * are then added to the request attributes.
+	 *
+	 * @throws SimpleSAML_Error_Exception
+	 * @param $request
+	 */
+	public function process(&$request) {
+		assert('is_array($request)');
+		assert('array_key_exists("Attributes", $request)');
+
+		// Log the process
+		SimpleSAML_Logger::debug(
+			$this->title . 'Attempting to get the users groups...'
+		);
+
+		// Reference the attributes, just to make the names shorter
+		$attributes =& $request['Attributes'];
+		$map =& $this->attribute_map;
+
+		// Get the users groups from LDAP
+		$groups = $this->getGroups($attributes);
+
+		// Make the array if it is not set already
+		if (!isset($attributes[$map['groups']])) {
+			$attributes[$map['groups']] = array();
+		}
+
+		// Must be an array, else cannot merge groups
+		if (!is_array($attributes[$map['groups']])) {
+			throw new SimpleSAML_Error_Exception(
+				$this->title . 'The group attribute [' . $map['groups'] .
+				'] is not an array of group DNs. ' . $this->var_export($attributes[$map['groups']])
+			);
+		}
+
+		// Add the users group(s)
+		$group_attribute =& $attributes[$map['groups']];
+		$group_attribute = array_merge($group_attribute, $groups);
+		$group_attribute = array_unique($group_attribute);
+
+		// All done
+		SimpleSAML_Logger::debug(
+			$this->title . 'Added users groups to the group attribute [' .
+			$map['groups'] . ']: ' . implode('; ', $groups)
+		);
+	}
+
+
+	/**
+	 * This section of code was broken out because the child
+	 * filter AuthorizeByGroup can use this method as well.
+	 * Based on the LDAP product, it will do an optimized search
+	 * using the required attribute values from the user to
+	 * get their group membership, recursively.
+	 *
+	 * @throws SimpleSAML_Error_Exception
+	 * @param array $attributes
+	 * @return array
+	 */
+	protected function getGroups(array $attributes) {
+
+		// Reference the map, just to make the name shorter
+		$map =& $this->attribute_map;
+
+		// Log the request
+		SimpleSAML_Logger::debug(
+			$this->title . 'Checking for groups based on the best method for the LDAP product.'
+		);
+
+		// Based on the directory service, search LDAP for groups.
+		// If any attributes are needed, prepare them before calling search method
+		switch ($this->product) {
+
+			case 'ACTIVEDIRECTORY':
+
+				// Log the AD specific search
+				SimpleSAML_Logger::debug(
+					$this->title . 'Searching LDAP using ActiveDirectory specific method.'
+				);
+
+				// Make sure the defined dn attribute exists
+				if (!isset($attributes[$map['dn']])) {
+					throw new SimpleSAML_Error_Exception(
+						$this->title . 'The DN attribute [' . $map['dn'] .
+						'] is not defined in the users Attributes: ' . implode(', ', array_keys($attributes))
+					);
+				}
+
+				// DN attribute must have a value
+				if (!isset($attributes[$map['dn']][0]) || !$attributes[$map['dn']][0]) {
+					throw new SimpleSAML_Error_Exception(
+						$this->title . 'The DN attribute [' . $map['dn'] .
+						'] does not have a [0] value defined. ' . $this->var_export($attributes[$map['dn']])
+					);
+				}
+
+				// Pass to the AD specific search
+				$groups = $this->searchActiveDirectory($attributes[$map['dn']][0]);
+				break;
+
+			default:
+
+				// Log the general search
+				SimpleSAML_Logger::debug(
+					$this->title . 'Searching LDAP using the default search method.'
+				);
+
+				// Make sure the defined memberOf attribute exists
+				if (!isset($attributes[$map['memberof']])) {
+					throw new SimpleSAML_Error_Exception(
+						$this->title . 'The memberof attribute [' . $map['memberof'] .
+						'] is not defined in the users Attributes: ' . implode(', ', array_keys($attributes))
+					);
+				}
+
+				// MemberOf must be an array of group DN's
+				if (!is_array($attributes[$map['memberof']])) {
+					throw new SimpleSAML_Error_Exception(
+						$this->title . 'The memberof attribute [' . $map['memberof'] .
+						'] is not an array of group DNs. ' . $this->var_export($attributes[$map['memberof']])
+					);
+				}
+
+				// Search for the users group membership, recursively
+				$groups = $this->search($attributes[$map['memberof']]);
+		}
+
+		// All done
+		SimpleSAML_Logger::debug(
+			$this->title . 'User found to be a member of the groups:' . implode('; ', $groups)
+		);
+		return $groups;
+	}
+
+
+	/**
+	 * Looks for groups from the list of DN's passed. Also
+	 * recursively searches groups for further membership.
+	 * Avoids loops by only searching a DN once. Returns
+	 * the list of groups found.
+	 *
+	 * @param array $memberof
+	 * @return array
+	 */
+	protected function search($memberof) {
+		assert('is_array($memberof)');
+
+		// Used to determine what DN's have already been searched
+		static $searched = array();
+
+		// Init the groups variable
+		$groups = array();
+
+		// Shorten the variable name
+		$map =& $this->attribute_map;
+
+		// Log the search
+		SimpleSAML_Logger::debug(
+			$this->title . 'Checking DNs for groups.' .
+			' DNs: '. implode('; ', $memberof) .
+			' Attributes: ' . $map['memberof'] . ', ' . $map['type'] .
+			' Group Type: ' . $this->type_map['group']
+		);
+
+		// Check each DN of the passed memberOf
+		foreach ($memberof as $dn) {
+
+			// Avoid infinite loops, only need to check a DN once
+			if (isset($searched[$dn])) {
+				continue;
+			}
+
+			// Track all DN's that are searched
+			// Use DN for key as well, isset() is faster than in_array()
+			$searched[$dn] = $dn;
+
+			// Query LDAP for the attribute values for the DN
+			try {
+				$attributes = $this->getLdap()->getAttributes($dn, array($map['memberof'], $map['type']));
+			} catch (SimpleSAML_Error_AuthSource $e) {
+				continue; // DN must not exist, just continue. Logged by the LDAP object
+			}
+
+			// Only look for groups
+			if (!in_array($this->type_map['group'], $attributes[$map['type']])) {
+				continue;
+			}
+
+			// Add to found groups array
+			$groups[] = $dn;
+
+			// Recursively search "sub" groups
+			$groups = array_merge($groups, $this->search($attributes[$map['memberof']]));
+		}
+
+		// Return only the unique group names
+		return array_unique($groups);
+	}
+
+
+	/**
+	 * Searches LDAP using a ActiveDirectory specific filter,
+	 * looking for group membership for the users DN. Returns
+	 * the list of group DNs retrieved.
+	 *
+	 * @param string $dn
+	 * @return array
+	 */
+	protected function searchActiveDirectory($dn) {
+		assert('is_string($dn) && $dn != ""');
+
+		// Shorten the variable name
+		$map =& $this->attribute_map;
+
+		// Log the search
+		SimpleSAML_Logger::debug(
+			$this->title . 'Searching ActiveDirectory group membership.' .
+			' DN: ' . $dn .
+			' DN Attribute: ' . $map['dn'] .
+			' Member Attribute: ' . $map['member'] .
+			' Type Attribute: ' . $map['type'] .
+			' Type Value: ' . $this->type_map['group'] .
+			' Base: ' . implode('; ', $this->base_dn)
+		);
+
+		// AD connections should have this set
+		$this->getLdap()->setOption(LDAP_OPT_REFERRALS, 0);
+
+		// Search AD with the specific recursive flag
+		try {
+			$entries = $this->getLdap()->searchformultiple(
+				$this->base_dn,
+				array($map['type'] => $this->type_map['group'], $map['member'] . ':1.2.840.113556.1.4.1941:' => $dn),
+				array($map['dn'])
+			);
+
+		// The search may throw an exception if no entries
+		// are found, unlikely but possible.
+		} catch (SimpleSAML_Error_UserNotFound $e) {
+			return array();
+		}
+
+		//Init the groups
+		$groups = array();
+
+		// Check each entry..
+		foreach ($entries as $entry) {
+
+			// Check for the DN using the original attribute name
+			if (isset($entry[$map['dn']][0])) {
+				$groups[] = $entry[$map['dn']][0];
+				continue;
+			}
+
+			// Sometimes the returned attribute names are lowercase
+			if (isset($entry[strtolower($map['dn'])][0])) {
+				$groups[] = $entry[strtolower($map['dn'])][0];
+				continue;
+			}
+
+			// AD queries also seem to return the objects dn by default
+			if (isset($entry['dn'])) {
+				$groups[] = $entry['dn'];
+				continue;
+			}
+
+			// Could not find DN, log and continue
+			SimpleSAML_Logger::notice(
+				$this->title . 'The DN attribute [' .
+				implode(', ', array($map['dn'], strtolower($map['dn']), 'dn')) .
+				'] could not be found in the entry. ' . $this->var_export($entry)
+			);
+		}
+
+		// All done
+		return $groups;
+	}
+}
diff --git a/modules/ldap/lib/Auth/Process/BaseFilter.php b/modules/ldap/lib/Auth/Process/BaseFilter.php
new file mode 100644
index 000000000..0e92aa820
--- /dev/null
+++ b/modules/ldap/lib/Auth/Process/BaseFilter.php
@@ -0,0 +1,277 @@
+<?php
+
+/**
+ * This base LDAP filter class can be extended to enable real
+ * filter classes direct access to the authsource ldap config
+ * and connects to the ldap server.
+ *
+ * @author Ryan Panning <panman@traileyes.com>
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+abstract class sspmod_ldap_Auth_Process_BaseFilter extends SimpleSAML_Auth_ProcessingFilter {
+
+	/**
+	 * List of attribute "alias's" linked to the real attribute
+	 * name. Used for abstraction / configuration of the LDAP
+	 * attribute names, which may change between dir service.
+	 *
+	 * @var array
+	 */
+	protected $attribute_map;
+
+
+	/**
+	 * The base DN of the LDAP connection. Used when searching
+	 * the LDAP server.
+	 *
+	 * @var string|array
+	 */
+	protected $base_dn;
+
+
+	/**
+	 * The construct method will change the filter config into
+	 * a SimpleSAML_Configuration object and store it here for
+	 * later use, if needed.
+	 *
+	 * @var SimpleSAML_Configuration
+	 */
+	protected $config;
+
+
+	/**
+	 * Instance, object of the ldap connection. Stored here to
+	 * be access later during processing.
+	 *
+	 * @var sspmod_ldap_LdapConnection
+	 */
+	private $ldap;
+
+
+	/**
+	 * Many times a LDAP product specific query can be used to
+	 * speed up or reduce the filter process. This helps the
+	 * child classes determine the product used to optimize
+	 * those queries.
+	 *
+	 * @var string
+	 */
+	protected $product;
+
+
+	/**
+	 * The class "title" used in logging and exception messages.
+	 * This should be prepended to the beginning of the message.
+	 *
+	 * @var string
+	 */
+	protected $title = 'ldap:BaseFilter : ';
+
+
+	/**
+	 * List of LDAP object types, used to determine the type of
+	 * object that a DN references.
+	 *
+	 * @var array
+	 */
+	protected $type_map;
+
+
+	/**
+	 * Checks the authsource, if defined, for configuration values
+	 * to the LDAP server. Then sets up the LDAP connection for the
+	 * instance/object and stores everything in class members.
+	 *
+	 * @throws SimpleSAML_Error_Exception
+	 * @param array $config
+	 * @param $reserved
+	 */
+	public function __construct(&$config, $reserved) {
+		parent::__construct($config, $reserved);
+
+		// Change the class $title to match it's true name
+		// This way if the class is extended the proper name is used
+		$classname = get_class($this);
+		$classname = explode('_', $classname);
+		$this->title = 'ldap:' . end($classname) . ' : ';
+
+		// Log the construction
+		SimpleSAML_Logger::debug(
+			$this->title . 'Creating and configuring the filter.'
+		);
+
+		// If an authsource was defined (an not empty string)...
+		if (isset($config['authsource']) && $config['authsource']) {
+
+			// Log the authsource request
+			SimpleSAML_Logger::debug(
+				$this->title . 'Attempting to get configuration values from authsource [' .
+				$config['authsource'] . ']'
+			);
+
+			// Get the authsources file, which should contain the config
+			$authsource = SimpleSAML_Configuration::getConfig('authsources.php');
+
+			// Verify that the authsource config exists
+			if (!$authsource->hasValue($config['authsource'])) {
+				throw new SimpleSAML_Error_Exception(
+					$this->title . 'Authsource [' . $config['authsource'] .
+					'] defined in filter parameters not found in authsources.php'
+				);
+			}
+
+			// Get just the specified authsource config values
+			$authsource = $authsource->getConfigItem($config['authsource']);
+			$authsource = $authsource->toArray();
+
+			// Make sure it is an ldap source
+			// TODO: Support ldap:LDAPMulti, if possible
+			if (@$authsource[0] != 'ldap:LDAP') {
+				throw new SimpleSAML_Error_Exception(
+					$this->title . 'Authsource [' . $config['authsource'] .
+					'] specified in filter parameters is not an ldap:LDAP type'
+				);
+			}
+
+			// Build the authsource config
+			$authconfig = array();
+			$authconfig['ldap.hostname']   = @$authsource['hostname'];
+			$authconfig['ldap.enable_tls'] = @$authsource['enable_tls'];
+			$authconfig['ldap.timeout']    = @$authsource['timeout'];
+			$authconfig['ldap.debug']      = @$authsource['debug'];
+			$authconfig['ldap.basedn']     = (@$authsource['search.enable'] ? @$authsource['search.base'] : NULL);
+			$authconfig['ldap.username']   = (@$authsource['search.enable'] ? @$authsource['search.username'] : NULL);
+			$authconfig['ldap.password']   = (@$authsource['search.enable'] ? @$authsource['search.password'] : NULL);
+			$authconfig['ldap.username']   = (@$authsource['priv.read'] ? @$authsource['priv.username'] : $authconfig['ldap.username']);
+			$authconfig['ldap.password']   = (@$authsource['priv.read'] ? @$authsource['priv.password'] : $authconfig['ldap.password']);
+
+			// Only set the username attribute if the authsource specifies one attribute
+			if (@$authsource['search.enable'] && is_array(@$authsource['search.attributes'])
+			     && count($authsource['search.attributes']) == 1) {
+				$authconfig['attribute.username'] = reset($authsource['search.attributes']);
+			}
+
+			// Merge the authsource config with the filter config,
+			// but have the filter config override the authsource config
+			$config = array_merge($authconfig, $config);
+
+			// Authsource complete
+			SimpleSAML_Logger::debug(
+				$this->title . 'Retrieved authsource [' . $config['authsource'] .
+				'] configuration values: ' . $this->var_export($authconfig)
+			);
+		}
+
+		// Convert the config array to a config class,
+		// that way we can verify type and define defaults.
+		// Store in the instance in-case needed later, by a child class.
+		$this->config = SimpleSAML_Configuration::loadFromArray($config, 'ldap:AuthProcess');
+
+		// Set all the filter values, setting defaults if needed
+		$this->base_dn = $this->config->getArrayizeString('ldap.basedn', '');
+		$this->product = $this->config->getString('ldap.product', '');
+
+		// Cleanup the directory service, so that it is easier for
+		// child classes to determine service name consistently
+		$this->product = trim($this->product);
+		$this->product = strtoupper($this->product);
+
+		// Log the member values retrieved above
+		SimpleSAML_Logger::debug(
+			$this->title . 'Configuration values retrieved;' .
+			' BaseDN: ' . $this->var_export($this->base_dn) .
+			' Product: ' . $this->var_export($this->product)
+		);
+
+		// Setup the attribute map which will be used to search LDAP
+		$this->attribute_map = array(
+			'dn'       => $this->config->getString('attribute.dn', 'distinguishedName'),
+			'groups'   => $this->config->getString('attribute.groups', 'groups'),
+			'member'   => $this->config->getString('attribute.member', 'member'),
+			'memberof' => $this->config->getString('attribute.memberof', 'memberOf'),
+			'name'     => $this->config->getString('attribute.groupname', 'name'),
+			'type'     => $this->config->getString('attribute.type', 'objectClass'),
+			'username' => $this->config->getString('attribute.username', 'sAMAccountName')
+		);
+
+		// Log the attribute map
+		SimpleSAML_Logger::debug(
+			$this->title . 'Attribute map created: ' . $this->var_export($this->attribute_map)
+		);
+
+		// Setup the object type map which is used to determine a DNs' type
+		$this->type_map = array(
+			'group' => $this->config->getString('type.group', 'group'),
+			'user'  => $this->config->getString('type.user', 'user')
+		);
+
+		// Log the type map
+		SimpleSAML_Logger::debug(
+			$this->title . 'Type map created: ' . $this->var_export($this->type_map)
+		);
+	}
+
+
+	/**
+	 * Getter for the LDAP connection object. Created this getter
+	 * rather than setting in the constructor to avoid unnecessarily
+	 * connecting to LDAP when it might not be needed.
+	 *
+	 * @return sspmod_ldap_LdapConnection
+	 */
+	protected function getLdap() {
+
+		// Check if already connected
+		if ($this->ldap) {
+			return $this->ldap;
+		}
+
+		// Get the connection specific options
+		$hostname   = $this->config->getString('ldap.hostname');
+		$port       = $this->config->getInteger('ldap.port', 389);
+		$enable_tls = $this->config->getBoolean('ldap.enable_tls', FALSE);
+		$debug      = $this->config->getBoolean('ldap.debug', FALSE);
+		$timeout    = $this->config->getInteger('ldap.timeout', 0);
+		$username   = $this->config->getString('ldap.username', NULL);
+		$password   = $this->config->getString('ldap.password', NULL);
+
+		// Log the LDAP connection
+		SimpleSAML_Logger::debug(
+			$this->title . 'Connecting to LDAP server;' .
+			' Hostname: ' . $hostname .
+			' Port: ' . $port .
+			' Enable TLS: ' . ($enable_tls ? 'Yes' : 'No') .
+			' Debug: ' . ($debug ? 'Yes' : 'No') .
+			' Timeout: ' . $timeout .
+			' Username: ' . $username .
+			' Password: ' . str_repeat('*', strlen($password))
+		);
+
+		// Connect to the LDAP server to be queried during processing
+		$this->ldap = new SimpleSAML_Auth_LDAP($hostname, $enable_tls, $debug, $timeout, $port);
+		$this->ldap->bind($username, $password);
+
+		// All done
+		return $this->ldap;
+	}
+
+
+	/**
+	 * Local utility function to get details about a variable,
+	 * basically converting it to a string to be used in a log
+	 * message. The var_export() function returns several lines
+	 * so this will remove the new lines and trim each line.
+	 *
+	 * @param mixed $value
+	 * @return string
+	 */
+	protected function var_export($value) {
+		$export = var_export($value, TRUE);
+		$lines = explode("\n", $export);
+		foreach ($lines as &$line) {
+			$line = trim($line);
+		}
+		return implode(' ', $lines);
+	}
+}
-- 
GitLab