[Web] Fix mailbox editing when password is unchanged, fix adding new administrator (fixes #4054, fixes #4053); [Web] Update libs, add LDAP for future admin/domain admin authentication

This commit is contained in:
andryyy
2021-04-13 21:34:47 +02:00
parent 75c313ca92
commit 19843cc786
1623 changed files with 131949 additions and 2288 deletions
+194
View File
@@ -0,0 +1,194 @@
<?php
namespace Adldap;
use Adldap\Log\EventLogger;
use Adldap\Connections\Ldap;
use InvalidArgumentException;
use Adldap\Log\LogsInformation;
use Adldap\Connections\Provider;
use Adldap\Events\DispatchesEvents;
use Adldap\Connections\ProviderInterface;
use Adldap\Connections\ConnectionInterface;
use Adldap\Configuration\DomainConfiguration;
class Adldap implements AdldapInterface
{
use DispatchesEvents;
use LogsInformation;
/**
* The default provider name.
*
* @var string
*/
protected $default = 'default';
/**
* The connection providers.
*
* @var array
*/
protected $providers = [];
/**
* The events to register listeners for during initialization.
*
* @var array
*/
protected $listen = [
'Adldap\Auth\Events\*',
'Adldap\Query\Events\*',
'Adldap\Models\Events\*',
];
/**
* {@inheritdoc}
*/
public function __construct(array $providers = [])
{
foreach ($providers as $name => $config) {
$this->addProvider($config, $name);
}
if ($default = key($providers)) {
$this->setDefaultProvider($default);
}
$this->initEventLogger();
}
/**
* {@inheritdoc}
*/
public function addProvider($config, $name = 'default', ConnectionInterface $connection = null)
{
if ($this->isValidConfig($config)) {
$config = new Provider($config, $connection ?? new Ldap($name));
}
if ($config instanceof ProviderInterface) {
$this->providers[$name] = $config;
return $this;
}
throw new InvalidArgumentException(
"You must provide a configuration array or an instance of Adldap\Connections\ProviderInterface."
);
}
/**
* Determines if the given config is valid.
*
* @param mixed $config
*
* @return bool
*/
protected function isValidConfig($config)
{
return is_array($config) || $config instanceof DomainConfiguration;
}
/**
* {@inheritdoc}
*/
public function getProviders()
{
return $this->providers;
}
/**
* {@inheritdoc}
*/
public function getProvider($name)
{
if (array_key_exists($name, $this->providers)) {
return $this->providers[$name];
}
throw new AdldapException("The connection provider '$name' does not exist.");
}
/**
* {@inheritdoc}
*/
public function setDefaultProvider($name = 'default')
{
if ($this->getProvider($name) instanceof ProviderInterface) {
$this->default = $name;
}
}
/**
* {@inheritdoc}
*/
public function getDefaultProvider()
{
return $this->getProvider($this->default);
}
/**
* {@inheritdoc}
*/
public function removeProvider($name)
{
unset($this->providers[$name]);
return $this;
}
/**
* {@inheritdoc}
*/
public function connect($name = null, $username = null, $password = null)
{
$provider = $name ? $this->getProvider($name) : $this->getDefaultProvider();
return $provider->connect($username, $password);
}
/**
* {@inheritdoc}
*/
public function __call($method, $parameters)
{
$provider = $this->getDefaultProvider();
if (! $provider->getConnection()->isBound()) {
$provider->connect();
}
return call_user_func_array([$provider, $method], $parameters);
}
/**
* Initializes the event logger.
*
* @return void
*/
public function initEventLogger()
{
$dispatcher = static::getEventDispatcher();
$logger = $this->newEventLogger();
// We will go through each of our event wildcards and register their listener.
foreach ($this->listen as $event) {
$dispatcher->listen($event, function ($eventName, $events) use ($logger) {
foreach ($events as $event) {
$logger->log($event);
}
});
}
}
/**
* Returns a new event logger instance.
*
* @return EventLogger
*/
protected function newEventLogger()
{
return new EventLogger(static::getLogger());
}
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap;
class AdldapException extends \Exception
{
//
}
@@ -0,0 +1,91 @@
<?php
namespace Adldap;
use Adldap\Connections\ProviderInterface;
use Adldap\Connections\ConnectionInterface;
interface AdldapInterface
{
/**
* Add a provider by the specified name.
*
* @param mixed $configuration
* @param string $name
* @param ConnectionInterface $connection
*
* @throws \InvalidArgumentException When an invalid type is given as the configuration argument.
*
* @return $this
*/
public function addProvider($configuration, $name, ConnectionInterface $connection = null);
/**
* Returns all of the connection providers.
*
* @return array
*/
public function getProviders();
/**
* Retrieves a Provider using its specified name.
*
* @param string $name
*
* @throws AdldapException When the specified provider does not exist.
*
* @return ProviderInterface
*/
public function getProvider($name);
/**
* Sets the default provider.
*
* @param string $name
*
* @throws AdldapException When the specified provider does not exist.
*/
public function setDefaultProvider($name);
/**
* Retrieves the first default provider.
*
* @throws AdldapException When no default provider exists.
*
* @return ProviderInterface
*/
public function getDefaultProvider();
/**
* Removes a provider by the specified name.
*
* @param string $name
*
* @return $this
*/
public function removeProvider($name);
/**
* Connects to the specified provider.
*
* If no username and password is given, then the providers
* configured admin credentials are used.
*
* @param string|null $name
* @param string|null $username
* @param string|null $password
*
* @return ProviderInterface
*/
public function connect($name = null, $username = null, $password = null);
/**
* Call methods upon the default provider dynamically.
*
* @param string $method
* @param array $parameters
*
* @return mixed
*/
public function __call($method, $parameters);
}
@@ -0,0 +1,45 @@
<?php
namespace Adldap\Auth;
use Adldap\AdldapException;
use Adldap\Connections\DetailedError;
/**
* Class BindException.
*
* Thrown when binding to an LDAP connection fails.
*/
class BindException extends AdldapException
{
/**
* The detailed LDAP error.
*
* @var DetailedError
*/
protected $detailedError;
/**
* Sets the detailed error.
*
* @param DetailedError|null $error
*
* @return $this
*/
public function setDetailedError(DetailedError $error = null)
{
$this->detailedError = $error;
return $this;
}
/**
* Returns the detailed error.
*
* @return DetailedError|null
*/
public function getDetailedError()
{
return $this->detailedError;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Auth\Events;
class Attempting extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Auth\Events;
class Binding extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Auth\Events;
class Bound extends Event
{
//
}
@@ -0,0 +1,73 @@
<?php
namespace Adldap\Auth\Events;
use Adldap\Connections\ConnectionInterface;
abstract class Event
{
/**
* The connection that the username and password is being bound on.
*
* @var ConnectionInterface
*/
protected $connection;
/**
* The username that is being used for binding.
*
* @var string
*/
protected $username;
/**
* The password that is being used for binding.
*
* @var string
*/
protected $password;
/**
* Constructor.
*
* @param ConnectionInterface $connection
* @param string $username
* @param string $password
*/
public function __construct(ConnectionInterface $connection, $username, $password)
{
$this->connection = $connection;
$this->username = $username;
$this->password = $password;
}
/**
* Returns the events connection.
*
* @return ConnectionInterface
*/
public function getConnection()
{
return $this->connection;
}
/**
* Returns the authentication events username.
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Returns the authentication events password.
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Auth\Events;
class Failed extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Auth\Events;
class Passed extends Event
{
//
}
@@ -0,0 +1,259 @@
<?php
namespace Adldap\Auth;
use Exception;
use Throwable;
use Adldap\Auth\Events\Bound;
use Adldap\Auth\Events\Failed;
use Adldap\Auth\Events\Passed;
use Adldap\Auth\Events\Binding;
use Adldap\Auth\Events\Attempting;
use Adldap\Events\DispatcherInterface;
use Adldap\Connections\ConnectionInterface;
use Adldap\Configuration\DomainConfiguration;
/**
* Class Guard.
*
* Binds users to the current connection.
*/
class Guard implements GuardInterface
{
/**
* The connection to bind to.
*
* @var ConnectionInterface
*/
protected $connection;
/**
* The domain configuration to utilize.
*
* @var DomainConfiguration
*/
protected $configuration;
/**
* The event dispatcher.
*
* @var DispatcherInterface
*/
protected $events;
/**
* {@inheritdoc}
*/
public function __construct(ConnectionInterface $connection, DomainConfiguration $configuration)
{
$this->connection = $connection;
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
public function attempt($username, $password, $bindAsUser = false)
{
$this->validateCredentials($username, $password);
$this->fireAttemptingEvent($username, $password);
try {
$this->bind(
$this->applyPrefixAndSuffix($username),
$password
);
$result = true;
$this->firePassedEvent($username, $password);
} catch (BindException $e) {
// We'll catch the BindException here to allow
// developers to use a simple if / else
// using the attempt method.
$result = false;
}
// If we're not allowed to bind as the user,
// we'll rebind as administrator.
if ($bindAsUser === false) {
// We won't catch any BindException here so we can
// catch rebind failures. However this shouldn't
// occur if our credentials are correct
// in the first place.
$this->bindAsAdministrator();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function bind($username = null, $password = null)
{
$this->fireBindingEvent($username, $password);
try {
if (@$this->connection->bind($username, $password) === true) {
$this->fireBoundEvent($username, $password);
} else {
throw new Exception($this->connection->getLastError(), $this->connection->errNo());
}
} catch (Throwable $e) {
$this->fireFailedEvent($username, $password);
throw (new BindException($e->getMessage(), $e->getCode(), $e))
->setDetailedError($this->connection->getDetailedError());
}
}
/**
* {@inheritdoc}
*/
public function bindAsAdministrator()
{
$this->bind(
$this->configuration->get('username'),
$this->configuration->get('password')
);
}
/**
* Get the event dispatcher instance.
*
* @return DispatcherInterface
*/
public function getDispatcher()
{
return $this->events;
}
/**
* Sets the event dispatcher instance.
*
* @param DispatcherInterface $dispatcher
*
* @return void
*/
public function setDispatcher(DispatcherInterface $dispatcher)
{
$this->events = $dispatcher;
}
/**
* Applies the prefix and suffix to the given username.
*
* @param string $username
*
* @throws \Adldap\Configuration\ConfigurationException If account_suffix or account_prefix do not
* exist in the providers domain configuration
*
* @return string
*/
protected function applyPrefixAndSuffix($username)
{
$prefix = $this->configuration->get('account_prefix');
$suffix = $this->configuration->get('account_suffix');
return $prefix.$username.$suffix;
}
/**
* Validates the specified username and password from being empty.
*
* @param string $username
* @param string $password
*
* @throws PasswordRequiredException When the given password is empty.
* @throws UsernameRequiredException When the given username is empty.
*/
protected function validateCredentials($username, $password)
{
if (empty($username)) {
// Check for an empty username.
throw new UsernameRequiredException('A username must be specified.');
}
if (empty($password)) {
// Check for an empty password.
throw new PasswordRequiredException('A password must be specified.');
}
}
/**
* Fire the attempting event.
*
* @param string $username
* @param string $password
*
* @return void
*/
protected function fireAttemptingEvent($username, $password)
{
if (isset($this->events)) {
$this->events->fire(new Attempting($this->connection, $username, $password));
}
}
/**
* Fire the passed event.
*
* @param string $username
* @param string $password
*
* @return void
*/
protected function firePassedEvent($username, $password)
{
if (isset($this->events)) {
$this->events->fire(new Passed($this->connection, $username, $password));
}
}
/**
* Fire the failed event.
*
* @param string $username
* @param string $password
*
* @return void
*/
protected function fireFailedEvent($username, $password)
{
if (isset($this->events)) {
$this->events->fire(new Failed($this->connection, $username, $password));
}
}
/**
* Fire the binding event.
*
* @param string $username
* @param string $password
*
* @return void
*/
protected function fireBindingEvent($username, $password)
{
if (isset($this->events)) {
$this->events->fire(new Binding($this->connection, $username, $password));
}
}
/**
* Fire the bound event.
*
* @param string $username
* @param string $password
*
* @return void
*/
protected function fireBoundEvent($username, $password)
{
if (isset($this->events)) {
$this->events->fire(new Bound($this->connection, $username, $password));
}
}
}
@@ -0,0 +1,55 @@
<?php
namespace Adldap\Auth;
use Adldap\Connections\ConnectionInterface;
use Adldap\Configuration\DomainConfiguration;
interface GuardInterface
{
/**
* Constructor.
*
* @param ConnectionInterface $connection
* @param DomainConfiguration $configuration
*/
public function __construct(ConnectionInterface $connection, DomainConfiguration $configuration);
/**
* Authenticates a user using the specified credentials.
*
* @param string $username The users LDAP username.
* @param string $password The users LDAP password.
* @param bool $bindAsUser Whether or not to bind as the user.
*
* @throws \Adldap\Auth\BindException When re-binding to your LDAP server fails.
* @throws \Adldap\Auth\UsernameRequiredException When username is empty.
* @throws \Adldap\Auth\PasswordRequiredException When password is empty.
*
* @return bool
*/
public function attempt($username, $password, $bindAsUser = false);
/**
* Binds to the current connection using the inserted credentials.
*
* @param string|null $username
* @param string|null $password
*
* @throws \Adldap\Auth\BindException If binding to the LDAP server fails.
* @throws \Adldap\Connections\ConnectionException If upgrading the connection to TLS fails
*
* @return void
*/
public function bind($username = null, $password = null);
/**
* Binds to the current LDAP server using the
* configuration administrator credentials.
*
* @throws \Adldap\Auth\BindException When binding as your administrator account fails.
*
* @return void
*/
public function bindAsAdministrator();
}
@@ -0,0 +1,10 @@
<?php
namespace Adldap\Auth;
use Adldap\AdldapException;
class PasswordRequiredException extends AdldapException
{
//
}
@@ -0,0 +1,10 @@
<?php
namespace Adldap\Auth;
use Adldap\AdldapException;
class UsernameRequiredException extends AdldapException
{
//
}
@@ -0,0 +1,16 @@
<?php
namespace Adldap\Configuration;
use Adldap\AdldapException;
/**
* Class ConfigurationException.
*
* Thrown when a configuration value does not exist, or a
* configuration value being set is not valid.
*/
class ConfigurationException extends AdldapException
{
//
}
@@ -0,0 +1,161 @@
<?php
namespace Adldap\Configuration;
use Adldap\Schemas\ActiveDirectory;
use Adldap\Connections\ConnectionInterface;
/**
* Class DomainConfiguration.
*
* Contains an array of configuration options for a single LDAP connection.
*/
class DomainConfiguration
{
/**
* The configuration options array.
*
* The default values for each key indicate the type of value it requires.
*
* @var array
*/
protected $options = [
// An array of LDAP hosts.
'hosts' => [],
// The global LDAP operation timeout limit in seconds.
'timeout' => 5,
// The LDAP version to utilize.
'version' => 3,
// The port to use for connecting to your hosts.
'port' => ConnectionInterface::PORT,
// The schema to use for your LDAP connection.
'schema' => ActiveDirectory::class,
// The base distinguished name of your domain.
'base_dn' => '',
// The username to connect to your hosts with.
'username' => '',
// The password that is utilized with the above user.
'password' => '',
// The account prefix to use when authenticating users.
'account_prefix' => null,
// The account suffix to use when authenticating users.
'account_suffix' => null,
// Whether or not to use SSL when connecting to your hosts.
'use_ssl' => false,
// Whether or not to use TLS when connecting to your hosts.
'use_tls' => false,
// Whether or not follow referrals is enabled when performing LDAP operations.
'follow_referrals' => false,
// Custom LDAP options that you'd like to utilize.
'custom_options' => [],
];
/**
* Constructor.
*
* @param array $options
*
* @throws ConfigurationException When an option value given is an invalid type.
*/
public function __construct(array $options = [])
{
foreach ($options as $key => $value) {
$this->set($key, $value);
}
}
/**
* Sets a configuration option.
*
* Throws an exception if the specified option does
* not exist, or if it's an invalid type.
*
* @param string $key
* @param mixed $value
*
* @throws ConfigurationException When an option value given is an invalid type.
*/
public function set($key, $value)
{
if ($this->validate($key, $value)) {
$this->options[$key] = $value;
}
}
/**
* Returns the value for the specified configuration options.
*
* Throws an exception if the specified option does not exist.
*
* @param string $key
*
* @throws ConfigurationException When the option specified does not exist.
*
* @return mixed
*/
public function get($key)
{
if ($this->has($key)) {
return $this->options[$key];
}
throw new ConfigurationException("Option {$key} does not exist.");
}
/**
* Checks if a configuration option exists.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
return array_key_exists($key, $this->options);
}
/**
* Validates the new configuration option against its
* default value to ensure it's the correct type.
*
* If an invalid type is given, an exception is thrown.
*
* @param string $key
* @param mixed $value
*
* @throws ConfigurationException When an option value given is an invalid type.
*
* @return bool
*/
protected function validate($key, $value)
{
$default = $this->get($key);
if (is_array($default)) {
$validator = new Validators\ArrayValidator($key, $value);
} elseif (is_int($default)) {
$validator = new Validators\IntegerValidator($key, $value);
} elseif (is_bool($default)) {
$validator = new Validators\BooleanValidator($key, $value);
} elseif (class_exists($default)) {
$validator = new Validators\ClassValidator($key, $value);
} else {
$validator = new Validators\StringOrNullValidator($key, $value);
}
return $validator->validate();
}
}
@@ -0,0 +1,25 @@
<?php
namespace Adldap\Configuration\Validators;
use Adldap\Configuration\ConfigurationException;
/**
* Class ArrayValidator.
*
* Validates that the configuration value is an array.
*/
class ArrayValidator extends Validator
{
/**
* {@inheritdoc}
*/
public function validate()
{
if (!is_array($this->value)) {
throw new ConfigurationException("Option {$this->key} must be an array.");
}
return true;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Adldap\Configuration\Validators;
use Adldap\Configuration\ConfigurationException;
/**
* Class BooleanValidator.
*
* Validates that the configuration value is a boolean.
*/
class BooleanValidator extends Validator
{
/**
* {@inheritdoc}
*/
public function validate()
{
if (!is_bool($this->value)) {
throw new ConfigurationException("Option {$this->key} must be a boolean.");
}
return true;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Adldap\Configuration\Validators;
use Adldap\Configuration\ConfigurationException;
class ClassValidator extends Validator
{
/**
* Validates the configuration value.
*
* @throws ConfigurationException When the value given fails validation.
*
* @return bool
*/
public function validate()
{
if (!class_exists($this->value)) {
throw new ConfigurationException("Option {$this->key} must be a valid class.");
}
return true;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Adldap\Configuration\Validators;
use Adldap\Configuration\ConfigurationException;
/**
* Class IntegerValidator.
*
* Validates that the configuration value is an integer / number.
*/
class IntegerValidator extends Validator
{
/**
* {@inheritdoc}
*/
public function validate()
{
if (!is_numeric($this->value)) {
throw new ConfigurationException("Option {$this->key} must be an integer.");
}
return true;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Adldap\Configuration\Validators;
use Adldap\Configuration\ConfigurationException;
/**
* Class StringOrNullValidator.
*
* Validates that the configuration value is a string or null.
*/
class StringOrNullValidator extends Validator
{
/**
* {@inheritdoc}
*/
public function validate()
{
if (is_string($this->value) || is_null($this->value)) {
return true;
}
throw new ConfigurationException("Option {$this->key} must be a string or null.");
}
}
@@ -0,0 +1,46 @@
<?php
namespace Adldap\Configuration\Validators;
/**
* Class Validator.
*
* Validates configuration values.
*/
abstract class Validator
{
/**
* The configuration key under validation.
*
* @var string
*/
protected $key;
/**
* The configuration value under validation.
*
* @var mixed
*/
protected $value;
/**
* Constructor.
*
* @param string $key
* @param mixed $value
*/
public function __construct($key, $value)
{
$this->key = $key;
$this->value = $value;
}
/**
* Validates the configuration value.
*
* @throws \Adldap\Configuration\ConfigurationException When the value given fails validation.
*
* @return bool
*/
abstract public function validate();
}
@@ -0,0 +1,10 @@
<?php
namespace Adldap\Connections;
use Adldap\AdldapException;
class ConnectionException extends AdldapException
{
//
}
@@ -0,0 +1,539 @@
<?php
namespace Adldap\Connections;
/**
* The Connection interface used for making connections. Implementing
* this interface on connection classes helps unit and functional
* test classes that require a connection.
*
* Interface ConnectionInterface
*/
interface ConnectionInterface
{
/**
* The SSL LDAP protocol string.
*
* @var string
*/
const PROTOCOL_SSL = 'ldaps://';
/**
* The standard LDAP protocol string.
*
* @var string
*/
const PROTOCOL = 'ldap://';
/**
* The LDAP SSL port number.
*
* @var string
*/
const PORT_SSL = 636;
/**
* The standard LDAP port number.
*
* @var string
*/
const PORT = 389;
/**
* Constructor.
*
* @param string|null $name The connection name.
*/
public function __construct($name = null);
/**
* Returns true / false if the current connection instance is using SSL.
*
* @return bool
*/
public function isUsingSSL();
/**
* Returns true / false if the current connection instance is using TLS.
*
* @return bool
*/
public function isUsingTLS();
/**
* Returns true / false if the current connection is able to modify passwords.
*
* @return bool
*/
public function canChangePasswords();
/**
* Returns true / false if the current connection is bound.
*
* @return bool
*/
public function isBound();
/**
* Sets the current connection to use SSL.
*
* @param bool $enabled
*
* @return ConnectionInterface
*/
public function ssl($enabled = true);
/**
* Sets the current connection to use TLS.
*
* @param bool $enabled
*
* @return ConnectionInterface
*/
public function tls($enabled = true);
/**
* Returns the full LDAP host URL.
*
* Ex: ldap://192.168.1.1:386
*
* @return string|null
*/
public function getHost();
/**
* Returns the connections name.
*
* @return string|null
*/
public function getName();
/**
* Get the current connection.
*
* @return mixed
*/
public function getConnection();
/**
* Retrieve the entries from a search result.
*
* @link http://php.net/manual/en/function.ldap-get-entries.php
*
* @param $searchResult
*
* @return mixed
*/
public function getEntries($searchResult);
/**
* Returns the number of entries from a search result.
*
* @link http://php.net/manual/en/function.ldap-count-entries.php
*
* @param $searchResult
*
* @return int
*/
public function countEntries($searchResult);
/**
* Compare value of attribute found in entry specified with DN.
*
* @link http://php.net/manual/en/function.ldap-compare.php
*
* @param string $dn
* @param string $attribute
* @param string $value
*
* @return mixed
*/
public function compare($dn, $attribute, $value);
/**
* Retrieves the first entry from a search result.
*
* @link http://php.net/manual/en/function.ldap-first-entry.php
*
* @param $searchResult
*
* @return mixed
*/
public function getFirstEntry($searchResult);
/**
* Retrieves the next entry from a search result.
*
* @link http://php.net/manual/en/function.ldap-next-entry.php
*
* @param $entry
*
* @return mixed
*/
public function getNextEntry($entry);
/**
* Retrieves the ldap entry's attributes.
*
* @link http://php.net/manual/en/function.ldap-get-attributes.php
*
* @param $entry
*
* @return mixed
*/
public function getAttributes($entry);
/**
* Retrieve the last error on the current connection.
*
* @link http://php.net/manual/en/function.ldap-error.php
*
* @return string
*/
public function getLastError();
/**
* Return detailed information about an error.
*
* Returns false when there was a successful last request.
*
* Returns DetailedError when there was an error.
*
* @return DetailedError|null
*/
public function getDetailedError();
/**
* Get all binary values from the specified result entry.
*
* @link http://php.net/manual/en/function.ldap-get-values-len.php
*
* @param $entry
* @param $attribute
*
* @return array
*/
public function getValuesLen($entry, $attribute);
/**
* Sets an option on the current connection.
*
* @link http://php.net/manual/en/function.ldap-set-option.php
*
* @param int $option
* @param mixed $value
*
* @return mixed
*/
public function setOption($option, $value);
/**
* Sets options on the current connection.
*
* @param array $options
*
* @return mixed
*/
public function setOptions(array $options = []);
/**
* Set a callback function to do re-binds on referral chasing.
*
* @link http://php.net/manual/en/function.ldap-set-rebind-proc.php
*
* @param callable $callback
*
* @return bool
*/
public function setRebindCallback(callable $callback);
/**
* Connects to the specified hostname using the specified port.
*
* @link http://php.net/manual/en/function.ldap-start-tls.php
*
* @param string|array $hostname
* @param int $port
*
* @return mixed
*/
public function connect($hostname = [], $port = 389);
/**
* Starts a connection using TLS.
*
* @link http://php.net/manual/en/function.ldap-start-tls.php
*
* @throws ConnectionException If starting TLS fails.
*
* @return mixed
*/
public function startTLS();
/**
* Binds to the current connection using the specified username and password.
* If sasl is true, the current connection is bound using SASL.
*
* @link http://php.net/manual/en/function.ldap-bind.php
*
* @param string $username
* @param string $password
* @param bool $sasl
*
* @throws ConnectionException If starting TLS fails.
*
* @return bool
*/
public function bind($username, $password, $sasl = false);
/**
* Closes the current connection.
*
* Returns false if no connection is present.
*
* @link http://php.net/manual/en/function.ldap-close.php
*
* @return bool
*/
public function close();
/**
* Performs a search on the current connection.
*
* @link http://php.net/manual/en/function.ldap-search.php
*
* @param string $dn
* @param string $filter
* @param array $fields
* @param bool $onlyAttributes
* @param int $size
* @param int $time
*
* @return mixed
*/
public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0);
/**
* Reads an entry on the current connection.
*
* @link http://php.net/manual/en/function.ldap-read.php
*
* @param string $dn
* @param $filter
* @param array $fields
* @param bool $onlyAttributes
* @param int $size
* @param int $time
*
* @return mixed
*/
public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0);
/**
* Performs a single level search on the current connection.
*
* @link http://php.net/manual/en/function.ldap-list.php
*
* @param string $dn
* @param string $filter
* @param array $attributes
* @param bool $onlyAttributes
* @param int $size
* @param int $time
*
* @return mixed
*/
public function listing($dn, $filter, array $attributes, $onlyAttributes = false, $size = 0, $time = 0);
/**
* Adds an entry to the current connection.
*
* @link http://php.net/manual/en/function.ldap-add.php
*
* @param string $dn
* @param array $entry
*
* @return bool
*/
public function add($dn, array $entry);
/**
* Deletes an entry on the current connection.
*
* @link http://php.net/manual/en/function.ldap-delete.php
*
* @param string $dn
*
* @return bool
*/
public function delete($dn);
/**
* Modify the name of an entry on the current connection.
*
* @link http://php.net/manual/en/function.ldap-rename.php
*
* @param string $dn
* @param string $newRdn
* @param string $newParent
* @param bool $deleteOldRdn
*
* @return bool
*/
public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false);
/**
* Modifies an existing entry on the current connection.
*
* @link http://php.net/manual/en/function.ldap-modify.php
*
* @param string $dn
* @param array $entry
*
* @return bool
*/
public function modify($dn, array $entry);
/**
* Batch modifies an existing entry on the current connection.
*
* @link http://php.net/manual/en/function.ldap-modify-batch.php
*
* @param string $dn
* @param array $values
*
* @return mixed
*/
public function modifyBatch($dn, array $values);
/**
* Add attribute values to current attributes.
*
* @link http://php.net/manual/en/function.ldap-mod-add.php
*
* @param string $dn
* @param array $entry
*
* @return mixed
*/
public function modAdd($dn, array $entry);
/**
* Replaces attribute values with new ones.
*
* @link http://php.net/manual/en/function.ldap-mod-replace.php
*
* @param string $dn
* @param array $entry
*
* @return mixed
*/
public function modReplace($dn, array $entry);
/**
* Delete attribute values from current attributes.
*
* @link http://php.net/manual/en/function.ldap-mod-del.php
*
* @param string $dn
* @param array $entry
*
* @return mixed
*/
public function modDelete($dn, array $entry);
/**
* Send LDAP pagination control.
*
* @link http://php.net/manual/en/function.ldap-control-paged-result.php
*
* @param int $pageSize
* @param bool $isCritical
* @param string $cookie
*
* @return mixed
*/
public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '');
/**
* Retrieve the LDAP pagination cookie.
*
* @link http://php.net/manual/en/function.ldap-control-paged-result-response.php
*
* @param $result
* @param string $cookie
*
* @return mixed
*/
public function controlPagedResultResponse($result, &$cookie);
/**
* Frees up the memory allocated internally to store the result.
*
* @link https://www.php.net/manual/en/function.ldap-free-result.php
*
* @param resource $result
*
* @return bool
*/
public function freeResult($result);
/**
* Returns the error number of the last command
* executed on the current connection.
*
* @link http://php.net/manual/en/function.ldap-errno.php
*
* @return int
*/
public function errNo();
/**
* Returns the extended error string of the last command.
*
* @return string
*/
public function getExtendedError();
/**
* Returns the extended error hex code of the last command.
*
* @return string|null
*/
public function getExtendedErrorHex();
/**
* Returns the extended error code of the last command.
*
* @return string
*/
public function getExtendedErrorCode();
/**
* Returns the error string of the specified
* error number.
*
* @link http://php.net/manual/en/function.ldap-err2str.php
*
* @param int $number
*
* @return string
*/
public function err2Str($number);
/**
* Return the diagnostic Message.
*
* @return string
*/
public function getDiagnosticMessage();
/**
* Extract the diagnostic code from the message.
*
* @param string $message
*
* @return string|bool
*/
public function extractDiagnosticCode($message);
}
@@ -0,0 +1,71 @@
<?php
namespace Adldap\Connections;
class DetailedError
{
/**
* The error code from ldap_errno.
*
* @var int|null
*/
protected $errorCode;
/**
* The error message from ldap_error.
*
* @var string|null
*/
protected $errorMessage;
/**
* The diagnostic message when retrieved after an ldap_error.
*
* @var string|null
*/
protected $diagnosticMessage;
/**
* Constructor.
*
* @param int $errorCode
* @param string $errorMessage
* @param string $diagnosticMessage
*/
public function __construct($errorCode, $errorMessage, $diagnosticMessage)
{
$this->errorCode = $errorCode;
$this->errorMessage = $errorMessage;
$this->diagnosticMessage = $diagnosticMessage;
}
/**
* Returns the LDAP error code.
*
* @return int
*/
public function getErrorCode()
{
return $this->errorCode;
}
/**
* Returns the LDAP error message.
*
* @return string
*/
public function getErrorMessage()
{
return $this->errorMessage;
}
/**
* Returns the LDAP diagnostic message.
*
* @return string
*/
public function getDiagnosticMessage()
{
return $this->diagnosticMessage;
}
}
@@ -0,0 +1,545 @@
<?php
namespace Adldap\Connections;
/**
* Class Ldap.
*
* A class that abstracts PHP's LDAP functions and stores the bound connection.
*/
class Ldap implements ConnectionInterface
{
/**
* The connection name.
*
* @var string|null
*/
protected $name;
/**
* The LDAP host that is currently connected.
*
* @var string|null
*/
protected $host;
/**
* The active LDAP connection.
*
* @var resource
*/
protected $connection;
/**
* The bound status of the connection.
*
* @var bool
*/
protected $bound = false;
/**
* Whether the connection must be bound over SSL.
*
* @var bool
*/
protected $useSSL = false;
/**
* Whether the connection must be bound over TLS.
*
* @var bool
*/
protected $useTLS = false;
/**
* {@inheritdoc}
*/
public function __construct($name = null)
{
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public function isUsingSSL()
{
return $this->useSSL;
}
/**
* {@inheritdoc}
*/
public function isUsingTLS()
{
return $this->useTLS;
}
/**
* {@inheritdoc}
*/
public function isBound()
{
return $this->bound;
}
/**
* {@inheritdoc}
*/
public function canChangePasswords()
{
return $this->isUsingSSL() || $this->isUsingTLS();
}
/**
* {@inheritdoc}
*/
public function ssl($enabled = true)
{
$this->useSSL = $enabled;
return $this;
}
/**
* {@inheritdoc}
*/
public function tls($enabled = true)
{
$this->useTLS = $enabled;
return $this;
}
/**
* {@inheritdoc}
*/
public function getHost()
{
return $this->host;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getConnection()
{
return $this->connection;
}
/**
* {@inheritdoc}
*/
public function getEntries($searchResults)
{
return ldap_get_entries($this->connection, $searchResults);
}
/**
* {@inheritdoc}
*/
public function getFirstEntry($searchResults)
{
return ldap_first_entry($this->connection, $searchResults);
}
/**
* {@inheritdoc}
*/
public function getNextEntry($entry)
{
return ldap_next_entry($this->connection, $entry);
}
/**
* {@inheritdoc}
*/
public function getAttributes($entry)
{
return ldap_get_attributes($this->connection, $entry);
}
/**
* {@inheritdoc}
*/
public function countEntries($searchResults)
{
return ldap_count_entries($this->connection, $searchResults);
}
/**
* {@inheritdoc}
*/
public function compare($dn, $attribute, $value)
{
return ldap_compare($this->connection, $dn, $attribute, $value);
}
/**
* {@inheritdoc}
*/
public function getLastError()
{
return ldap_error($this->connection);
}
/**
* {@inheritdoc}
*/
public function getDetailedError()
{
// If the returned error number is zero, the last LDAP operation
// succeeded. We won't return a detailed error.
if ($number = $this->errNo()) {
ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $message);
return new DetailedError($number, $this->err2Str($number), $message);
}
}
/**
* {@inheritdoc}
*/
public function getValuesLen($entry, $attribute)
{
return ldap_get_values_len($this->connection, $entry, $attribute);
}
/**
* {@inheritdoc}
*/
public function setOption($option, $value)
{
return ldap_set_option($this->connection, $option, $value);
}
/**
* {@inheritdoc}
*/
public function setOptions(array $options = [])
{
foreach ($options as $option => $value) {
$this->setOption($option, $value);
}
}
/**
* {@inheritdoc}
*/
public function setRebindCallback(callable $callback)
{
return ldap_set_rebind_proc($this->connection, $callback);
}
/**
* {@inheritdoc}
*/
public function startTLS()
{
try {
return ldap_start_tls($this->connection);
} catch (\ErrorException $e) {
throw new ConnectionException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function connect($hosts = [], $port = 389)
{
$this->host = $this->getConnectionString($hosts, $this->getProtocol(), $port);
// Reset the bound status if reinitializing the connection.
$this->bound = false;
return $this->connection = ldap_connect($this->host);
}
/**
* {@inheritdoc}
*/
public function close()
{
$connection = $this->connection;
$result = is_resource($connection) ? ldap_close($connection) : false;
$this->bound = false;
return $result;
}
/**
* {@inheritdoc}
*/
public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0)
{
return ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time);
}
/**
* {@inheritdoc}
*/
public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0)
{
return ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time);
}
/**
* {@inheritdoc}
*/
public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0)
{
return ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time);
}
/**
* Extract information from an LDAP result.
*
* @link https://www.php.net/manual/en/function.ldap-parse-result.php
*
* @param resource $result
* @param int $errorCode
* @param string $dn
* @param string $errorMessage
* @param array $referrals
* @param array $serverControls
*
* @return bool
*/
public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = [])
{
return $this->supportsServerControlsInMethods() && !empty($serverControls) ?
ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals, $serverControls) :
ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals);
}
/**
* {@inheritdoc}
*/
public function bind($username, $password, $sasl = false)
{
// Prior to binding, we will upgrade our connectivity to TLS on our current
// connection and ensure we are not already bound before upgrading.
// This is to prevent subsequent upgrading on several binds.
if ($this->isUsingTLS() && !$this->isBound()) {
$this->startTLS();
}
if ($sasl) {
return $this->bound = ldap_sasl_bind($this->connection, null, null, 'GSSAPI');
}
return $this->bound = ldap_bind(
$this->connection,
$username,
html_entity_decode($password)
);
}
/**
* {@inheritdoc}
*/
public function add($dn, array $entry)
{
return ldap_add($this->connection, $dn, $entry);
}
/**
* {@inheritdoc}
*/
public function delete($dn)
{
return ldap_delete($this->connection, $dn);
}
/**
* {@inheritdoc}
*/
public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
{
return ldap_rename($this->connection, $dn, $newRdn, $newParent, $deleteOldRdn);
}
/**
* {@inheritdoc}
*/
public function modify($dn, array $entry)
{
return ldap_modify($this->connection, $dn, $entry);
}
/**
* {@inheritdoc}
*/
public function modifyBatch($dn, array $values)
{
return ldap_modify_batch($this->connection, $dn, $values);
}
/**
* {@inheritdoc}
*/
public function modAdd($dn, array $entry)
{
return ldap_mod_add($this->connection, $dn, $entry);
}
/**
* {@inheritdoc}
*/
public function modReplace($dn, array $entry)
{
return ldap_mod_replace($this->connection, $dn, $entry);
}
/**
* {@inheritdoc}
*/
public function modDelete($dn, array $entry)
{
return ldap_mod_del($this->connection, $dn, $entry);
}
/**
* {@inheritdoc}
*/
public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '')
{
return ldap_control_paged_result($this->connection, $pageSize, $isCritical, $cookie);
}
/**
* {@inheritdoc}
*/
public function controlPagedResultResponse($result, &$cookie)
{
return ldap_control_paged_result_response($this->connection, $result, $cookie);
}
/**
* {@inheritdoc}
*/
public function freeResult($result)
{
return ldap_free_result($result);
}
/**
* {@inheritdoc}
*/
public function errNo()
{
return ldap_errno($this->connection);
}
/**
* {@inheritdoc}
*/
public function getExtendedError()
{
return $this->getDiagnosticMessage();
}
/**
* {@inheritdoc}
*/
public function getExtendedErrorHex()
{
if (preg_match("/(?<=data\s).*?(?=\,)/", $this->getExtendedError(), $code)) {
return $code[0];
}
}
/**
* {@inheritdoc}
*/
public function getExtendedErrorCode()
{
return $this->extractDiagnosticCode($this->getExtendedError());
}
/**
* {@inheritdoc}
*/
public function err2Str($number)
{
return ldap_err2str($number);
}
/**
* {@inheritdoc}
*/
public function getDiagnosticMessage()
{
ldap_get_option($this->connection, LDAP_OPT_ERROR_STRING, $message);
return $message;
}
/**
* {@inheritdoc}
*/
public function extractDiagnosticCode($message)
{
preg_match('/^([\da-fA-F]+):/', $message, $matches);
return isset($matches[1]) ? $matches[1] : false;
}
/**
* Returns the LDAP protocol to utilize for the current connection.
*
* @return string
*/
public function getProtocol()
{
return $this->isUsingSSL() ? $this::PROTOCOL_SSL : $this::PROTOCOL;
}
/**
* Determine if the current PHP version supports server controls.
*
* @return bool
*/
public function supportsServerControlsInMethods()
{
return version_compare(PHP_VERSION, '7.3.0') >= 0;
}
/**
* Generates an LDAP connection string for each host given.
*
* @param string|array $hosts
* @param string $protocol
* @param string $port
*
* @return string
*/
protected function getConnectionString($hosts, $protocol, $port)
{
// If we are using SSL and using the default port, we
// will override it to use the default SSL port.
if ($this->isUsingSSL() && $port == 389) {
$port = self::PORT_SSL;
}
// Normalize hosts into an array.
$hosts = is_array($hosts) ? $hosts : [$hosts];
$hosts = array_map(function ($host) use ($protocol, $port) {
return "{$protocol}{$host}:{$port}";
}, $hosts);
return implode(' ', $hosts);
}
}
@@ -0,0 +1,291 @@
<?php
namespace Adldap\Connections;
use Adldap\Adldap;
use Adldap\Auth\Guard;
use Adldap\Query\Cache;
use InvalidArgumentException;
use Adldap\Auth\GuardInterface;
use Adldap\Schemas\ActiveDirectory;
use Adldap\Schemas\SchemaInterface;
use Psr\SimpleCache\CacheInterface;
use Adldap\Models\Factory as ModelFactory;
use Adldap\Query\Factory as SearchFactory;
use Adldap\Configuration\DomainConfiguration;
/**
* Class Provider.
*
* Contains the LDAP connection and domain configuration to
* instantiate factories for retrieving and creating
* LDAP records as well as authentication (binding).
*/
class Provider implements ProviderInterface
{
/**
* The providers connection.
*
* @var ConnectionInterface
*/
protected $connection;
/**
* The providers configuration.
*
* @var DomainConfiguration
*/
protected $configuration;
/**
* The providers schema.
*
* @var SchemaInterface
*/
protected $schema;
/**
* The providers auth guard instance.
*
* @var GuardInterface
*/
protected $guard;
/**
* The providers cache instance.
*
* @var Cache|null
*/
protected $cache;
/**
* {@inheritdoc}
*/
public function __construct($configuration = [], ConnectionInterface $connection = null)
{
$this->setConfiguration($configuration)
->setConnection($connection);
}
/**
* Does nothing. Implemented in order to remain backwards compatible.
*
* @deprecated since v10.3.0
*/
public function __destruct()
{
//
}
/**
* {@inheritdoc}
*/
public function setConfiguration($configuration = [])
{
if (is_array($configuration)) {
$configuration = new DomainConfiguration($configuration);
}
if ($configuration instanceof DomainConfiguration) {
$this->configuration = $configuration;
$schema = $configuration->get('schema');
// We will update our schema here when our configuration is set.
$this->setSchema(new $schema());
return $this;
}
$class = DomainConfiguration::class;
throw new InvalidArgumentException(
"Configuration must be array or instance of $class"
);
}
/**
* {@inheritdoc}
*/
public function setConnection(ConnectionInterface $connection = null)
{
// We will create a standard connection if one isn't given.
$this->connection = $connection ?: new Ldap();
// Prepare the connection.
$this->prepareConnection();
// Instantiate the LDAP connection.
$this->connection->connect(
$this->configuration->get('hosts'),
$this->configuration->get('port')
);
return $this;
}
/**
* {@inheritdoc}
*/
public function setSchema(SchemaInterface $schema = null)
{
$this->schema = $schema ?: new ActiveDirectory();
return $this;
}
/**
* {@inheritdoc}
*/
public function setGuard(GuardInterface $guard)
{
$this->guard = $guard;
return $this;
}
/**
* Sets the cache store.
*
* @param CacheInterface $store
*
* @return $this
*/
public function setCache(CacheInterface $store)
{
$this->cache = new Cache($store);
return $this;
}
/**
* {@inheritdoc}
*/
public function getConfiguration()
{
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function getConnection()
{
return $this->connection;
}
/**
* {@inheritdoc}
*/
public function getSchema()
{
return $this->schema;
}
/**
* {@inheritdoc}
*/
public function getGuard()
{
if (!$this->guard instanceof GuardInterface) {
$this->setGuard($this->getDefaultGuard($this->connection, $this->configuration));
}
return $this->guard;
}
/**
* {@inheritdoc}
*/
public function getDefaultGuard(ConnectionInterface $connection, DomainConfiguration $configuration)
{
$guard = new Guard($connection, $configuration);
$guard->setDispatcher(Adldap::getEventDispatcher());
return $guard;
}
/**
* {@inheritdoc}
*/
public function make()
{
return new ModelFactory(
$this->search()->newQuery()
);
}
/**
* {@inheritdoc}
*/
public function search()
{
$factory = new SearchFactory(
$this->connection,
$this->schema,
$this->configuration->get('base_dn')
);
if ($this->cache) {
$factory->setCache($this->cache);
}
return $factory;
}
/**
* {@inheritdoc}
*/
public function auth()
{
return $this->getGuard();
}
/**
* {@inheritdoc}
*/
public function connect($username = null, $password = null)
{
// Get the default guard instance.
$guard = $this->getGuard();
if (is_null($username) && is_null($password)) {
// If both the username and password are null, we'll connect to the server
// using the configured administrator username and password.
$guard->bindAsAdministrator();
} else {
// Bind to the server with the specified username and password otherwise.
$guard->bind($username, $password);
}
return $this;
}
/**
* Prepares the connection by setting configured parameters.
*
* @throws \Adldap\Configuration\ConfigurationException When configuration options requested do not exist
*
* @return void
*/
protected function prepareConnection()
{
if ($this->configuration->get('use_ssl')) {
$this->connection->ssl();
} elseif ($this->configuration->get('use_tls')) {
$this->connection->tls();
}
$options = array_replace(
$this->configuration->get('custom_options'),
[
LDAP_OPT_PROTOCOL_VERSION => $this->configuration->get('version'),
LDAP_OPT_NETWORK_TIMEOUT => $this->configuration->get('timeout'),
LDAP_OPT_REFERRALS => $this->configuration->get('follow_referrals'),
]
);
$this->connection->setOptions($options);
}
}
@@ -0,0 +1,129 @@
<?php
namespace Adldap\Connections;
use Adldap\Auth\GuardInterface;
use Adldap\Schemas\SchemaInterface;
use Adldap\Configuration\DomainConfiguration;
interface ProviderInterface
{
/**
* Constructor.
*
* @param array|DomainConfiguration $configuration
* @param ConnectionInterface $connection
*/
public function __construct($configuration, ConnectionInterface $connection);
/**
* Returns the current connection instance.
*
* @return ConnectionInterface
*/
public function getConnection();
/**
* Returns the current configuration instance.
*
* @return DomainConfiguration
*/
public function getConfiguration();
/**
* Returns the current Guard instance.
*
* @return \Adldap\Auth\Guard
*/
public function getGuard();
/**
* Returns a new default Guard instance.
*
* @param ConnectionInterface $connection
* @param DomainConfiguration $configuration
*
* @return \Adldap\Auth\Guard
*/
public function getDefaultGuard(ConnectionInterface $connection, DomainConfiguration $configuration);
/**
* Sets the current connection.
*
* @param ConnectionInterface $connection
*
* @return $this
*/
public function setConnection(ConnectionInterface $connection = null);
/**
* Sets the current configuration.
*
* @param DomainConfiguration|array $configuration
*
* @throws \Adldap\Configuration\ConfigurationException
*/
public function setConfiguration($configuration = []);
/**
* Sets the current LDAP attribute schema.
*
* @param SchemaInterface|null $schema
*
* @return $this
*/
public function setSchema(SchemaInterface $schema = null);
/**
* Returns the current LDAP attribute schema.
*
* @return SchemaInterface
*/
public function getSchema();
/**
* Sets the current Guard instance.
*
* @param GuardInterface $guard
*
* @return $this
*/
public function setGuard(GuardInterface $guard);
/**
* Returns a new Model factory instance.
*
* @return \Adldap\Models\Factory
*/
public function make();
/**
* Returns a new Search factory instance.
*
* @return \Adldap\Query\Factory
*/
public function search();
/**
* Returns a new Auth Guard instance.
*
* @return \Adldap\Auth\Guard
*/
public function auth();
/**
* Connects and Binds to the Domain Controller.
*
* If no username or password is specified, then the
* configured administrator credentials are used.
*
* @param string|null $username
* @param string|null $password
*
* @throws \Adldap\Auth\BindException If binding to the LDAP server fails.
* @throws ConnectionException If upgrading the connection to TLS fails
*
* @return ProviderInterface
*/
public function connect($username = null, $password = null);
}
@@ -0,0 +1,320 @@
<?php
namespace Adldap\Events;
use Illuminate\Support\Arr;
/**
* Class Dispatcher.
*
* Handles event listening and dispatching.
*
* This code was taken out of the Laravel Framework core
* with broadcasting and queuing omitted to remove
* an extra dependency that would be required.
*
* @author Taylor Otwell
*
* @see https://github.com/laravel/framework
*/
class Dispatcher implements DispatcherInterface
{
/**
* The registered event listeners.
*
* @var array
*/
protected $listeners = [];
/**
* The wildcard listeners.
*
* @var array
*/
protected $wildcards = [];
/**
* The cached wildcard listeners.
*
* @var array
*/
protected $wildcardsCache = [];
/**
* {@inheritdoc}
*/
public function listen($events, $listener)
{
foreach ((array) $events as $event) {
if (strpos($event, '*') !== false) {
$this->setupWildcardListen($event, $listener);
} else {
$this->listeners[$event][] = $this->makeListener($listener);
}
}
}
/**
* Setup a wildcard listener callback.
*
* @param string $event
* @param mixed $listener
*
* @return void
*/
protected function setupWildcardListen($event, $listener)
{
$this->wildcards[$event][] = $this->makeListener($listener, true);
$this->wildcardsCache = [];
}
/**
* {@inheritdoc}
*/
public function hasListeners($eventName)
{
return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
}
/**
* {@inheritdoc}
*/
public function until($event, $payload = [])
{
return $this->dispatch($event, $payload, true);
}
/**
* {@inheritdoc}
*/
public function fire($event, $payload = [], $halt = false)
{
return $this->dispatch($event, $payload, $halt);
}
/**
* {@inheritdoc}
*/
public function dispatch($event, $payload = [], $halt = false)
{
// When the given "event" is actually an object we will assume it is an event
// object and use the class as the event name and this event itself as the
// payload to the handler, which makes object based events quite simple.
list($event, $payload) = $this->parseEventAndPayload(
$event,
$payload
);
$responses = [];
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ($halt && !is_null($response)) {
return $response;
}
// If a boolean false is returned from a listener, we will stop propagating
// the event to any further listeners down in the chain, else we keep on
// looping through the listeners and firing every one in our sequence.
if ($response === false) {
break;
}
$responses[] = $response;
}
return $halt ? null : $responses;
}
/**
* Parse the given event and payload and prepare them for dispatching.
*
* @param mixed $event
* @param mixed $payload
*
* @return array
*/
protected function parseEventAndPayload($event, $payload)
{
if (is_object($event)) {
list($payload, $event) = [[$event], get_class($event)];
}
return [$event, Arr::wrap($payload)];
}
/**
* {@inheritdoc}
*/
public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];
$listeners = array_merge(
$listeners,
$this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
);
return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}
/**
* Get the wildcard listeners for the event.
*
* @param string $eventName
*
* @return array
*/
protected function getWildcardListeners($eventName)
{
$wildcards = [];
foreach ($this->wildcards as $key => $listeners) {
if ($this->wildcardContainsEvent($key, $eventName)) {
$wildcards = array_merge($wildcards, $listeners);
}
}
return $this->wildcardsCache[$eventName] = $wildcards;
}
/**
* Determine if the wildcard matches or contains the given event.
*
* This function is a direct excerpt from Laravel's Str::is().
*
* @param string $wildcard
* @param string $eventName
*
* @return bool
*/
protected function wildcardContainsEvent($wildcard, $eventName)
{
$patterns = Arr::wrap($wildcard);
if (empty($patterns)) {
return false;
}
foreach ($patterns as $pattern) {
// If the given event is an exact match we can of course return true right
// from the beginning. Otherwise, we will translate asterisks and do an
// actual pattern match against the two strings to see if they match.
if ($pattern == $eventName) {
return true;
}
$pattern = preg_quote($pattern, '#');
// Asterisks are translated into zero-or-more regular expression wildcards
// to make it convenient to check if the strings starts with the given
// pattern such as "library/*", making any string check convenient.
$pattern = str_replace('\*', '.*', $pattern);
if (preg_match('#^'.$pattern.'\z#u', $eventName) === 1) {
return true;
}
}
return false;
}
/**
* Add the listeners for the event's interfaces to the given array.
*
* @param string $eventName
* @param array $listeners
*
* @return array
*/
protected function addInterfaceListeners($eventName, array $listeners = [])
{
foreach (class_implements($eventName) as $interface) {
if (isset($this->listeners[$interface])) {
foreach ($this->listeners[$interface] as $names) {
$listeners = array_merge($listeners, (array) $names);
}
}
}
return $listeners;
}
/**
* Register an event listener with the dispatcher.
*
* @param \Closure|string $listener
* @param bool $wildcard
*
* @return \Closure
*/
public function makeListener($listener, $wildcard = false)
{
if (is_string($listener)) {
return $this->createClassListener($listener, $wildcard);
}
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return $listener($event, $payload);
}
return $listener(...array_values($payload));
};
}
/**
* Create a class based listener.
*
* @param string $listener
* @param bool $wildcard
*
* @return \Closure
*/
protected function createClassListener($listener, $wildcard = false)
{
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return call_user_func($this->parseListenerCallback($listener), $event, $payload);
}
return call_user_func_array(
$this->parseListenerCallback($listener),
$payload
);
};
}
/**
* Parse the class listener into class and method.
*
* @param string $listener
*
* @return array
*/
protected function parseListenerCallback($listener)
{
return strpos($listener, '@') !== false ?
explode('@', $listener, 2) :
[$listener, 'handle'];
}
/**
* {@inheritdoc}
*/
public function forget($event)
{
if (strpos($event, '*') !== false) {
unset($this->wildcards[$event]);
} else {
unset($this->listeners[$event]);
}
}
}
@@ -0,0 +1,75 @@
<?php
namespace Adldap\Events;
interface DispatcherInterface
{
/**
* Register an event listener with the dispatcher.
*
* @param string|array $events
* @param mixed $listener
*
* @return void
*/
public function listen($events, $listener);
/**
* Determine if a given event has listeners.
*
* @param string $eventName
*
* @return bool
*/
public function hasListeners($eventName);
/**
* Fire an event until the first non-null response is returned.
*
* @param string|object $event
* @param mixed $payload
*
* @return array|null
*/
public function until($event, $payload = []);
/**
* Fire an event and call the listeners.
*
* @param string|object $event
* @param mixed $payload
* @param bool $halt
*
* @return mixed
*/
public function fire($event, $payload = [], $halt = false);
/**
* Fire an event and call the listeners.
*
* @param string|object $event
* @param mixed $payload
* @param bool $halt
*
* @return array|null
*/
public function dispatch($event, $payload = [], $halt = false);
/**
* Get all of the listeners for a given event name.
*
* @param string $eventName
*
* @return array
*/
public function getListeners($eventName);
/**
* Remove a set of listeners from the dispatcher.
*
* @param string $event
*
* @return void
*/
public function forget($event);
}
@@ -0,0 +1,51 @@
<?php
namespace Adldap\Events;
trait DispatchesEvents
{
/**
* The event dispatcher instance.
*
* @var DispatcherInterface
*/
protected static $dispatcher;
/**
* Get the event dispatcher instance.
*
* @return DispatcherInterface
*/
public static function getEventDispatcher()
{
// If no event dispatcher has been set, well instantiate and
// set one here. This will be our singleton instance.
if (!isset(static::$dispatcher)) {
static::setEventDispatcher(new Dispatcher());
}
return static::$dispatcher;
}
/**
* Set the event dispatcher instance.
*
* @param DispatcherInterface $dispatcher
*
* @return void
*/
public static function setEventDispatcher(DispatcherInterface $dispatcher)
{
static::$dispatcher = $dispatcher;
}
/**
* Unset the event dispatcher instance.
*
* @return void
*/
public static function unsetEventDispatcher()
{
static::$dispatcher = null;
}
}
@@ -0,0 +1,141 @@
<?php
namespace Adldap\Log;
use ReflectionClass;
use Psr\Log\LoggerInterface;
use Adldap\Auth\Events\Failed;
use Adldap\Auth\Events\Event as AuthEvent;
use Adldap\Models\Events\Event as ModelEvent;
use Adldap\Query\Events\QueryExecuted as QueryEvent;
class EventLogger
{
/**
* The logger instance.
*
* @var LoggerInterface|null
*/
protected $logger;
/**
* Constructor.
*
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger = null)
{
$this->logger = $logger;
}
/**
* Logs the given event.
*
* @param mixed $event
*/
public function log($event)
{
if ($event instanceof AuthEvent) {
$this->auth($event);
} elseif ($event instanceof ModelEvent) {
$this->model($event);
} elseif ($event instanceof QueryEvent) {
$this->query($event);
}
}
/**
* Logs an authentication event.
*
* @param AuthEvent $event
*
* @return void
*/
public function auth(AuthEvent $event)
{
if (isset($this->logger)) {
$connection = $event->getConnection();
$message = "LDAP ({$connection->getHost()})"
." - Connection: {$connection->getName()}"
." - Operation: {$this->getOperationName($event)}"
." - Username: {$event->getUsername()}";
$result = null;
$type = 'info';
if (is_a($event, Failed::class)) {
$type = 'warning';
$result = " - Reason: {$connection->getLastError()}";
}
$this->logger->$type($message.$result);
}
}
/**
* Logs a model event.
*
* @param ModelEvent $event
*
* @return void
*/
public function model(ModelEvent $event)
{
if (isset($this->logger)) {
$model = $event->getModel();
$on = get_class($model);
$connection = $model->getQuery()->getConnection();
$message = "LDAP ({$connection->getHost()})"
." - Connection: {$connection->getName()}"
." - Operation: {$this->getOperationName($event)}"
." - On: {$on}"
." - Distinguished Name: {$model->getDn()}";
$this->logger->info($message);
}
}
/**
* Logs a query event.
*
* @param QueryEvent $event
*
* @return void
*/
public function query(QueryEvent $event)
{
if (isset($this->logger)) {
$query = $event->getQuery();
$connection = $query->getConnection();
$selected = implode(',', $query->getSelects());
$message = "LDAP ({$connection->getHost()})"
." - Connection: {$connection->getName()}"
." - Operation: {$this->getOperationName($event)}"
." - Base DN: {$query->getDn()}"
." - Filter: {$query->getUnescapedQuery()}"
." - Selected: ({$selected})"
." - Time Elapsed: {$event->getTime()}";
$this->logger->info($message);
}
}
/**
* Returns the operational name of the given event.
*
* @param mixed $event
*
* @return string
*/
protected function getOperationName($event)
{
return (new ReflectionClass($event))->getShortName();
}
}
@@ -0,0 +1,47 @@
<?php
namespace Adldap\Log;
use Psr\Log\LoggerInterface;
trait LogsInformation
{
/**
* The logger instance.
*
* @var LoggerInterface|null
*/
protected static $logger;
/**
* Get the logger instance.
*
* @return LoggerInterface|null
*/
public static function getLogger()
{
return static::$logger;
}
/**
* Set the logger instance.
*
* @param LoggerInterface $logger
*
* @return void
*/
public static function setLogger(LoggerInterface $logger)
{
static::$logger = $logger;
}
/**
* Unset the logger instance.
*
* @return void
*/
public static function unsetLogger()
{
static::$logger = null;
}
}
@@ -0,0 +1,458 @@
<?php
namespace Adldap\Models\Attributes;
use ReflectionClass;
/**
* The Account Control class.
*
* This class is for easily building a user account control value.
*
* @link https://support.microsoft.com/en-us/kb/305144
*/
class AccountControl
{
const SCRIPT = 1;
const ACCOUNTDISABLE = 2;
const HOMEDIR_REQUIRED = 8;
const LOCKOUT = 16;
const PASSWD_NOTREQD = 32;
const ENCRYPTED_TEXT_PWD_ALLOWED = 128;
const TEMP_DUPLICATE_ACCOUNT = 256;
const NORMAL_ACCOUNT = 512;
const INTERDOMAIN_TRUST_ACCOUNT = 2048;
const WORKSTATION_TRUST_ACCOUNT = 4096;
const SERVER_TRUST_ACCOUNT = 8192;
const DONT_EXPIRE_PASSWORD = 65536;
const MNS_LOGON_ACCOUNT = 131072;
const SMARTCARD_REQUIRED = 262144;
const TRUSTED_FOR_DELEGATION = 524288;
const NOT_DELEGATED = 1048576;
const USE_DES_KEY_ONLY = 2097152;
const DONT_REQ_PREAUTH = 4194304;
const PASSWORD_EXPIRED = 8388608;
const TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216;
const PARTIAL_SECRETS_ACCOUNT = 67108864;
/**
* Stores the values to be added together to
* build the user account control integer.
*
* @var array
*/
protected $values = [];
/**
* Constructor.
*
* @param int $flag
*/
public function __construct($flag = null)
{
if (!is_null($flag)) {
$this->apply($flag);
}
}
/**
* Get the value when casted to string.
*
* @return string
*/
public function __toString()
{
return (string) $this->getValue();
}
/**
* Get the value when casted to int.
*
* @return int
*/
public function __toInt()
{
return $this->getValue();
}
/**
* Add the value to the account control values.
*
* @param int $value
*
* @return AccountControl
*/
public function add($value)
{
// Use the value as a key so if the same value
// is used, it will always be overwritten
$this->values[$value] = $value;
return $this;
}
/**
* Remove the value from the account control.
*
* @param int $value
*
* @return $this
*/
public function remove($value)
{
unset($this->values[$value]);
return $this;
}
/**
* Extract and apply the flag.
*
* @param int $flag
*/
public function apply($flag)
{
$this->setValues($this->extractFlags($flag));
}
/**
* Determine if the current AccountControl object contains the given UAC flag(s).
*
* @param int $flag
*
* @return bool
*/
public function has($flag)
{
// We'll extract the given flag into an array of possible flags, and
// see if our AccountControl object contains any of them.
$flagsUsed = array_intersect($this->extractFlags($flag), $this->values);
return in_array($flag, $flagsUsed);
}
/**
* The logon script will be run.
*
* @return AccountControl
*/
public function runLoginScript()
{
return $this->add(static::SCRIPT);
}
/**
* The user account is locked.
*
* @return AccountControl
*/
public function accountIsLocked()
{
return $this->add(static::LOCKOUT);
}
/**
* The user account is disabled.
*
* @return AccountControl
*/
public function accountIsDisabled()
{
return $this->add(static::ACCOUNTDISABLE);
}
/**
* This is an account for users whose primary account is in another domain.
*
* This account provides user access to this domain, but not to any domain that
* trusts this domain. This is sometimes referred to as a local user account.
*
* @return AccountControl
*/
public function accountIsTemporary()
{
return $this->add(static::TEMP_DUPLICATE_ACCOUNT);
}
/**
* This is a default account type that represents a typical user.
*
* @return AccountControl
*/
public function accountIsNormal()
{
return $this->add(static::NORMAL_ACCOUNT);
}
/**
* This is a permit to trust an account for a system domain that trusts other domains.
*
* @return AccountControl
*/
public function accountIsForInterdomain()
{
return $this->add(static::INTERDOMAIN_TRUST_ACCOUNT);
}
/**
* This is a computer account for a computer that is running Microsoft
* Windows NT 4.0 Workstation, Microsoft Windows NT 4.0 Server, Microsoft
* Windows 2000 Professional, or Windows 2000 Server and is a member of this domain.
*
* @return AccountControl
*/
public function accountIsForWorkstation()
{
return $this->add(static::WORKSTATION_TRUST_ACCOUNT);
}
/**
* This is a computer account for a domain controller that is a member of this domain.
*
* @return AccountControl
*/
public function accountIsForServer()
{
return $this->add(static::SERVER_TRUST_ACCOUNT);
}
/**
* This is an MNS logon account.
*
* @return AccountControl
*/
public function accountIsMnsLogon()
{
return $this->add(static::MNS_LOGON_ACCOUNT);
}
/**
* (Windows 2000/Windows Server 2003) This account does
* not require Kerberos pre-authentication for logging on.
*
* @return AccountControl
*/
public function accountDoesNotRequirePreAuth()
{
return $this->add(static::DONT_REQ_PREAUTH);
}
/**
* When this flag is set, it forces the user to log on by using a smart card.
*
* @return AccountControl
*/
public function accountRequiresSmartCard()
{
return $this->add(static::SMARTCARD_REQUIRED);
}
/**
* (Windows Server 2008/Windows Server 2008 R2) The account is a read-only domain controller (RODC).
*
* This is a security-sensitive setting. Removing this setting from an RODC compromises security on that server.
*
* @return AccountControl
*/
public function accountIsReadOnly()
{
return $this->add(static::PARTIAL_SECRETS_ACCOUNT);
}
/**
* The home folder is required.
*
* @return AccountControl
*/
public function homeFolderIsRequired()
{
return $this->add(static::HOMEDIR_REQUIRED);
}
/**
* No password is required.
*
* @return AccountControl
*/
public function passwordIsNotRequired()
{
return $this->add(static::PASSWD_NOTREQD);
}
/**
* The user cannot change the password. This is a permission on the user's object.
*
* For information about how to programmatically set this permission, visit the following link:
*
* @link http://msdn2.microsoft.com/en-us/library/aa746398.aspx
*
* @return AccountControl
*/
public function passwordCannotBeChanged()
{
return $this->add(static::PASSWD_NOTREQD);
}
/**
* Represents the password, which should never expire on the account.
*
* @return AccountControl
*/
public function passwordDoesNotExpire()
{
return $this->add(static::DONT_EXPIRE_PASSWORD);
}
/**
* (Windows 2000/Windows Server 2003) The user's password has expired.
*
* @return AccountControl
*/
public function passwordIsExpired()
{
return $this->add(static::PASSWORD_EXPIRED);
}
/**
* The user can send an encrypted password.
*
* @return AccountControl
*/
public function allowEncryptedTextPassword()
{
return $this->add(static::ENCRYPTED_TEXT_PWD_ALLOWED);
}
/**
* When this flag is set, the service account (the user or computer account)
* under which a service runs is trusted for Kerberos delegation.
*
* Any such service can impersonate a client requesting the service.
*
* To enable a service for Kerberos delegation, you must set this
* flag on the userAccountControl property of the service account.
*
* @return AccountControl
*/
public function trustForDelegation()
{
return $this->add(static::TRUSTED_FOR_DELEGATION);
}
/**
* (Windows 2000/Windows Server 2003) The account is enabled for delegation.
*
* This is a security-sensitive setting. Accounts that have this option enabled
* should be tightly controlled. This setting lets a service that runs under the
* account assume a client's identity and authenticate as that user to other remote
* servers on the network.
*
* @return AccountControl
*/
public function trustToAuthForDelegation()
{
return $this->add(static::TRUSTED_TO_AUTH_FOR_DELEGATION);
}
/**
* When this flag is set, the security context of the user is not delegated to a
* service even if the service account is set as trusted for Kerberos delegation.
*
* @return AccountControl
*/
public function doNotTrustForDelegation()
{
return $this->add(static::NOT_DELEGATED);
}
/**
* (Windows 2000/Windows Server 2003) Restrict this principal to
* use only Data Encryption Standard (DES) encryption types for keys.
*
* @return AccountControl
*/
public function useDesKeyOnly()
{
return $this->add(static::USE_DES_KEY_ONLY);
}
/**
* Get the account control value.
*
* @return int
*/
public function getValue()
{
return array_sum($this->values);
}
/**
* Get the account control flag values.
*
* @return array
*/
public function getValues()
{
return $this->values;
}
/**
* Set the account control values.
*
* @param array $flags
*/
public function setValues(array $flags)
{
$this->values = $flags;
}
/**
* Get all possible account control flags.
*
* @return array
*/
public function getAllFlags()
{
return (new ReflectionClass(__CLASS__))->getConstants();
}
/**
* Extracts the given flag into an array of flags used.
*
* @param int $flag
*
* @return array
*/
public function extractFlags($flag)
{
$flags = [];
for ($i = 0; $i <= 26; $i++) {
if ((int) $flag & (1 << $i)) {
$flags[1 << $i] = 1 << $i;
}
}
return $flags;
}
}
@@ -0,0 +1,312 @@
<?php
namespace Adldap\Models\Attributes;
use Adldap\Utilities;
class DistinguishedName
{
/**
* The distinguished name components (in order of assembly).
*
* @var array
*/
protected $components = [
'cn' => [],
'uid' => [],
'ou' => [],
'dc' => [],
'o' => [],
];
/**
* Constructor.
*
* @param mixed $baseDn
*/
public function __construct($baseDn = null)
{
$this->setBase($baseDn);
}
/**
* Returns the complete distinguished name.
*
* @return string
*/
public function __toString()
{
return $this->get();
}
/**
* Returns the complete distinguished name by assembling the RDN components.
*
* @return string
*/
public function get()
{
$components = [];
// We'll go through each component type and assemble its RDN.
foreach ($this->components as $component => $values) {
array_map(function ($value) use ($component, &$components) {
// Assemble the component and escape the value.
$components[] = sprintf('%s=%s', $component, ldap_escape($value, '', 2));
}, $values);
}
return implode(',', $components);
}
/**
* Adds a domain component.
*
* @param string $dc
*
* @return DistinguishedName
*/
public function addDc($dc)
{
$this->addComponent('dc', $dc);
return $this;
}
/**
* Removes a domain component.
*
* @param string $dc
*
* @return DistinguishedName
*/
public function removeDc($dc)
{
$this->removeComponent('dc', $dc);
return $this;
}
/**
* Adds an organization name.
*
* @param string $o
*
* @return $this
*/
public function addO($o)
{
$this->addComponent('o', $o);
return $this;
}
/**
* Removes an organization name.
*
* @param string $o
*
* @return DistinguishedName
*/
public function removeO($o)
{
$this->removeComponent('o', $o);
return $this;
}
/**
* Add a user identifier.
*
* @param string $uid
*
* @return DistinguishedName
*/
public function addUid($uid)
{
$this->addComponent('uid', $uid);
return $this;
}
/**
* Removes a user identifier.
*
* @param string $uid
*
* @return DistinguishedName
*/
public function removeUid($uid)
{
$this->removeComponent('uid', $uid);
return $this;
}
/**
* Adds a common name.
*
* @param string $cn
*
* @return DistinguishedName
*/
public function addCn($cn)
{
$this->addComponent('cn', $cn);
return $this;
}
/**
* Removes a common name.
*
* @param string $cn
*
* @return DistinguishedName
*/
public function removeCn($cn)
{
$this->removeComponent('cn', $cn);
return $this;
}
/**
* Adds an organizational unit.
*
* @param string $ou
*
* @return DistinguishedName
*/
public function addOu($ou)
{
$this->addComponent('ou', $ou);
return $this;
}
/**
* Removes an organizational unit.
*
* @param string $ou
*
* @return DistinguishedName
*/
public function removeOu($ou)
{
$this->removeComponent('ou', $ou);
return $this;
}
/**
* Sets the base RDN of the distinguished name.
*
* @param string|DistinguishedName $base
*
* @return DistinguishedName
*/
public function setBase($base)
{
// Typecast base to string in case we've been given
// an instance of the distinguished name object.
$base = (string) $base;
// If the base DN isn't null we'll try to explode it.
$base = Utilities::explodeDn($base, false) ?: [];
// Remove the count key from the exploded distinguished name.
unset($base['count']);
foreach ($base as $key => $rdn) {
// We'll break the RDN into pieces
$pieces = explode('=', $rdn) ?: [];
// If there's exactly 2 pieces, then we can work with it.
if (count($pieces) === 2) {
$attribute = ucfirst(strtolower($pieces[0]));
$method = 'add'.$attribute;
if (method_exists($this, $method)) {
// We see what type of RDN it is and add each accordingly.
call_user_func_array([$this, $method], [$pieces[1]]);
}
}
}
return $this;
}
/**
* Returns an array of all components in the distinguished name.
*
* If a component name is given ('cn', 'dc' for example) then
* the values of that component will be returned.
*
* @param string|null $component The component to retrieve values of
*
* @return array
*/
public function getComponents($component = null)
{
if (is_null($component)) {
return $this->components;
}
$this->validateComponentExists($component);
return $this->components[$component];
}
/**
* Adds a component to the distinguished name.
*
* @param string $component
* @param string $value
*
* @throws \UnexpectedValueException When the given name does not exist.
*/
protected function addComponent($component, $value)
{
$this->validateComponentExists($component);
// We need to make sure the value we're given isn't empty before adding it into our components.
if (!empty($value)) {
$this->components[$component][] = $value;
}
}
/**
* Removes the given value from the given component.
*
* @param string $component
* @param string $value
*
* @throws \UnexpectedValueException When the given component does not exist.
*
* @return void
*/
protected function removeComponent($component, $value)
{
$this->validateComponentExists($component);
$this->components[$component] = array_diff($this->components[$component], [$value]);
}
/**
* Validates that the given component exists in the available components.
*
* @param string $component The name of the component to validate.
*
* @throws \UnexpectedValueException When the given component does not exist.
*
* @return void
*/
protected function validateComponentExists($component)
{
if (!array_key_exists($component, $this->components)) {
throw new \UnexpectedValueException("The RDN component '$component' does not exist.");
}
}
}
@@ -0,0 +1,157 @@
<?php
namespace Adldap\Models\Attributes;
use Adldap\Utilities;
use InvalidArgumentException;
class Guid
{
/**
* The string GUID value.
*
* @var string
*/
protected $value;
/**
* The guid structure in order by section to parse using substr().
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*
* @link https://github.com/ldaptools/ldaptools
*
* @var array
*/
protected $guidSections = [
[[-26, 2], [-28, 2], [-30, 2], [-32, 2]],
[[-22, 2], [-24, 2]],
[[-18, 2], [-20, 2]],
[[-16, 4]],
[[-12, 12]],
];
/**
* The hexadecimal octet order based on string position.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*
* @link https://github.com/ldaptools/ldaptools
*
* @var array
*/
protected $octetSections = [
[6, 4, 2, 0],
[10, 8],
[14, 12],
[16, 18, 20, 22, 24, 26, 28, 30],
];
/**
* Determines if the specified GUID is valid.
*
* @param string $guid
*
* @return bool
*/
public static function isValid($guid)
{
return Utilities::isValidGuid($guid);
}
/**
* Constructor.
*
* @param mixed $value
*
* @throws InvalidArgumentException
*/
public function __construct($value)
{
if (static::isValid($value)) {
$this->value = $value;
} elseif ($value = $this->binaryGuidToString($value)) {
$this->value = $value;
} else {
throw new InvalidArgumentException('Invalid Binary / String GUID.');
}
}
/**
* Returns the string value of the GUID.
*
* @return string
*/
public function __toString()
{
return $this->getValue();
}
/**
* Returns the string value of the SID.
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* Get the binary representation of the GUID string.
*
* @return string
*/
public function getBinary()
{
$data = '';
$guid = str_replace('-', '', $this->value);
foreach ($this->octetSections as $section) {
$data .= $this->parseSection($guid, $section, true);
}
return hex2bin($data);
}
/**
* Returns the string variant of a binary GUID.
*
* @param string $binary
*
* @return string|null
*/
protected function binaryGuidToString($binary)
{
return Utilities::binaryGuidToString($binary);
}
/**
* Return the specified section of the hexadecimal string.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*
* @link https://github.com/ldaptools/ldaptools
*
* @param string $hex The full hex string.
* @param array $sections An array of start and length (unless octet is true, then length is always 2).
* @param bool $octet Whether this is for octet string form.
*
* @return string The concatenated sections in upper-case.
*/
protected function parseSection($hex, array $sections, $octet = false)
{
$parsedString = '';
foreach ($sections as $section) {
$start = $octet ? $section : $section[0];
$length = $octet ? 2 : $section[1];
$parsedString .= substr($hex, $start, $length);
}
return $parsedString;
}
}
@@ -0,0 +1,80 @@
<?php
namespace Adldap\Models\Attributes;
class MbString
{
/**
* Get the integer value of a specific character.
*
* @param $string
*
* @return int
*/
public static function ord($string)
{
if (self::isLoaded()) {
$result = unpack('N', mb_convert_encoding($string, 'UCS-4BE', 'UTF-8'));
if (is_array($result) === true) {
return $result[1];
}
}
return ord($string);
}
/**
* Get the character for a specific integer value.
*
* @param $int
*
* @return string
*/
public static function chr($int)
{
if (self::isLoaded()) {
return mb_convert_encoding(pack('n', $int), 'UTF-8', 'UTF-16BE');
}
return chr($int);
}
/**
* Split a string into its individual characters and return it as an array.
*
* @param string $value
*
* @return string[]
*/
public static function split($value)
{
return preg_split('/(?<!^)(?!$)/u', $value);
}
/**
* Detects if the given string is UTF 8.
*
* @param $string
*
* @return string|false
*/
public static function isUtf8($string)
{
if (self::isLoaded()) {
return mb_detect_encoding($string, 'UTF-8', $strict = true);
}
return $string;
}
/**
* Checks if the mbstring extension is enabled in PHP.
*
* @return bool
*/
public static function isLoaded()
{
return extension_loaded('mbstring');
}
}
@@ -0,0 +1,101 @@
<?php
namespace Adldap\Models\Attributes;
use Adldap\Utilities;
use InvalidArgumentException;
class Sid
{
/**
* The string SID value.
*
* @var string
*/
protected $value;
/**
* Determines if the specified SID is valid.
*
* @param string $sid
*
* @return bool
*/
public static function isValid($sid)
{
return Utilities::isValidSid($sid);
}
/**
* Constructor.
*
* @param mixed $value
*
* @throws InvalidArgumentException
*/
public function __construct($value)
{
if (static::isValid($value)) {
$this->value = $value;
} elseif ($value = $this->binarySidToString($value)) {
$this->value = $value;
} else {
throw new InvalidArgumentException('Invalid Binary / String SID.');
}
}
/**
* Returns the string value of the SID.
*
* @return string
*/
public function __toString()
{
return $this->getValue();
}
/**
* Returns the string value of the SID.
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* Returns the binary variant of the SID.
*
* @return string
*/
public function getBinary()
{
$sid = explode('-', ltrim($this->value, 'S-'));
$level = (int) array_shift($sid);
$authority = (int) array_shift($sid);
$subAuthorities = array_map('intval', $sid);
$params = array_merge(
['C2xxNV*', $level, count($subAuthorities), $authority],
$subAuthorities
);
return call_user_func_array('pack', $params);
}
/**
* Returns the string variant of a binary SID.
*
* @param string $binary
*
* @return string|null
*/
protected function binarySidToString($binary)
{
return Utilities::binarySidToString($binary);
}
}
@@ -0,0 +1,396 @@
<?php
namespace Adldap\Models\Attributes;
class TSProperty
{
/**
* Nibble control values. The first value for each is if the nibble is <= 9, otherwise the second value is used.
*/
const NIBBLE_CONTROL = [
'X' => ['001011', '011010'],
'Y' => ['001110', '011010'],
];
/**
* The nibble header.
*/
const NIBBLE_HEADER = '1110';
/**
* Conversion factor needed for time values in the TSPropertyArray (stored in microseconds).
*/
const TIME_CONVERSION = 60 * 1000;
/**
* A simple map to help determine how the property needs to be decoded/encoded from/to its binary value.
*
* There are some names that are simple repeats but have 'W' at the end. Not sure as to what that signifies. I
* cannot find any information on them in Microsoft documentation. However, their values appear to stay in sync with
* their non 'W' counterparts. But not doing so when manipulating the data manually does not seem to affect anything.
* This probably needs more investigation.
*
* @var array
*/
protected $propTypes = [
'string' => [
'CtxWFHomeDir',
'CtxWFHomeDirW',
'CtxWFHomeDirDrive',
'CtxWFHomeDirDriveW',
'CtxInitialProgram',
'CtxInitialProgramW',
'CtxWFProfilePath',
'CtxWFProfilePathW',
'CtxWorkDirectory',
'CtxWorkDirectoryW',
'CtxCallbackNumber',
],
'time' => [
'CtxMaxDisconnectionTime',
'CtxMaxConnectionTime',
'CtxMaxIdleTime',
],
'int' => [
'CtxCfgFlags1',
'CtxCfgPresent',
'CtxKeyboardLayout',
'CtxMinEncryptionLevel',
'CtxNWLogonServer',
'CtxShadow',
],
];
/**
* The property name.
*
* @var string
*/
protected $name;
/**
* The property value.
*
* @var string|int
*/
protected $value;
/**
* The property value type.
*
* @var int
*/
protected $valueType = 1;
/**
* Pass binary TSProperty data to construct its object representation.
*
* @param string|null $value
*/
public function __construct($value = null)
{
if ($value) {
$this->decode(bin2hex($value));
}
}
/**
* Set the name for the TSProperty.
*
* @param string $name
*
* @return TSProperty
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get the name for the TSProperty.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the value for the TSProperty.
*
* @param string|int $value
*
* @return TSProperty
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* Get the value for the TSProperty.
*
* @return string|int
*/
public function getValue()
{
return $this->value;
}
/**
* Convert the TSProperty name/value back to its binary
* representation for the userParameters blob.
*
* @return string
*/
public function toBinary()
{
$name = bin2hex($this->name);
$binValue = $this->getEncodedValueForProp($this->name, $this->value);
$valueLen = strlen(bin2hex($binValue)) / 3;
$binary = hex2bin(
$this->dec2hex(strlen($name))
.$this->dec2hex($valueLen)
.$this->dec2hex($this->valueType)
.$name
);
return $binary.$binValue;
}
/**
* Given a TSProperty blob, decode the name/value/type/etc.
*
* @param string $tsProperty
*/
protected function decode($tsProperty)
{
$nameLength = hexdec(substr($tsProperty, 0, 2));
// 1 data byte is 3 encoded bytes
$valueLength = hexdec(substr($tsProperty, 2, 2)) * 3;
$this->valueType = hexdec(substr($tsProperty, 4, 2));
$this->name = pack('H*', substr($tsProperty, 6, $nameLength));
$this->value = $this->getDecodedValueForProp($this->name, substr($tsProperty, 6 + $nameLength, $valueLength));
}
/**
* Based on the property name/value in question, get its encoded form.
*
* @param string $propName
* @param string|int $propValue
*
* @return string
*/
protected function getEncodedValueForProp($propName, $propValue)
{
if (in_array($propName, $this->propTypes['string'])) {
// Simple strings are null terminated. Unsure if this is
// needed or simply a product of how ADUC does stuff?
$value = $this->encodePropValue($propValue."\0", true);
} elseif (in_array($propName, $this->propTypes['time'])) {
// Needs to be in microseconds (assuming it is in minute format)...
$value = $this->encodePropValue($propValue * self::TIME_CONVERSION);
} else {
$value = $this->encodePropValue($propValue);
}
return $value;
}
/**
* Based on the property name in question, get its actual value from the binary blob value.
*
* @param string $propName
* @param string $propValue
*
* @return string|int
*/
protected function getDecodedValueForProp($propName, $propValue)
{
if (in_array($propName, $this->propTypes['string'])) {
// Strip away null terminators. I think this should
// be desired, otherwise it just ends in confusion.
$value = str_replace("\0", '', $this->decodePropValue($propValue, true));
} elseif (in_array($propName, $this->propTypes['time'])) {
// Convert from microseconds to minutes (how ADUC displays
// it anyway, and seems the most practical).
$value = hexdec($this->decodePropValue($propValue)) / self::TIME_CONVERSION;
} elseif (in_array($propName, $this->propTypes['int'])) {
$value = hexdec($this->decodePropValue($propValue));
} else {
$value = $this->decodePropValue($propValue);
}
return $value;
}
/**
* Decode the property by inspecting the nibbles of each blob, checking
* the control, and adding up the results into a final value.
*
* @param string $hex
* @param bool $string Whether or not this is simple string data.
*
* @return string
*/
protected function decodePropValue($hex, $string = false)
{
$decodePropValue = '';
$blobs = str_split($hex, 6);
foreach ($blobs as $blob) {
$bin = decbin(hexdec($blob));
$controlY = substr($bin, 4, 6);
$nibbleY = substr($bin, 10, 4);
$controlX = substr($bin, 14, 6);
$nibbleX = substr($bin, 20, 4);
$byte = $this->nibbleControl($nibbleX, $controlX).$this->nibbleControl($nibbleY, $controlY);
if ($string) {
$decodePropValue .= MbString::chr(bindec($byte));
} else {
$decodePropValue = $this->dec2hex(bindec($byte)).$decodePropValue;
}
}
return $decodePropValue;
}
/**
* Get the encoded property value as a binary blob.
*
* @param string $value
* @param bool $string
*
* @return string
*/
protected function encodePropValue($value, $string = false)
{
// An int must be properly padded. (then split and reversed).
// For a string, we just split the chars. This seems
// to be the easiest way to handle UTF-8 characters
// instead of trying to work with their hex values.
$chars = $string ? MbString::split($value) : array_reverse(str_split($this->dec2hex($value, 8), 2));
$encoded = '';
foreach ($chars as $char) {
// Get the bits for the char. Using this method to ensure it is fully padded.
$bits = sprintf('%08b', $string ? MbString::ord($char) : hexdec($char));
$nibbleX = substr($bits, 0, 4);
$nibbleY = substr($bits, 4, 4);
// Construct the value with the header, high nibble, then low nibble.
$value = self::NIBBLE_HEADER;
foreach (['Y' => $nibbleY, 'X' => $nibbleX] as $nibbleType => $nibble) {
$value .= $this->getNibbleWithControl($nibbleType, $nibble);
}
// Convert it back to a binary bit stream
foreach ([0, 8, 16] as $start) {
$encoded .= $this->packBitString(substr($value, $start, 8), 8);
}
}
return $encoded;
}
/**
* PHP's pack() function has no 'b' or 'B' template. This is
* a workaround that turns a literal bit-string into a
* packed byte-string with 8 bits per byte.
*
* @param string $bits
* @param bool $len
*
* @return string
*/
protected function packBitString($bits, $len)
{
$bits = substr($bits, 0, $len);
// Pad input with zeros to next multiple of 4 above $len
$bits = str_pad($bits, 4 * (int) (($len + 3) / 4), '0');
// Split input into chunks of 4 bits, convert each to hex and pack them
$nibbles = str_split($bits, 4);
foreach ($nibbles as $i => $nibble) {
$nibbles[$i] = base_convert($nibble, 2, 16);
}
return pack('H*', implode('', $nibbles));
}
/**
* Based on the control, adjust the nibble accordingly.
*
* @param string $nibble
* @param string $control
*
* @return string
*/
protected function nibbleControl($nibble, $control)
{
// This control stays constant for the low/high nibbles,
// so it doesn't matter which we compare to
if ($control == self::NIBBLE_CONTROL['X'][1]) {
$dec = bindec($nibble);
$dec += 9;
$nibble = str_pad(decbin($dec), 4, '0', STR_PAD_LEFT);
}
return $nibble;
}
/**
* Get the nibble value with the control prefixed.
*
* If the nibble dec is <= 9, the control X equals 001011 and Y equals 001110, otherwise if the nibble dec is > 9
* the control for X or Y equals 011010. Additionally, if the dec value of the nibble is > 9, then the nibble value
* must be subtracted by 9 before the final value is constructed.
*
* @param string $nibbleType Either X or Y
* @param string $nibble
*
* @return string
*/
protected function getNibbleWithControl($nibbleType, $nibble)
{
$dec = bindec($nibble);
if ($dec > 9) {
$dec -= 9;
$control = self::NIBBLE_CONTROL[$nibbleType][1];
} else {
$control = self::NIBBLE_CONTROL[$nibbleType][0];
}
return $control.sprintf('%04d', decbin($dec));
}
/**
* Need to make sure hex values are always an even length, so pad as needed.
*
* @param int $int
* @param int $padLength The hex string must be padded to this length (with zeros).
*
* @return string
*/
protected function dec2hex($int, $padLength = 2)
{
return str_pad(dechex($int), $padLength, 0, STR_PAD_LEFT);
}
}
@@ -0,0 +1,295 @@
<?php
namespace Adldap\Models\Attributes;
use InvalidArgumentException;
class TSPropertyArray
{
/**
* Represents that the TSPropertyArray data is valid.
*/
const VALID_SIGNATURE = 'P';
/**
* The default values for the TSPropertyArray structure.
*
* @var array
*/
const DEFAULTS = [
'CtxCfgPresent' => 2953518677,
'CtxWFProfilePath' => '',
'CtxWFProfilePathW' => '',
'CtxWFHomeDir' => '',
'CtxWFHomeDirW' => '',
'CtxWFHomeDirDrive' => '',
'CtxWFHomeDirDriveW' => '',
'CtxShadow' => 1,
'CtxMaxDisconnectionTime' => 0,
'CtxMaxConnectionTime' => 0,
'CtxMaxIdleTime' => 0,
'CtxWorkDirectory' => '',
'CtxWorkDirectoryW' => '',
'CtxCfgFlags1' => 2418077696,
'CtxInitialProgram' => '',
'CtxInitialProgramW' => '',
];
/**
* @var string The default data that occurs before the TSPropertyArray (CtxCfgPresent with a bunch of spaces...?)
*/
protected $defaultPreBinary = '43747843666750726573656e742020202020202020202020202020202020202020202020202020202020202020202020';
/**
* @var TSProperty[]
*/
protected $tsProperty = [];
/**
* @var string
*/
protected $signature = self::VALID_SIGNATURE;
/**
* Binary data that occurs before the TSPropertyArray data in userParameters.
*
* @var string
*/
protected $preBinary = '';
/**
* Binary data that occurs after the TSPropertyArray data in userParameters.
*
* @var string
*/
protected $postBinary = '';
/**
* Construct in one of the following ways:.
*
* - Pass an array of TSProperty key => value pairs (See DEFAULTS constant).
* - Pass the userParameters binary value. The object representation of that will be decoded and constructed.
* - Pass nothing and a default set of TSProperty key => value pairs will be used (See DEFAULTS constant).
*
* @param mixed $tsPropertyArray
*/
public function __construct($tsPropertyArray = null)
{
$this->preBinary = hex2bin($this->defaultPreBinary);
if (is_null($tsPropertyArray) || is_array($tsPropertyArray)) {
$tsPropertyArray = $tsPropertyArray ?: self::DEFAULTS;
foreach ($tsPropertyArray as $key => $value) {
$tsProperty = new TSProperty();
$this->tsProperty[$key] = $tsProperty->setName($key)->setValue($value);
}
} else {
$this->decodeUserParameters($tsPropertyArray);
}
}
/**
* Check if a specific TSProperty exists by its property name.
*
* @param string $propName
*
* @return bool
*/
public function has($propName)
{
return array_key_exists(strtolower($propName), array_change_key_case($this->tsProperty));
}
/**
* Get a TSProperty object by its property name (ie. CtxWFProfilePath).
*
* @param string $propName
*
* @return TSProperty
*/
public function get($propName)
{
$this->validateProp($propName);
return $this->getTsPropObj($propName);
}
/**
* Add a TSProperty object. If it already exists, it will be overwritten.
*
* @param TSProperty $tsProperty
*
* @return $this
*/
public function add(TSProperty $tsProperty)
{
$this->tsProperty[$tsProperty->getName()] = $tsProperty;
return $this;
}
/**
* Remove a TSProperty by its property name (ie. CtxMinEncryptionLevel).
*
* @param string $propName
*
* @return $this
*/
public function remove($propName)
{
foreach (array_keys($this->tsProperty) as $property) {
if (strtolower($propName) == strtolower($property)) {
unset($this->tsProperty[$property]);
}
}
return $this;
}
/**
* Set the value for a specific TSProperty by its name.
*
* @param string $propName
* @param mixed $propValue
*
* @return $this
*/
public function set($propName, $propValue)
{
$this->validateProp($propName);
$this->getTsPropObj($propName)->setValue($propValue);
return $this;
}
/**
* Get the full binary representation of the userParameters containing the TSPropertyArray data.
*
* @return string
*/
public function toBinary()
{
$binary = $this->preBinary;
$binary .= hex2bin(str_pad(dechex(MbString::ord($this->signature)), 2, 0, STR_PAD_LEFT));
$binary .= hex2bin(str_pad(dechex(count($this->tsProperty)), 2, 0, STR_PAD_LEFT));
foreach ($this->tsProperty as $tsProperty) {
$binary .= $tsProperty->toBinary();
}
return $binary.$this->postBinary;
}
/**
* Get a simple associative array containing of all TSProperty names and values.
*
* @return array
*/
public function toArray()
{
$userParameters = [];
foreach ($this->tsProperty as $property => $tsPropObj) {
$userParameters[$property] = $tsPropObj->getValue();
}
return $userParameters;
}
/**
* Get all TSProperty objects.
*
* @return TSProperty[]
*/
public function getTSProperties()
{
return $this->tsProperty;
}
/**
* Validates that the given property name exists.
*
* @param string $propName
*/
protected function validateProp($propName)
{
if (!$this->has($propName)) {
throw new InvalidArgumentException(sprintf('TSProperty for "%s" does not exist.', $propName));
}
}
/**
* @param string $propName
*
* @return TSProperty
*/
protected function getTsPropObj($propName)
{
return array_change_key_case($this->tsProperty)[strtolower($propName)];
}
/**
* Get an associative array with all of the userParameters property names and values.
*
* @param string $userParameters
*
* @return void
*/
protected function decodeUserParameters($userParameters)
{
$userParameters = bin2hex($userParameters);
// Save the 96-byte array of reserved data, so as to not ruin anything that may be stored there.
$this->preBinary = hex2bin(substr($userParameters, 0, 96));
// The signature is a 2-byte unicode character at the front
$this->signature = MbString::chr(hexdec(substr($userParameters, 96, 2)));
// This asserts the validity of the tsPropertyArray data. For some reason 'P' means valid...
if ($this->signature != self::VALID_SIGNATURE) {
throw new InvalidArgumentException('Invalid TSPropertyArray data');
}
// The property count is a 2-byte unsigned integer indicating the number of elements for the tsPropertyArray
// It starts at position 98. The actual variable data begins at position 100.
$length = $this->addTSPropData(substr($userParameters, 100), hexdec(substr($userParameters, 98, 2)));
// Reserved data length + (count and sig length == 4) + the added lengths of the TSPropertyArray
// This saves anything after that variable TSPropertyArray data, so as to not squash anything stored there
if (strlen($userParameters) > (96 + 4 + $length)) {
$this->postBinary = hex2bin(substr($userParameters, (96 + 4 + $length)));
}
}
/**
* Given the start of TSPropertyArray hex data, and the count for the number
* of TSProperty structures in contains, parse and split out the
* individual TSProperty structures. Return the full length
* of the TSPropertyArray data.
*
* @param string $tsPropertyArray
* @param int $tsPropCount
*
* @return int The length of the data in the TSPropertyArray
*/
protected function addTSPropData($tsPropertyArray, $tsPropCount)
{
$length = 0;
for ($i = 0; $i < $tsPropCount; $i++) {
// Prop length = name length + value length + type length + the space for the length data.
$propLength = hexdec(substr($tsPropertyArray, $length, 2)) + (hexdec(substr($tsPropertyArray, $length + 2, 2)) * 3) + 6;
$tsProperty = new TSProperty(hex2bin(substr($tsPropertyArray, $length, $propLength)));
$this->tsProperty[$tsProperty->getName()] = $tsProperty;
$length += $propLength;
}
return $length;
}
}
@@ -0,0 +1,270 @@
<?php
namespace Adldap\Models;
use InvalidArgumentException;
/**
* Class BatchModification.
*
* A utility class to assist in the creation of LDAP
* batch modifications and ensure their validity.
*/
class BatchModification
{
/**
* The array keys to be used in batch modifications.
*/
const KEY_ATTRIB = 'attrib';
const KEY_MODTYPE = 'modtype';
const KEY_VALUES = 'values';
/**
* The original value of the attribute before modification.
*
* @var null
*/
protected $original = null;
/**
* The attribute of the modification.
*
* @var int|string
*/
protected $attribute;
/**
* The values of the modification.
*
* @var array
*/
protected $values = [];
/**
* The modtype integer of the batch modification.
*
* @var int
*/
protected $type;
/**
* Constructor.
*
* @param string|null $attribute
* @param string|int|null $type
* @param array $values
*/
public function __construct($attribute = null, $type = null, $values = [])
{
$this->setAttribute($attribute)
->setType($type)
->setValues($values);
}
/**
* Sets the original value of the attribute before modification.
*
* @param mixed $original
*
* @return $this
*/
public function setOriginal($original = null)
{
$this->original = $original;
return $this;
}
/**
* Returns the original value of the attribute before modification.
*
* @return mixed
*/
public function getOriginal()
{
return $this->original;
}
/**
* Sets the attribute of the modification.
*
* @param string $attribute
*
* @return $this
*/
public function setAttribute($attribute)
{
$this->attribute = $attribute;
return $this;
}
/**
* Returns the attribute of the modification.
*
* @return string
*/
public function getAttribute()
{
return $this->attribute;
}
/**
* Sets the values of the modification.
*
* @param array $values
*
* @return $this
*/
public function setValues(array $values = [])
{
$this->values = array_map(function ($value) {
// We need to make sure all values given to a batch modification are
// strings, otherwise we'll receive an LDAP exception when
// we try to process the modification.
return (string) $value;
}, $values);
return $this;
}
/**
* Returns the values of the modification.
*
* @return array
*/
public function getValues()
{
return $this->values;
}
/**
* Sets the type of the modification.
*
* @param int|null $type
*
* @return $this
*/
public function setType($type = null)
{
if (!is_null($type) && !$this->isValidType($type)) {
throw new InvalidArgumentException('Given batch modification type is invalid.');
}
$this->type = $type;
return $this;
}
/**
* Returns the type of the modification.
*
* @return int
*/
public function getType()
{
return $this->type;
}
/**
* Determines if the batch modification
* is valid in its current state.
*
* @return bool
*/
public function isValid()
{
return !is_null($this->get());
}
/**
* Builds the type of modification automatically
* based on the current and original values.
*
* @return $this
*/
public function build()
{
$filtered = array_diff(
array_map('trim', $this->values),
['']
);
if (is_null($this->original)) {
// If the original value is null, we'll assume
// that the attribute doesn't exist yet.
if (!empty($filtered)) {
// If the filtered array is not empty, we can
// assume we're looking to add values
// to the current attribute.
$this->setType(LDAP_MODIFY_BATCH_ADD);
}
// If the filtered array is empty and there is no original
// value, then we can ignore this attribute since
// we can't push null values to the server.
} else {
if (empty($filtered)) {
// If there's an original value and the array is
// empty then we can assume we are looking
// to completely remove all values
// of the current attribute.
$this->setType(LDAP_MODIFY_BATCH_REMOVE_ALL);
} else {
// If the array isn't empty then we can assume
// we're looking to replace all attributes.
$this->setType(LDAP_MODIFY_BATCH_REPLACE);
}
}
return $this;
}
/**
* Returns the built batch modification array.
*
* @return array|null
*/
public function get()
{
switch ($this->type) {
case LDAP_MODIFY_BATCH_REMOVE_ALL:
// A values key cannot be provided when
// a remove all type is selected.
return [
static::KEY_ATTRIB => $this->attribute,
static::KEY_MODTYPE => $this->type,
];
case LDAP_MODIFY_BATCH_REMOVE:
// Fallthrough.
case LDAP_MODIFY_BATCH_ADD:
// Fallthrough.
case LDAP_MODIFY_BATCH_REPLACE:
return [
static::KEY_ATTRIB => $this->attribute,
static::KEY_MODTYPE => $this->type,
static::KEY_VALUES => $this->values,
];
default:
// If the modtype isn't recognized, we'll return null.
return;
}
}
/**
* Determines if the given modtype is valid.
*
* @param int $type
*
* @return bool
*/
protected function isValidType($type)
{
return in_array($type, [
LDAP_MODIFY_BATCH_REMOVE_ALL,
LDAP_MODIFY_BATCH_REMOVE,
LDAP_MODIFY_BATCH_REPLACE,
LDAP_MODIFY_BATCH_ADD,
]);
}
}
@@ -0,0 +1,87 @@
<?php
namespace Adldap\Models;
/**
* Class Computer.
*
* Represents an LDAP computer / server.
*/
class Computer extends Entry
{
use Concerns\HasMemberOf;
use Concerns\HasDescription;
use Concerns\HasLastLogonAndLogOff;
use Concerns\HasUserAccountControl;
use Concerns\HasCriticalSystemObject;
/**
* Returns the computers operating system.
*
* @link https://msdn.microsoft.com/en-us/library/ms679076(v=vs.85).aspx
*
* @return string
*/
public function getOperatingSystem()
{
return $this->getFirstAttribute($this->schema->operatingSystem());
}
/**
* Returns the computers operating system version.
*
* @link https://msdn.microsoft.com/en-us/library/ms679079(v=vs.85).aspx
*
* @return string
*/
public function getOperatingSystemVersion()
{
return $this->getFirstAttribute($this->schema->operatingSystemVersion());
}
/**
* Returns the computers operating system service pack.
*
* @link https://msdn.microsoft.com/en-us/library/ms679078(v=vs.85).aspx
*
* @return string
*/
public function getOperatingSystemServicePack()
{
return $this->getFirstAttribute($this->schema->operatingSystemServicePack());
}
/**
* Returns the computers DNS host name.
*
* @return string
*/
public function getDnsHostName()
{
return $this->getFirstAttribute($this->schema->dnsHostName());
}
/**
* Returns the computers bad password time.
*
* @link https://msdn.microsoft.com/en-us/library/ms675243(v=vs.85).aspx
*
* @return int
*/
public function getBadPasswordTime()
{
return $this->getFirstAttribute($this->schema->badPasswordTime());
}
/**
* Returns the computers account expiry date.
*
* @link https://msdn.microsoft.com/en-us/library/ms675098(v=vs.85).aspx
*
* @return int
*/
public function getAccountExpiry()
{
return $this->getFirstAttribute($this->schema->accountExpires());
}
}
@@ -0,0 +1,345 @@
<?php
namespace Adldap\Models\Concerns;
use Illuminate\Support\Arr;
trait HasAttributes
{
/**
* The default output date format for all time related methods.
*
* Default format is suited for MySQL timestamps.
*
* @var string
*/
public $dateFormat = 'Y-m-d H:i:s';
/**
* The format that is used to convert timestamps to unix timestamps.
*
* Currently set for compatibility with Active Directory.
*
* @var string
*/
protected $timestampFormat = 'YmdHis.0Z';
/**
* The models attributes.
*
* @var array
*/
protected $attributes = [];
/**
* The models original attributes.
*
* @var array
*/
protected $original = [];
/**
* Dynamically retrieve attributes on the object.
*
* @param mixed $key
*
* @return bool
*/
public function __get($key)
{
return $this->getAttribute($key);
}
/**
* Dynamically set attributes on the object.
*
* @param mixed $key
* @param mixed $value
*
* @return $this
*/
public function __set($key, $value)
{
return $this->setAttribute($key, $value);
}
/**
* Synchronizes the models original attributes
* with the model's current attributes.
*
* @return $this
*/
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
/**
* Returns the models attribute with the specified key.
*
* If a sub-key is specified, it will try and
* retrieve it from the parent keys array.
*
* @param int|string $key
* @param int|string $subKey
*
* @return mixed
*/
public function getAttribute($key, $subKey = null)
{
if (!$key) {
return;
}
// We'll normalize the given key to prevent case sensitivity issues.
$key = $this->normalizeAttributeKey($key);
if (is_null($subKey) && $this->hasAttribute($key)) {
return $this->attributes[$key];
} elseif ($this->hasAttribute($key, $subKey)) {
return $this->attributes[$key][$subKey];
}
}
/**
* Returns the first attribute by the specified key.
*
* @param string $key
*
* @return mixed
*/
public function getFirstAttribute($key)
{
return $this->getAttribute($key, 0);
}
/**
* Returns all of the models attributes.
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Fills the entry with the supplied attributes.
*
* @param array $attributes
*
* @return $this
*/
public function fill(array $attributes = [])
{
foreach ($attributes as $key => $value) {
$this->setAttribute($key, $value);
}
return $this;
}
/**
* Sets an attributes value by the specified key and sub-key.
*
* @param int|string $key
* @param mixed $value
* @param int|string $subKey
*
* @return $this
*/
public function setAttribute($key, $value, $subKey = null)
{
// Normalize key.
$key = $this->normalizeAttributeKey($key);
// If the key is equal to 'dn', we'll automatically
// change it to the full attribute name.
$key = ($key == 'dn' ? $this->schema->distinguishedName() : $key);
if (is_null($subKey)) {
// We need to ensure all attributes are set as arrays so all
// of our model methods retrieve attributes correctly.
$this->attributes[$key] = is_array($value) ? $value : [$value];
} else {
$this->attributes[$key][$subKey] = $value;
}
return $this;
}
/**
* Sets the first attributes value by the specified key.
*
* @param int|string $key
* @param mixed $value
*
* @return $this
*/
public function setFirstAttribute($key, $value)
{
return $this->setAttribute($key, $value, 0);
}
/**
* Sets the attributes property.
*
* Used when constructing an existing LDAP record.
*
* @param array $attributes
*
* @return $this
*/
public function setRawAttributes(array $attributes = [])
{
// We'll filter out those annoying 'count' keys returned with LDAP results,
// and lowercase all root array keys to prevent any casing issues.
$this->attributes = array_change_key_case($this->filterRawAttributes($attributes), CASE_LOWER);
// We'll pull out the distinguished name from our raw attributes
// and set it into our attributes array with the full attribute
// definition. This allows us to normalize distinguished
// names across different LDAP variants.
if ($dn = Arr::get($attributes, 'dn')) {
// In some LDAP variants, the distinguished
// name is returned as an array.
if (is_array($dn)) {
$dn = Arr::first($dn);
}
$this->setDistinguishedName($dn);
}
$this->syncOriginal();
// Set exists to true since raw attributes are only
// set in the case of attributes being loaded by
// query results.
$this->exists = true;
return $this;
}
/**
* Filters the count key recursively from raw LDAP attributes.
*
* @param array $attributes
* @param array|string $keys
*
* @return array
*/
public function filterRawAttributes(array $attributes = [], $keys = ['count', 'dn'])
{
$attributes = Arr::except($attributes, $keys);
array_walk($attributes, function (&$value) use ($keys) {
$value = is_array($value) ?
$this->filterRawAttributes($value, $keys) :
$value;
});
return $attributes;
}
/**
* Returns true / false if the specified attribute
* exists in the attributes array.
*
* @param int|string $key
* @param int|string $subKey
*
* @return bool
*/
public function hasAttribute($key, $subKey = null)
{
// Normalize key.
$key = $this->normalizeAttributeKey($key);
if (is_null($subKey)) {
return Arr::has($this->attributes, $key);
}
return Arr::has($this->attributes, "$key.$subKey");
}
/**
* Returns the number of attributes inside
* the attributes property.
*
* @return int
*/
public function countAttributes()
{
return count($this->getAttributes());
}
/**
* Returns the models original attributes.
*
* @return array
*/
public function getOriginal()
{
return $this->original;
}
/**
* Get the attributes that have been changed since last sync.
*
* @return array
*/
public function getDirty()
{
$dirty = [];
foreach ($this->attributes as $key => $value) {
if (!$this->originalIsEquivalent($key)) {
// We need to reset the array's indices using array_values due to
// LDAP requiring consecutive indices (0, 1, 2 etc.)
$dirty[$key] = array_values($value);
}
}
return $dirty;
}
/**
* Returns a normalized attribute key.
*
* @param string $key
*
* @return string
*/
protected function normalizeAttributeKey($key)
{
return strtolower($key);
}
/**
* Determine if the new and old values for a given key are equivalent.
*
* @param string $key
*
* @return bool
*/
protected function originalIsEquivalent($key)
{
if (!array_key_exists($key, $this->original)) {
return false;
}
$current = $this->attributes[$key];
$original = $this->original[$key];
if ($current === $original) {
return true;
}
return is_numeric($current) &&
is_numeric($original) &&
strcmp((string) $current, (string) $original) === 0;
}
}
@@ -0,0 +1,18 @@
<?php
namespace Adldap\Models\Concerns;
trait HasCriticalSystemObject
{
/**
* Returns true / false if the entry is a critical system object.
*
* @return null|bool
*/
public function isCriticalSystemObject()
{
$attribute = $this->getFirstAttribute($this->schema->isCriticalSystemObject());
return $this->convertStringToBool($attribute);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Adldap\Models\Concerns;
trait HasDescription
{
/**
* Returns the models's description.
*
* @link https://msdn.microsoft.com/en-us/library/ms675492(v=vs.85).aspx
*
* @return string
*/
public function getDescription()
{
return $this->getFirstAttribute($this->schema->description());
}
/**
* Sets the models's description.
*
* @param string $description
*
* @return $this
*/
public function setDescription($description)
{
return $this->setFirstAttribute($this->schema->description(), $description);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Adldap\Models\Concerns;
use Adldap\Adldap;
use Adldap\Models\Events\Event;
trait HasEvents
{
/**
* Fires the specified model event.
*
* @param Event $event
*
* @return mixed
*/
protected function fireModelEvent(Event $event)
{
return Adldap::getEventDispatcher()->fire($event);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Adldap\Models\Concerns;
trait HasLastLogonAndLogOff
{
/**
* Returns the models's last log off date.
*
* @link https://msdn.microsoft.com/en-us/library/ms676822(v=vs.85).aspx
*
* @return string
*/
public function getLastLogOff()
{
return $this->getFirstAttribute($this->schema->lastLogOff());
}
/**
* Returns the models's last log on date.
*
* @link https://msdn.microsoft.com/en-us/library/ms676823(v=vs.85).aspx
*
* @return string
*/
public function getLastLogon()
{
return $this->getFirstAttribute($this->schema->lastLogOn());
}
/**
* Returns the models's last log on timestamp.
*
* @link https://msdn.microsoft.com/en-us/library/ms676824(v=vs.85).aspx
*
* @return string
*/
public function getLastLogonTimestamp()
{
return $this->getFirstAttribute($this->schema->lastLogOnTimestamp());
}
}
@@ -0,0 +1,260 @@
<?php
namespace Adldap\Models\Concerns;
use Adldap\Utilities;
use Adldap\Models\User;
use Adldap\Models\Group;
use Adldap\Query\Collection;
trait HasMemberOf
{
/**
* Returns an array of distinguished names of groups that the current model belongs to.
*
* @link https://msdn.microsoft.com/en-us/library/ms677099(v=vs.85).aspx
*
* @return array
*/
public function getMemberOf()
{
$dns = $this->getAttribute($this->schema->memberOf());
// Normalize returned distinguished names if the attribute is null.
return is_array($dns) ? $dns : [];
}
/**
* Adds the current model to the specified group.
*
* @param string|Group $group
*
* @return bool
*/
public function addGroup($group)
{
if (is_string($group)) {
// If the group is a string, we'll assume the dev is passing
// in a DN string of the group. We'll try to locate it.
$group = $this->query->newInstance()->findByDn($group);
}
if ($group instanceof Group) {
// If the group is Group model instance, we can
// add the current models DN to the group.
return $group->addMember($this->getDn());
}
return false;
}
/**
* Removes the current model from the specified group.
*
* @param string|Group $group
*
* @return bool
*/
public function removeGroup($group)
{
if (is_string($group)) {
// If the group is a string, we'll assume the dev is passing
// in a DN string of the group. We'll try to locate it.
$group = $this->query->newInstance()->findByDn($group);
}
if ($group instanceof Group) {
// If the group is Group model instance, we can
// remove the current models DN from the group.
return $group->removeMember($this->getDn());
}
return false;
}
/**
* Removes the current model from all groups.
*
* @return array The group distinguished names that were successfully removed
*/
public function removeAllGroups()
{
$removed = [];
foreach ($this->getMemberOf() as $group) {
if ($this->removeGroup($group)) {
$removed[] = $group;
}
}
return $removed;
}
/**
* Returns the models groups that it is apart of.
*
* If a recursive option is given, groups of groups
* are retrieved and then merged with
* the resulting collection.
*
* @link https://msdn.microsoft.com/en-us/library/ms677099(v=vs.85).aspx
*
* @param array $fields
* @param bool $recursive
* @param array $visited
*
* @return Collection
*/
public function getGroups(array $fields = ['*'], $recursive = false, array $visited = [])
{
if (!in_array($this->schema->memberOf(), $fields)) {
// We want to make sure that we always select the memberof
// field in case developers want recursive members.
$fields = array_merge($fields, [$this->schema->memberOf()]);
}
$groups = $this->getGroupsByNames($this->getMemberOf(), $fields);
// We need to check if we're working with a User model. Only users
// contain a primary group. If we are, we'll merge the users
// primary group into the resulting collection.
if ($this instanceof User && $primary = $this->getPrimaryGroup()) {
$groups->push($primary);
}
// If recursive results are requested, we'll ask each group
// for their groups, and merge the resulting collection.
if ($recursive) {
/** @var Group $group */
foreach ($groups as $group) {
// We need to validate that we haven't already queried
// for this group's members so we don't allow
// infinite recursion in case of circular
// group dependencies in LDAP.
if (!in_array($group->getDn(), $visited)) {
$visited[] = $group->getDn();
$members = $group->getGroups($fields, $recursive, $visited);
/** @var Group $member */
foreach ($members as $member) {
$visited[] = $member->getDn();
}
$groups = $groups->merge($members);
}
}
}
return $groups;
}
/**
* Returns the models groups names in a single dimension array.
*
* If a recursive option is given, groups of groups
* are retrieved and then merged with
* the resulting collection.
*
* @param bool $recursive
*
* @return array
*/
public function getGroupNames($recursive = false)
{
$fields = [$this->schema->commonName(), $this->schema->memberOf()];
$names = $this->getGroups($fields, $recursive)->map(function (Group $group) {
return $group->getCommonName();
})->toArray();
return array_unique($names);
}
/**
* Determine if the current model is a member of the specified group(s).
*
* @param mixed $group
* @param bool $recursive
*
* @return bool
*/
public function inGroup($group, $recursive = false)
{
$memberOf = $this->getGroups(['cn'], $recursive);
if ($group instanceof Collection) {
// If we've been given a collection then we'll convert
// it to an array to normalize the value.
$group = $group->toArray();
}
$groups = is_array($group) ? $group : [$group];
foreach ($groups as $group) {
// We need to iterate through each given group that the
// model must be apart of, then go through the models
// actual groups and perform validation.
$exists = $memberOf->filter(function (Group $parent) use ($group) {
return $this->groupIsParent($group, $parent);
})->count() !== 0;
if (!$exists) {
// If the current group isn't at all contained
// in the memberOf collection, we'll
// return false here.
return false;
}
}
return true;
}
/**
* Retrieves groups by their distinguished name.
*
* @param array $dns
* @param array $fields
*
* @return Collection
*/
protected function getGroupsByNames(array $dns = [], $fields = [])
{
$query = $this->query->newInstance();
return $query->newCollection($dns)->map(function ($dn) use ($query, $fields) {
return $query->select($fields)->clearFilters()->findByDn($dn);
})->filter(function ($group) {
return $group instanceof Group;
});
}
/**
* Validates if the specified group is the given parent instance.
*
* @param Group|string $group
* @param Group $parent
*
* @return bool
*/
protected function groupIsParent($group, Group $parent)
{
if ($group instanceof Group) {
// We've been given a group instance, we'll compare their DNs.
return $parent->getDn() === $group->getDn();
}
if (Utilities::explodeDn($group)) {
// We've been given a DN, we'll compare it to the parents.
return $parent->getDn() === $group;
}
if (!empty($group)) {
// We've been given just a string, we'll
// compare it to the parents name.
return $parent->getCommonName() === $group;
}
return false;
}
}
@@ -0,0 +1,60 @@
<?php
namespace Adldap\Models\Concerns;
use Adldap\Models\Attributes\AccountControl;
trait HasUserAccountControl
{
/**
* Returns the users user account control integer.
*
* @return string
*/
public function getUserAccountControl()
{
return $this->getFirstAttribute($this->schema->userAccountControl());
}
/**
* Returns the users user account control as an AccountControl object.
*
* @return AccountControl
*/
public function getUserAccountControlObject()
{
return new AccountControl($this->getUserAccountControl());
}
/**
* Sets the users account control property.
*
* @param int|string|AccountControl $accountControl
*
* @return $this
*/
public function setUserAccountControl($accountControl)
{
return $this->setAttribute($this->schema->userAccountControl(), (string) $accountControl);
}
/**
* Returns if the user is disabled.
*
* @return bool
*/
public function isDisabled()
{
return ($this->getUserAccountControl() & AccountControl::ACCOUNTDISABLE) === AccountControl::ACCOUNTDISABLE;
}
/**
* Returns if the user is enabled.
*
* @return bool
*/
public function isEnabled()
{
return $this->getUserAccountControl() === null ? false : !$this->isDisabled();
}
}
@@ -0,0 +1,453 @@
<?php
namespace Adldap\Models\Concerns;
trait HasUserProperties
{
/**
* Returns the users country.
*
* @return string|null
*/
public function getCountry()
{
return $this->getFirstAttribute($this->schema->country());
}
/**
* Sets the users country.
*
* @param string $country
*
* @return $this
*/
public function setCountry($country)
{
return $this->setFirstAttribute($this->schema->country(), $country);
}
/**
* Returns the users department.
*
* @link https://msdn.microsoft.com/en-us/library/ms675490(v=vs.85).aspx
*
* @return string|null
*/
public function getDepartment()
{
return $this->getFirstAttribute($this->schema->department());
}
/**
* Sets the users department.
*
* @param string $department
*
* @return $this
*/
public function setDepartment($department)
{
return $this->setFirstAttribute($this->schema->department(), $department);
}
/**
* Returns the users email address.
*
* @link https://msdn.microsoft.com/en-us/library/ms676855(v=vs.85).aspx
*
* @return string|null
*/
public function getEmail()
{
return $this->getFirstAttribute($this->schema->email());
}
/**
* Sets the users email.
*
* Keep in mind this will remove all other
* email addresses the user currently has.
*
* @param string $email
*
* @return $this
*/
public function setEmail($email)
{
return $this->setFirstAttribute($this->schema->email(), $email);
}
/**
* Returns the users facsimile number.
*
* @link https://msdn.microsoft.com/en-us/library/ms675675(v=vs.85).aspx
*
* @return string|null
*/
public function getFacsimileNumber()
{
return $this->getFirstAttribute($this->schema->facsimile());
}
/**
* Sets the users facsimile number.
*
* @param string $number
*
* @return $this
*/
public function setFacsimileNumber($number)
{
return $this->setFirstAttribute($this->schema->facsimile(), $number);
}
/**
* Returns the users first name.
*
* @link https://msdn.microsoft.com/en-us/library/ms675719(v=vs.85).aspx
*
* @return string|null
*/
public function getFirstName()
{
return $this->getFirstAttribute($this->schema->firstName());
}
/**
* Sets the users first name.
*
* @param string $firstName
*
* @return $this
*/
public function setFirstName($firstName)
{
return $this->setFirstAttribute($this->schema->firstName(), $firstName);
}
/**
* Returns the users initials.
*
* @return string|null
*/
public function getInitials()
{
return $this->getFirstAttribute($this->schema->initials());
}
/**
* Sets the users initials.
*
* @param string $initials
*
* @return $this
*/
public function setInitials($initials)
{
return $this->setFirstAttribute($this->schema->initials(), $initials);
}
/**
* Returns the users IP Phone.
*
* @return string|null
*/
public function getIpPhone()
{
return $this->getFirstAttribute($this->schema->ipPhone());
}
/**
* Sets the users IP phone.
*
* @param string $ip
*
* @return $this
*/
public function setIpPhone($ip)
{
return $this->setFirstAttribute($this->schema->ipPhone(), $ip);
}
/**
* Returns the users last name.
*
* @link https://msdn.microsoft.com/en-us/library/ms679872(v=vs.85).aspx
*
* @return string|null
*/
public function getLastName()
{
return $this->getFirstAttribute($this->schema->lastName());
}
/**
* Sets the users last name.
*
* @param string $lastName
*
* @return $this
*/
public function setLastName($lastName)
{
return $this->setFirstAttribute($this->schema->lastName(), $lastName);
}
/**
* Returns the users postal code.
*
* @return string|null
*/
public function getPostalCode()
{
return $this->getFirstAttribute($this->schema->postalCode());
}
/**
* Sets the users postal code.
*
* @param string $postalCode
*
* @return $this
*/
public function setPostalCode($postalCode)
{
return $this->setFirstAttribute($this->schema->postalCode(), $postalCode);
}
/**
* Get the users post office box.
*
* @return string|null
*/
public function getPostOfficeBox()
{
return $this->getFirstAttribute($this->schema->postOfficeBox());
}
/**
* Sets the users post office box.
*
* @param string|int $box
*
* @return $this
*/
public function setPostOfficeBox($box)
{
return $this->setFirstAttribute($this->schema->postOfficeBox(), $box);
}
/**
* Sets the users proxy addresses.
*
* This will remove all proxy addresses on the user and insert the specified addresses.
*
* @link https://msdn.microsoft.com/en-us/library/ms679424(v=vs.85).aspx
*
* @param array $addresses
*
* @return $this
*/
public function setProxyAddresses(array $addresses = [])
{
return $this->setAttribute($this->schema->proxyAddresses(), $addresses);
}
/**
* Add's a single proxy address to the user.
*
* @param string $address
*
* @return $this
*/
public function addProxyAddress($address)
{
$addresses = $this->getProxyAddresses();
$addresses[] = $address;
return $this->setAttribute($this->schema->proxyAddresses(), $addresses);
}
/**
* Returns the users proxy addresses.
*
* @link https://msdn.microsoft.com/en-us/library/ms679424(v=vs.85).aspx
*
* @return array
*/
public function getProxyAddresses()
{
return $this->getAttribute($this->schema->proxyAddresses()) ?? [];
}
/**
* Returns the users street address.
*
* @return string|null
*/
public function getStreetAddress()
{
return $this->getFirstAttribute($this->schema->streetAddress());
}
/**
* Sets the users street address.
*
* @param string $address
*
* @return $this
*/
public function setStreetAddress($address)
{
return $this->setFirstAttribute($this->schema->streetAddress(), $address);
}
/**
* Returns the users title.
*
* @link https://msdn.microsoft.com/en-us/library/ms680037(v=vs.85).aspx
*
* @return string|null
*/
public function getTitle()
{
return $this->getFirstAttribute($this->schema->title());
}
/**
* Sets the users title.
*
* @param string $title
*
* @return $this
*/
public function setTitle($title)
{
return $this->setFirstAttribute($this->schema->title(), $title);
}
/**
* Returns the users telephone number.
*
* @link https://msdn.microsoft.com/en-us/library/ms680027(v=vs.85).aspx
*
* @return string|null
*/
public function getTelephoneNumber()
{
return $this->getFirstAttribute($this->schema->telephone());
}
/**
* Sets the users telephone number.
*
* @param string $number
*
* @return $this
*/
public function setTelephoneNumber($number)
{
return $this->setFirstAttribute($this->schema->telephone(), $number);
}
/**
* Returns the users primary mobile phone number.
*
* @return string|null
*/
public function getMobileNumber()
{
return $this->getFirstAttribute($this->schema->mobile());
}
/**
* Sets the users primary mobile phone number.
*
* @param string $number
*
* @return $this
*/
public function setMobileNumber($number)
{
return $this->setFirstAttribute($this->schema->mobile(), $number);
}
/**
* Returns the users secondary (other) mobile phone number.
*
* @return string|null
*/
public function getOtherMobileNumber()
{
return $this->getFirstAttribute($this->schema->otherMobile());
}
/**
* Sets the users secondary (other) mobile phone number.
*
* @param string $number
*
* @return $this
*/
public function setOtherMobileNumber($number)
{
return $this->setFirstAttribute($this->schema->otherMobile(), $number);
}
/**
* Returns the users other mailbox attribute.
*
* @link https://msdn.microsoft.com/en-us/library/ms679091(v=vs.85).aspx
*
* @return array
*/
public function getOtherMailbox()
{
return $this->getAttribute($this->schema->otherMailbox());
}
/**
* Sets the users other mailboxes.
*
* @param array $otherMailbox
*
* @return $this
*/
public function setOtherMailbox($otherMailbox = [])
{
return $this->setAttribute($this->schema->otherMailbox(), $otherMailbox);
}
/**
* Returns the distinguished name of the user who is the user's manager.
*
* @return string|null
*/
public function getManager()
{
return $this->getFirstAttribute($this->schema->manager());
}
/**
* Sets the distinguished name of the user who is the user's manager.
*
* @param string $managerDn
*
* @return $this
*/
public function setManager($managerDn)
{
return $this->setFirstAttribute($this->schema->manager(), $managerDn);
}
/**
* Returns the users mail nickname.
*
* @return string|null
*/
public function getMailNickname()
{
return $this->getFirstAttribute($this->schema->emailNickname());
}
}
@@ -0,0 +1,14 @@
<?php
namespace Adldap\Models;
/**
* Class Contact.
*
* Represents an LDAP contact.
*/
class Contact extends Entry
{
use Concerns\HasMemberOf;
use Concerns\HasUserProperties;
}
@@ -0,0 +1,28 @@
<?php
namespace Adldap\Models;
/**
* Class Container.
*
* Represents an LDAP container.
*/
class Container extends Entry
{
use Concerns\HasDescription;
use Concerns\HasCriticalSystemObject;
/**
* Returns the containers system flags integer.
*
* An integer value that contains flags that define additional properties of the class.
*
* @link https://msdn.microsoft.com/en-us/library/ms680022(v=vs.85).aspx
*
* @return string
*/
public function getSystemFlags()
{
return $this->getFirstAttribute($this->schema->systemFlags());
}
}
@@ -0,0 +1,13 @@
<?php
namespace Adldap\Models;
/**
* Class Entry.
*
* Represents an LDAP record that could not be identified as another type of model.
*/
class Entry extends Model
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Created extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Creating extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Deleted extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Deleting extends Event
{
//
}
@@ -0,0 +1,35 @@
<?php
namespace Adldap\Models\Events;
use Adldap\Models\Model;
abstract class Event
{
/**
* The model that the event is being triggered on.
*
* @var Model
*/
protected $model;
/**
* Constructor.
*
* @param Model $model
*/
public function __construct(Model $model)
{
$this->model = $model;
}
/**
* Returns the model that generated the event.
*
* @return Model
*/
public function getModel()
{
return $this->model;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Saved extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Saving extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Updated extends Event
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Models\Events;
class Updating extends Event
{
//
}
@@ -0,0 +1,209 @@
<?php
namespace Adldap\Models;
use Adldap\Query\Builder;
use Adldap\Schemas\ActiveDirectory;
use Adldap\Schemas\SchemaInterface;
/**
* Class Factory.
*
* Creates new LDAP models.
*/
class Factory
{
/**
* The LDAP query builder.
*
* @var Builder
*/
protected $query;
/**
* The LDAP schema.
*
* @var SchemaInterface
*/
protected $schema;
/**
* Constructor.
*
* @param Builder $builder
*/
public function __construct(Builder $builder)
{
$this->setQuery($builder)
->setSchema($builder->getSchema());
}
/**
* Sets the current query builder.
*
* @param Builder $builder
*
* @return $this
*/
public function setQuery(Builder $builder)
{
$this->query = $builder;
return $this;
}
/**
* Sets the current schema.
*
* If null is given, a default ActiveDirectory schema is set.
*
* @param SchemaInterface|null $schema
*
* @return $this
*/
public function setSchema(SchemaInterface $schema = null)
{
$this->schema = $schema ?: new ActiveDirectory();
return $this;
}
/**
* Creates a new generic LDAP entry instance.
*
* @param array $attributes
*
* @return Entry
*/
public function entry(array $attributes = [])
{
$model = $this->schema->entryModel();
return new $model($attributes, $this->query);
}
/**
* Creates a new user instance.
*
* @param array $attributes
*
* @return User
*/
public function user(array $attributes = [])
{
$model = $this->schema->userModel();
return (new $model($attributes, $this->query))
->setAttribute($this->schema->objectClass(), $this->schema->userObjectClasses());
}
/**
* Creates a new organizational unit instance.
*
* @param array $attributes
*
* @return OrganizationalUnit
*/
public function ou(array $attributes = [])
{
$model = $this->schema->organizationalUnitModel();
return (new $model($attributes, $this->query))
->setAttribute($this->schema->objectClass(), [
$this->schema->top(),
$this->schema->organizationalUnit(),
]);
}
/**
* Creates a new organizational unit instance.
*
* @param array $attributes
*
* @return Organization
*/
public function organization(array $attributes = [])
{
$model = $this->schema->organizationModel();
return (new $model($attributes, $this->query))
->setAttribute($this->schema->objectClass(), [
$this->schema->top(),
$this->schema->organization(),
]);
}
/**
* Creates a new group instance.
*
* @param array $attributes
*
* @return Group
*/
public function group(array $attributes = [])
{
$model = $this->schema->groupModel();
return (new $model($attributes, $this->query))
->setAttribute($this->schema->objectClass(), [
$this->schema->top(),
$this->schema->objectCategoryGroup(),
]);
}
/**
* Creates a new organizational unit instance.
*
* @param array $attributes
*
* @return Container
*/
public function container(array $attributes = [])
{
$model = $this->schema->containerModel();
return (new $model($attributes, $this->query))
->setAttribute($this->schema->objectClass(), $this->schema->objectClassContainer());
}
/**
* Creates a new user instance as a contact.
*
* @param array $attributes
*
* @return User
*/
public function contact(array $attributes = [])
{
$model = $this->schema->contactModel();
return (new $model($attributes, $this->query))
->setAttribute($this->schema->objectClass(), [
$this->schema->top(),
$this->schema->person(),
$this->schema->organizationalPerson(),
$this->schema->contact(),
]);
}
/**
* Creates a new computer instance.
*
* @param array $attributes
*
* @return Computer
*/
public function computer(array $attributes = [])
{
$model = $this->schema->computerModel();
return (new $model($attributes, $this->query))
->setAttribute($this->schema->objectClass(), [
$this->schema->top(),
$this->schema->person(),
$this->schema->organizationalPerson(),
$this->schema->user(),
$this->schema->computer(),
]);
}
}
@@ -0,0 +1,13 @@
<?php
namespace Adldap\Models;
/**
* Class ForeignSecurityPrincipal.
*
* Represents an LDAP ForeignSecurityPrincipal.
*/
class ForeignSecurityPrincipal extends Entry
{
use Concerns\HasMemberOf;
}
@@ -0,0 +1,288 @@
<?php
namespace Adldap\Models;
use Adldap\Utilities;
use InvalidArgumentException;
/**
* Class Group.
*
* Represents an LDAP group (security / distribution).
*/
class Group extends Entry
{
use Concerns\HasMemberOf;
use Concerns\HasDescription;
/**
* Returns all users apart of the current group.
*
* @link https://msdn.microsoft.com/en-us/library/ms677097(v=vs.85).aspx
*
* @return \Adldap\Query\Collection
*/
public function getMembers()
{
$members = $this->getMembersFromAttribute($this->schema->member());
if (count($members) === 0) {
$members = $this->getPaginatedMembers();
}
return $this->newCollection($members);
}
/**
* Returns the group's member names only.
*
* @return array
*/
public function getMemberNames()
{
$members = [];
$dns = $this->getAttribute($this->schema->member()) ?: [];
foreach ($dns as $dn) {
$exploded = Utilities::explodeDn($dn);
if (array_key_exists(0, $exploded)) {
$members[] = $exploded[0];
}
}
return $members;
}
/**
* Sets the groups members using an array of user DNs.
*
* @param array $entries
*
* @return $this
*/
public function setMembers(array $entries)
{
return $this->setAttribute($this->schema->member(), $entries);
}
/**
* Adds multiple entries to the current group.
*
* @param array $members
*
* @return bool
*/
public function addMembers(array $members)
{
$members = array_map(function ($member) {
return $member instanceof Model
? $member->getDn()
: $member;
}, $members);
$mod = $this->newBatchModification(
$this->schema->member(),
LDAP_MODIFY_BATCH_ADD,
$members
);
return $this->addModification($mod)->save();
}
/**
* Adds an entry to the current group.
*
* @param string|Entry $member
*
* @throws InvalidArgumentException When the given entry is empty or contains no distinguished name.
*
* @return bool
*/
public function addMember($member)
{
$member = ($member instanceof Model ? $member->getDn() : $member);
if (is_null($member)) {
throw new InvalidArgumentException(
'Cannot add member to group. The members distinguished name cannot be null.'
);
}
$mod = $this->newBatchModification(
$this->schema->member(),
LDAP_MODIFY_BATCH_ADD,
[$member]
);
return $this->addModification($mod)->save();
}
/**
* Removes an entry from the current group.
*
* @param string|Entry $member
*
* @throws InvalidArgumentException
*
* @return bool
*/
public function removeMember($member)
{
$member = ($member instanceof Model ? $member->getDn() : $member);
if (is_null($member)) {
throw new InvalidArgumentException(
'Cannot remove member to group. The members distinguished name cannot be null.'
);
}
$mod = $this->newBatchModification(
$this->schema->member(),
LDAP_MODIFY_BATCH_REMOVE,
[$member]
);
return $this->addModification($mod)->save();
}
/**
* Removes all members from the current group.
*
* @return bool
*/
public function removeMembers()
{
$mod = $this->newBatchModification(
$this->schema->member(),
LDAP_MODIFY_BATCH_REMOVE_ALL
);
return $this->addModification($mod)->save();
}
/**
* Returns the group type integer.
*
* @link https://msdn.microsoft.com/en-us/library/ms675935(v=vs.85).aspx
*
* @return string
*/
public function getGroupType()
{
return $this->getFirstAttribute($this->schema->groupType());
}
/**
* Retrieves group members by the specified model attribute.
*
* @param $attribute
*
* @return array
*/
protected function getMembersFromAttribute($attribute)
{
$members = [];
$entries = $this->getAttribute($attribute) ?: [];
$query = $this->query->newInstance();
// Retrieving the member identifier to allow
// compatibility with LDAP variants.
$identifier = $this->schema->memberIdentifier();
foreach ($entries as $entry) {
// If our identifier is a distinguished name, then we need to
// use an alternate query method, as we can't locate records
// by distinguished names using an LDAP filter.
if ($identifier == 'dn' || $identifier == 'distinguishedname') {
$member = $query->findByDn($entry);
} else {
// We'll ensure we clear our filters when retrieving each member,
// so we can continue fetching the next one in line.
$member = $query->clearFilters()->findBy($identifier, $entry);
}
// We'll double check that we've received a model from
// our query before adding it into our results.
if ($member instanceof Model) {
$members[] = $member;
}
}
return $members;
}
/**
* Retrieves members that are contained in a member range.
*
* @return array
*/
protected function getPaginatedMembers()
{
$members = [];
$keys = array_keys($this->attributes);
// We need to filter out the model attributes so
// we only retrieve the member range.
$attributes = array_values(array_filter($keys, function ($key) {
return strpos($key, 'member;range') !== false;
}));
// We'll grab the member range key so we can run a
// regex on it to determine the range.
$key = reset($attributes);
preg_match_all(
'/member;range\=([0-9]{1,4})-([0-9*]{1,4})/',
$key,
$matches
);
if ($key && count($matches) == 3) {
// Retrieve the ending range number.
$to = $matches[2][0];
// Retrieve the current groups members from the
// current range string (ex. 'member;0-50').
$members = $this->getMembersFromAttribute($key);
// If the query already included all member results (indicated
// by the '*'), then we can return here. Otherwise we need
// to continue on and retrieve the rest.
if ($to === '*') {
return $members;
}
// Determine the amount of members we're requesting per query.
$range = $to - $matches[1][0];
// Set our starting range to our last end range plus one.
$from = $to + 1;
// We'll determine the new end range by adding the
// total range to our new starting range.
$to = $from + $range;
// We'll need to query for the current model again but with
// a new range to retrieve the other members.
/** @var Group $group */
$group = $this->query->newInstance()->findByDn(
$this->getDn(),
[$this->query->getSchema()->memberRange($from, $to)]
);
// Finally, we'll merge our current members
// with the newly returned members.
$members = array_merge(
$members,
$group->getMembers()->toArray()
);
}
return $members;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
<?php
namespace Adldap\Models;
use Adldap\AdldapException;
/**
* Class ModelDoesNotExistException.
*
* Thrown when a model being saved / updated does not actually exist.
*/
class ModelDoesNotExistException extends AdldapException
{
/**
* The class name of the model that does not exist.
*
* @var string
*/
protected $model;
/**
* Sets the model that does not exist.
*
* @param string $model
*
* @return ModelDoesNotExistException
*/
public function setModel($model)
{
$this->model = $model;
$this->message = "Model [{$model}] does not exist.";
return $this;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Adldap\Models;
use Adldap\AdldapException;
/**
* Class ModelNotFoundException.
*
* Thrown when an LDAP record is not found.
*/
class ModelNotFoundException extends AdldapException
{
/**
* The query filter that was used.
*
* @var string
*/
protected $query;
/**
* The base DN of the query that was used.
*
* @var string
*/
protected $baseDn;
/**
* Sets the query that was used.
*
* @param string $query
* @param string $baseDn
*
* @return ModelNotFoundException
*/
public function setQuery($query, $baseDn)
{
$this->query = $query;
$this->baseDn = $baseDn;
$this->message = "No LDAP query results for filter: [{$query}] in: [{$baseDn}]";
return $this;
}
}
@@ -0,0 +1,31 @@
<?php
namespace Adldap\Models;
/**
* Class Organization.
*
* Represents an LDAP organization.
*/
class Organization extends Entry
{
use Concerns\HasDescription;
/**
* Retrieves the organization units OU attribute.
*
* @return string
*/
public function getOrganization()
{
return $this->getFirstAttribute($this->schema->organizationName());
}
/**
* {@inheritdoc}
*/
protected function getCreatableDn()
{
return $this->getDnBuilder()->addO($this->getOrganization());
}
}
@@ -0,0 +1,31 @@
<?php
namespace Adldap\Models;
/**
* Class OrganizationalUnit.
*
* Represents an LDAP organizational unit.
*/
class OrganizationalUnit extends Entry
{
use Concerns\HasDescription;
/**
* Retrieves the organization units OU attribute.
*
* @return string
*/
public function getOu()
{
return $this->getFirstAttribute($this->schema->organizationalUnitShort());
}
/**
* {@inheritdoc}
*/
protected function getCreatableDn()
{
return $this->getDnBuilder()->addOU($this->getOu());
}
}
@@ -0,0 +1,286 @@
<?php
namespace Adldap\Models;
/**
* Class Printer.
*
* Represents an LDAP printer.
*/
class Printer extends Entry
{
/**
* Returns the printers name.
*
* @link https://msdn.microsoft.com/en-us/library/ms679385(v=vs.85).aspx
*
* @return string
*/
public function getPrinterName()
{
return $this->getFirstAttribute($this->schema->printerName());
}
/**
* Returns the printers share name.
*
* @link https://msdn.microsoft.com/en-us/library/ms679408(v=vs.85).aspx
*
* @return string
*/
public function getPrinterShareName()
{
return $this->getFirstAttribute($this->schema->printerShareName());
}
/**
* Returns the printers memory.
*
* @link https://msdn.microsoft.com/en-us/library/ms679396(v=vs.85).aspx
*
* @return string
*/
public function getMemory()
{
return $this->getFirstAttribute($this->schema->printerMemory());
}
/**
* Returns the printers URL.
*
* @return string
*/
public function getUrl()
{
return $this->getFirstAttribute($this->schema->url());
}
/**
* Returns the printers location.
*
* @link https://msdn.microsoft.com/en-us/library/ms676839(v=vs.85).aspx
*
* @return string
*/
public function getLocation()
{
return $this->getFirstAttribute($this->schema->location());
}
/**
* Returns the server name that the
* current printer is connected to.
*
* @link https://msdn.microsoft.com/en-us/library/ms679772(v=vs.85).aspx
*
* @return string
*/
public function getServerName()
{
return $this->getFirstAttribute($this->schema->serverName());
}
/**
* Returns true / false if the printer can print in color.
*
* @link https://msdn.microsoft.com/en-us/library/ms679382(v=vs.85).aspx
*
* @return null|bool
*/
public function getColorSupported()
{
return $this->convertStringToBool(
$this->getFirstAttribute(
$this->schema->printerColorSupported()
)
);
}
/**
* Returns true / false if the printer supports duplex printing.
*
* @link https://msdn.microsoft.com/en-us/library/ms679383(v=vs.85).aspx
*
* @return null|bool
*/
public function getDuplexSupported()
{
return $this->convertStringToBool(
$this->getFirstAttribute(
$this->schema->printerDuplexSupported()
)
);
}
/**
* Returns an array of printer paper types that the printer supports.
*
* @link https://msdn.microsoft.com/en-us/library/ms679395(v=vs.85).aspx
*
* @return array
*/
public function getMediaSupported()
{
return $this->getAttribute($this->schema->printerMediaSupported());
}
/**
* Returns true / false if the printer supports stapling.
*
* @link https://msdn.microsoft.com/en-us/library/ms679410(v=vs.85).aspx
*
* @return null|bool
*/
public function getStaplingSupported()
{
return $this->convertStringToBool(
$this->getFirstAttribute(
$this->schema->printerStaplingSupported()
)
);
}
/**
* Returns an array of the printers bin names.
*
* @link https://msdn.microsoft.com/en-us/library/ms679380(v=vs.85).aspx
*
* @return array
*/
public function getPrintBinNames()
{
return $this->getAttribute($this->schema->printerBinNames());
}
/**
* Returns the printers maximum resolution.
*
* @link https://msdn.microsoft.com/en-us/library/ms679391(v=vs.85).aspx
*
* @return string
*/
public function getPrintMaxResolution()
{
return $this->getFirstAttribute($this->schema->printerMaxResolutionSupported());
}
/**
* Returns the printers orientations supported.
*
* @link https://msdn.microsoft.com/en-us/library/ms679402(v=vs.85).aspx
*
* @return string
*/
public function getPrintOrientations()
{
return $this->getFirstAttribute($this->schema->printerOrientationSupported());
}
/**
* Returns the driver name of the printer.
*
* @link https://msdn.microsoft.com/en-us/library/ms675652(v=vs.85).aspx
*
* @return string
*/
public function getDriverName()
{
return $this->getFirstAttribute($this->schema->driverName());
}
/**
* Returns the printer drivers version number.
*
* @link https://msdn.microsoft.com/en-us/library/ms675653(v=vs.85).aspx
*
* @return string
*/
public function getDriverVersion()
{
return $this->getFirstAttribute($this->schema->driverVersion());
}
/**
* Returns the priority number of the printer.
*
* @link https://msdn.microsoft.com/en-us/library/ms679413(v=vs.85).aspx
*
* @return string
*/
public function getPriority()
{
return $this->getFirstAttribute($this->schema->priority());
}
/**
* Returns the printers start time.
*
* @link https://msdn.microsoft.com/en-us/library/ms679411(v=vs.85).aspx
*
* @return string
*/
public function getPrintStartTime()
{
return $this->getFirstAttribute($this->schema->printerStartTime());
}
/**
* Returns the printers end time.
*
* @link https://msdn.microsoft.com/en-us/library/ms679384(v=vs.85).aspx
*
* @return string
*/
public function getPrintEndTime()
{
return $this->getFirstAttribute($this->schema->printerEndTime());
}
/**
* Returns the port name of printer.
*
* @link https://msdn.microsoft.com/en-us/library/ms679131(v=vs.85).aspx
*
* @return string
*/
public function getPortName()
{
return $this->getFirstAttribute($this->schema->portName());
}
/**
* Returns the printers version number.
*
* @link https://msdn.microsoft.com/en-us/library/ms680897(v=vs.85).aspx
*
* @return string
*/
public function getVersionNumber()
{
return $this->getFirstAttribute($this->schema->versionNumber());
}
/**
* Returns the print rate.
*
* @link https://msdn.microsoft.com/en-us/library/ms679405(v=vs.85).aspx
*
* @return string
*/
public function getPrintRate()
{
return $this->getFirstAttribute($this->schema->printerPrintRate());
}
/**
* Returns the print rate unit.
*
* @link https://msdn.microsoft.com/en-us/library/ms679406(v=vs.85).aspx
*
* @return string
*/
public function getPrintRateUnit()
{
return $this->getFirstAttribute($this->schema->printerPrintRateUnit());
}
}
@@ -0,0 +1,85 @@
<?php
namespace Adldap\Models;
use DateTime;
/**
* Class RootDse.
*
* Represents the LDAP connections Root DSE record.
*/
class RootDse extends Model
{
/**
* Returns the hosts current time in unix timestamp format.
*
* @return int
*/
public function getCurrentTime()
{
$time = $this->getFirstAttribute($this->schema->currentTime());
return DateTime::createFromFormat($this->timestampFormat, $time)->getTimestamp();
}
/**
* Returns the hosts current time in the models date format.
*
* @return string
*/
public function getCurrentTimeDate()
{
return (new DateTime())->setTimestamp($this->getCurrentTime())->format($this->dateFormat);
}
/**
* Returns the hosts configuration naming context.
*
* @return string
*/
public function getConfigurationNamingContext()
{
return $this->getFirstAttribute($this->schema->configurationNamingContext());
}
/**
* Returns the hosts schema naming context.
*
* @return string
*/
public function getSchemaNamingContext()
{
return $this->getFirstAttribute($this->schema->schemaNamingContext());
}
/**
* Returns the hosts DNS name.
*
* @return string
*/
public function getDnsHostName()
{
return $this->getFirstAttribute($this->schema->dnsHostName());
}
/**
* Returns the current hosts server name.
*
* @return string
*/
public function getServerName()
{
return $this->getFirstAttribute($this->schema->serverName());
}
/**
* Returns the DN of the root domain NC for this DC's forest.
*
* @return mixed
*/
public function getRootDomainNamingContext()
{
return $this->getFirstAttribute($this->schema->rootDomainNamingContext());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
<?php
namespace Adldap\Models;
use Adldap\AdldapException;
/**
* Class UserPasswordIncorrectException.
*
* Thrown when a users password is being changed
* and their current password given is incorrect.
*/
class UserPasswordIncorrectException extends AdldapException
{
//
}
@@ -0,0 +1,16 @@
<?php
namespace Adldap\Models;
use Adldap\AdldapException;
/**
* Class UserPasswordPolicyException.
*
* Thrown when a users password is being changed but their new password
* does not conform to the LDAP servers password policy.
*/
class UserPasswordPolicyException extends AdldapException
{
//
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
<?php
namespace Adldap\Query;
use Closure;
use Psr\SimpleCache\CacheInterface;
class Cache
{
/**
* The cache driver.
*
* @var CacheInterface
*/
protected $store;
/**
* Constructor.
*
* @param CacheInterface $store
*/
public function __construct(CacheInterface $store)
{
$this->store = $store;
}
/**
* Get an item from the cache.
*
* @param string $key
*
* @throws \Psr\SimpleCache\InvalidArgumentException
*
* @return mixed
*/
public function get($key)
{
return $this->store->get($key);
}
/**
* Store an item in the cache.
*
* @param string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
*
* @throws \Psr\SimpleCache\InvalidArgumentException
*
* @return bool
*/
public function put($key, $value, $ttl = null)
{
return $this->store->set($key, $value, $ttl);
}
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @param string $key
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @param Closure $callback
*
* @throws \Psr\SimpleCache\InvalidArgumentException
*
* @return mixed
*/
public function remember($key, $ttl, Closure $callback)
{
$value = $this->get($key);
if (!is_null($value)) {
return $value;
}
$this->put($key, $value = $callback(), $ttl);
return $value;
}
/**
* Delete an item from the cache.
*
* @param string $key
*
* @throws \Psr\Cache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException
*
* @return bool
*/
public function delete($key)
{
return $this->store->delete($key);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Adldap\Query;
use Adldap\Models\Model;
use Illuminate\Support\Collection as BaseCollection;
class Collection extends BaseCollection
{
/**
* {@inheritdoc}
*/
protected function valueRetriever($value)
{
if ($this->useAsCallable($value)) {
return $value;
}
return function ($item) use ($value) {
if ($item instanceof Model) {
return $item->getFirstAttribute($value);
}
return data_get($item, $value);
};
}
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Query\Events;
class Listing extends QueryExecuted
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Query\Events;
class Paginate extends QueryExecuted
{
//
}
@@ -0,0 +1,54 @@
<?php
namespace Adldap\Query\Events;
use Adldap\Query\Builder;
class QueryExecuted
{
/**
* The LDAP filter that was used for the query.
*
* @var string
*/
protected $query;
/**
* The number of milliseconds it took to execute the query.
*
* @var float
*/
protected $time;
/**
* Constructor.
*
* @param Builder $query
* @param null|float $time
*/
public function __construct(Builder $query, $time = null)
{
$this->query = $query;
$this->time = $time;
}
/**
* Returns the LDAP filter that was used for the query.
*
* @return Builder
*/
public function getQuery()
{
return $this->query;
}
/**
* Returns the number of milliseconds it took to execute the query.
*
* @return float|null
*/
public function getTime()
{
return $this->time;
}
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Query\Events;
class Read extends QueryExecuted
{
//
}
@@ -0,0 +1,8 @@
<?php
namespace Adldap\Query\Events;
class Search extends QueryExecuted
{
//
}
@@ -0,0 +1,297 @@
<?php
namespace Adldap\Query;
use Adldap\Models\RootDse;
use Adldap\Schemas\ActiveDirectory;
use Adldap\Schemas\SchemaInterface;
use Adldap\Connections\ConnectionInterface;
/**
* Adldap2 Search Factory.
*
* Constructs new LDAP queries.
*
*
* @mixin Builder
*/
class Factory
{
/**
* @var ConnectionInterface
*/
protected $connection;
/**
* Stores the current schema instance.
*
* @var SchemaInterface
*/
protected $schema;
/**
* The base DN to use for the search.
*
* @var string|null
*/
protected $base;
/**
* The query cache.
*
* @var Cache
*/
protected $cache;
/**
* Constructor.
*
* @param ConnectionInterface $connection The connection to use when constructing a new query.
* @param SchemaInterface|null $schema The schema to use for the query and models located.
* @param string $baseDn The base DN to use for all searches.
*/
public function __construct(ConnectionInterface $connection, SchemaInterface $schema = null, $baseDn = '')
{
$this->setConnection($connection)
->setSchema($schema)
->setBaseDn($baseDn);
}
/**
* Sets the connection property.
*
* @param ConnectionInterface $connection
*
* @return $this
*/
public function setConnection(ConnectionInterface $connection)
{
$this->connection = $connection;
return $this;
}
/**
* Sets the schema property.
*
* @param SchemaInterface|null $schema
*
* @return $this
*/
public function setSchema(SchemaInterface $schema = null)
{
$this->schema = $schema ?: new ActiveDirectory();
return $this;
}
/**
* Sets the base distinguished name to perform searches upon.
*
* @param string $base
*
* @return $this
*/
public function setBaseDn($base = '')
{
$this->base = $base;
return $this;
}
/**
* Sets the cache for storing query results.
*
* @param Cache $cache
*
* @return $this
*/
public function setCache(Cache $cache)
{
$this->cache = $cache;
return $this;
}
/**
* Returns a new query builder instance.
*
* @return Builder
*/
public function newQuery()
{
return $this->newBuilder()->in($this->base);
}
/**
* Performs a global 'all' search query on the current
* connection by performing a search for all entries
* that contain a common name attribute.
*
* @return \Adldap\Query\Collection|array
*/
public function get()
{
return $this->newQuery()->whereHas($this->schema->commonName())->get();
}
/**
* Returns a query builder scoped to users.
*
* @return Builder
*/
public function users()
{
$wheres = [
[$this->schema->objectClass(), Operator::$equals, $this->schema->objectClassUser()],
[$this->schema->objectCategory(), Operator::$equals, $this->schema->objectCategoryPerson()],
];
// OpenLDAP doesn't like specifying the omission of user objectclasses
// equal to `contact`. We'll make sure we're working with
// ActiveDirectory before adding this filter.
if (is_a($this->schema, ActiveDirectory::class)) {
$wheres[] = [$this->schema->objectClass(), Operator::$doesNotEqual, $this->schema->objectClassContact()];
}
return $this->where($wheres);
}
/**
* Returns a query builder scoped to printers.
*
* @return Builder
*/
public function printers()
{
return $this->where([
$this->schema->objectClass() => $this->schema->objectClassPrinter(),
]);
}
/**
* Returns a query builder scoped to organizational units.
*
* @return Builder
*/
public function ous()
{
return $this->where([
$this->schema->objectClass() => $this->schema->objectClassOu(),
]);
}
/**
* Returns a query builder scoped to organizations.
*
* @return Builder
*/
public function organizations()
{
return $this->where([
$this->schema->objectClass() => $this->schema->objectClassOrganization(),
]);
}
/**
* Returns a query builder scoped to groups.
*
* @return Builder
*/
public function groups()
{
return $this->where([
$this->schema->objectClass() => $this->schema->objectClassGroup(),
]);
}
/**
* Returns a query builder scoped to containers.
*
* @return Builder
*/
public function containers()
{
return $this->where([
$this->schema->objectClass() => $this->schema->objectClassContainer(),
]);
}
/**
* Returns a query builder scoped to contacts.
*
* @return Builder
*/
public function contacts()
{
return $this->where([
$this->schema->objectClass() => $this->schema->objectClassContact(),
]);
}
/**
* Returns a query builder scoped to computers.
*
* @return Builder
*/
public function computers()
{
return $this->where([
$this->schema->objectClass() => $this->schema->objectClassComputer(),
]);
}
/**
* Returns the root DSE record.
*
* @return RootDse|null
*/
public function getRootDse()
{
$query = $this->newQuery();
$root = $query->in('')->read()->whereHas($this->schema->objectClass())->first();
if ($root) {
return (new RootDse([], $query))
->setRawAttributes($root->getAttributes());
}
}
/**
* Handle dynamic method calls on the query builder object.
*
* @param string $method
* @param array $parameters
*
* @return mixed
*/
public function __call($method, $parameters)
{
return call_user_func_array([$this->newQuery(), $method], $parameters);
}
/**
* Returns a new query grammar instance.
*
* @return Grammar
*/
protected function newGrammar()
{
return new Grammar();
}
/**
* Returns a new query builder instance.
*
* @return Builder
*/
protected function newBuilder()
{
$builder = new Builder($this->connection, $this->newGrammar(), $this->schema);
$builder->setCache($this->cache);
return $builder;
}
}
@@ -0,0 +1,390 @@
<?php
namespace Adldap\Query;
class Grammar
{
/**
* Wraps a query string in brackets.
*
* Produces: (query)
*
* @param string $query
* @param string $prefix
* @param string $suffix
*
* @return string
*/
public function wrap($query, $prefix = '(', $suffix = ')')
{
return $prefix.$query.$suffix;
}
/**
* Compiles the Builder instance into an LDAP query string.
*
* @param Builder $builder
*
* @return string
*/
public function compile(Builder $builder)
{
$ands = $builder->filters['and'];
$ors = $builder->filters['or'];
$raws = $builder->filters['raw'];
$query = $this->concatenate($raws);
$query = $this->compileWheres($ands, $query);
$query = $this->compileOrWheres($ors, $query);
// We need to check if the query is already nested, otherwise
// we'll nest it here and return the result.
if (!$builder->isNested()) {
$total = count($ands) + count($raws);
// Make sure we wrap the query in an 'and' if using
// multiple filters. We also need to check if only
// one where is used with multiple orWheres, that
// we wrap it in an `and` query.
if ($total > 1 || (count($ands) === 1 && count($ors) > 0)) {
$query = $this->compileAnd($query);
}
}
return $query;
}
/**
* Concatenates filters into a single string.
*
* @param array $bindings
*
* @return string
*/
public function concatenate(array $bindings = [])
{
// Filter out empty query segments.
$bindings = array_filter($bindings, function ($value) {
return (string) $value !== '';
});
return implode('', $bindings);
}
/**
* Returns a query string for equals.
*
* Produces: (field=value)
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileEquals($field, $value)
{
return $this->wrap($field.Operator::$equals.$value);
}
/**
* Returns a query string for does not equal.
*
* Produces: (!(field=value))
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileDoesNotEqual($field, $value)
{
return $this->compileNot($this->compileEquals($field, $value));
}
/**
* Alias for does not equal operator (!=) operator.
*
* Produces: (!(field=value))
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileDoesNotEqualAlias($field, $value)
{
return $this->compileDoesNotEqual($field, $value);
}
/**
* Returns a query string for greater than or equals.
*
* Produces: (field>=value)
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileGreaterThanOrEquals($field, $value)
{
return $this->wrap($field.Operator::$greaterThanOrEquals.$value);
}
/**
* Returns a query string for less than or equals.
*
* Produces: (field<=value)
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileLessThanOrEquals($field, $value)
{
return $this->wrap($field.Operator::$lessThanOrEquals.$value);
}
/**
* Returns a query string for approximately equals.
*
* Produces: (field~=value)
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileApproximatelyEquals($field, $value)
{
return $this->wrap($field.Operator::$approximatelyEquals.$value);
}
/**
* Returns a query string for starts with.
*
* Produces: (field=value*)
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileStartsWith($field, $value)
{
return $this->wrap($field.Operator::$equals.$value.Operator::$has);
}
/**
* Returns a query string for does not start with.
*
* Produces: (!(field=*value))
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileNotStartsWith($field, $value)
{
return $this->compileNot($this->compileStartsWith($field, $value));
}
/**
* Returns a query string for ends with.
*
* Produces: (field=*value)
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileEndsWith($field, $value)
{
return $this->wrap($field.Operator::$equals.Operator::$has.$value);
}
/**
* Returns a query string for does not end with.
*
* Produces: (!(field=value*))
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileNotEndsWith($field, $value)
{
return $this->compileNot($this->compileEndsWith($field, $value));
}
/**
* Returns a query string for contains.
*
* Produces: (field=*value*)
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileContains($field, $value)
{
return $this->wrap($field.Operator::$equals.Operator::$has.$value.Operator::$has);
}
/**
* Returns a query string for does not contain.
*
* Produces: (!(field=*value*))
*
* @param string $field
* @param string $value
*
* @return string
*/
public function compileNotContains($field, $value)
{
return $this->compileNot($this->compileContains($field, $value));
}
/**
* Returns a query string for a where has.
*
* Produces: (field=*)
*
* @param string $field
*
* @return string
*/
public function compileHas($field)
{
return $this->wrap($field.Operator::$equals.Operator::$has);
}
/**
* Returns a query string for a where does not have.
*
* Produces: (!(field=*))
*
* @param string $field
*
* @return string
*/
public function compileNotHas($field)
{
return $this->compileNot($this->compileHas($field));
}
/**
* Wraps the inserted query inside an AND operator.
*
* Produces: (&query)
*
* @param string $query
*
* @return string
*/
public function compileAnd($query)
{
return $query ? $this->wrap($query, '(&') : '';
}
/**
* Wraps the inserted query inside an OR operator.
*
* Produces: (|query)
*
* @param string $query
*
* @return string
*/
public function compileOr($query)
{
return $query ? $this->wrap($query, '(|') : '';
}
/**
* Wraps the inserted query inside an NOT operator.
*
* @param string $query
*
* @return string
*/
public function compileNot($query)
{
return $query ? $this->wrap($query, '(!') : '';
}
/**
* Assembles all where clauses in the current wheres property.
*
* @param array $wheres
* @param string $query
*
* @return string
*/
protected function compileWheres(array $wheres = [], $query = '')
{
foreach ($wheres as $where) {
$query .= $this->compileWhere($where);
}
return $query;
}
/**
* Assembles all or where clauses in the current orWheres property.
*
* @param array $orWheres
* @param string $query
*
* @return string
*/
protected function compileOrWheres(array $orWheres = [], $query = '')
{
$or = '';
foreach ($orWheres as $where) {
$or .= $this->compileWhere($where);
}
// Make sure we wrap the query in an 'or' if using multiple
// orWheres. For example (|(QUERY)(ORWHEREQUERY)).
if (($query && count($orWheres) > 0) || count($orWheres) > 1) {
$query .= $this->compileOr($or);
} else {
$query .= $or;
}
return $query;
}
/**
* Assembles a single where query based
* on its operator and returns it.
*
* @param array $where
*
* @return string|null
*/
protected function compileWhere(array $where)
{
// Get the name of the operator.
if ($name = array_search($where['operator'], Operator::all())) {
// If the name was found we'll camel case it
// to run it through the compile method.
$method = 'compile'.ucfirst($name);
// Make sure the compile method exists for the operator.
if (method_exists($this, $method)) {
return $this->{$method}($where['field'], $where['value']);
}
}
}
}
@@ -0,0 +1,116 @@
<?php
namespace Adldap\Query;
use ReflectionClass;
class Operator
{
/**
* The 'has' wildcard operator.
*
* @var string
*/
public static $has = '*';
/**
* The custom `notHas` operator.
*
* @var string
*/
public static $notHas = '!*';
/**
* The equals operator.
*
* @var string
*/
public static $equals = '=';
/**
* The does not equal operator.
*
* @var string
*/
public static $doesNotEqual = '!';
/**
* The does not equal operator (alias).
*
* @var string
*/
public static $doesNotEqualAlias = '!=';
/**
* The greater than or equal to operator.
*
* @var string
*/
public static $greaterThanOrEquals = '>=';
/**
* The less than or equal to operator.
*
* @var string
*/
public static $lessThanOrEquals = '<=';
/**
* The approximately equal to operator.
*
* @var string
*/
public static $approximatelyEquals = '~=';
/**
* The custom starts with operator.
*
* @var string
*/
public static $startsWith = 'starts_with';
/**
* The custom not starts with operator.
*
* @var string
*/
public static $notStartsWith = 'not_starts_with';
/**
* The custom ends with operator.
*
* @var string
*/
public static $endsWith = 'ends_with';
/**
* The custom not ends with operator.
*
* @var string
*/
public static $notEndsWith = 'not_ends_with';
/**
* The custom contains operator.
*
* @var string
*/
public static $contains = 'contains';
/**
* The custom not contains operator.
*
* @var string
*/
public static $notContains = 'not_contains';
/**
* Returns all available operators.
*
* @return array
*/
public static function all()
{
return (new ReflectionClass(new static()))->getStaticProperties();
}
}
@@ -0,0 +1,206 @@
<?php
namespace Adldap\Query;
use Countable;
use ArrayIterator;
use IteratorAggregate;
class Paginator implements Countable, IteratorAggregate
{
/**
* The complete results array.
*
* @var array
*/
protected $results = [];
/**
* The total amount of pages.
*
* @var int
*/
protected $pages;
/**
* The amount of entries per page.
*
* @var int
*/
protected $perPage;
/**
* The current page number.
*
* @var int
*/
protected $currentPage;
/**
* The current entry offset number.
*
* @var int
*/
protected $currentOffset;
/**
* Constructor.
*
* @param array $results
* @param int $perPage
* @param int $currentPage
* @param int $pages
*/
public function __construct(array $results = [], $perPage = 50, $currentPage = 0, $pages = 0)
{
$this->setResults($results)
->setPerPage($perPage)
->setCurrentPage($currentPage)
->setPages($pages)
->setCurrentOffset(($this->getCurrentPage() * $this->getPerPage()));
}
/**
* Get an iterator for the entries.
*
* @return ArrayIterator
*/
public function getIterator()
{
$entries = array_slice($this->getResults(), $this->getCurrentOffset(), $this->getPerPage(), true);
return new ArrayIterator($entries);
}
/**
* Returns the complete results array.
*
* @return array
*/
public function getResults()
{
return $this->results;
}
/**
* Returns the total amount of pages
* in a paginated result.
*
* @return int
*/
public function getPages()
{
return $this->pages;
}
/**
* Returns the total amount of entries
* allowed per page.
*
* @return int
*/
public function getPerPage()
{
return $this->perPage;
}
/**
* Returns the current page number.
*
* @return int
*/
public function getCurrentPage()
{
return $this->currentPage;
}
/**
* Returns the current offset number.
*
* @return int
*/
public function getCurrentOffset()
{
return $this->currentOffset;
}
/**
* Returns the total amount of results.
*
* @return int
*/
public function count()
{
return count($this->results);
}
/**
* Sets the results array property.
*
* @param array $results
*
* @return Paginator
*/
protected function setResults(array $results)
{
$this->results = $results;
return $this;
}
/**
* Sets the total number of pages.
*
* @param int $pages
*
* @return Paginator
*/
protected function setPages($pages = 0)
{
$this->pages = (int) $pages;
return $this;
}
/**
* Sets the number of entries per page.
*
* @param int $perPage
*
* @return Paginator
*/
protected function setPerPage($perPage = 50)
{
$this->perPage = (int) $perPage;
return $this;
}
/**
* Sets the current page number.
*
* @param int $currentPage
*
* @return Paginator
*/
protected function setCurrentPage($currentPage = 0)
{
$this->currentPage = (int) $currentPage;
return $this;
}
/**
* Sets the current offset number.
*
* @param int $offset
*
* @return Paginator
*/
protected function setCurrentOffset($offset = 0)
{
$this->currentOffset = (int) $offset;
return $this;
}
}
@@ -0,0 +1,211 @@
<?php
namespace Adldap\Query;
use Adldap\Models\Entry;
use Adldap\Models\Model;
use InvalidArgumentException;
use Adldap\Schemas\SchemaInterface;
use Adldap\Connections\ConnectionInterface;
class Processor
{
/**
* @var Builder
*/
protected $builder;
/**
* @var ConnectionInterface
*/
protected $connection;
/**
* @var SchemaInterface
*/
protected $schema;
/**
* Constructor.
*
* @param Builder $builder
*/
public function __construct(Builder $builder)
{
$this->builder = $builder;
$this->schema = $builder->getSchema();
$this->connection = $builder->getConnection();
}
/**
* Processes LDAP search results and constructs their model instances.
*
* @param array $entries The LDAP entries to process.
*
* @return Collection|array
*/
public function process($entries)
{
if ($this->builder->isRaw()) {
// If the builder is asking for a raw
// LDAP result, we can return here.
return $entries;
}
$models = [];
if (array_key_exists('count', $entries)) {
for ($i = 0; $i < $entries['count']; $i++) {
// We'll go through each entry and construct a new
// model instance with the raw LDAP attributes.
$models[] = $this->newLdapEntry($entries[$i]);
}
}
// If the query contains paginated results, we'll return them here.
if ($this->builder->isPaginated()) {
return $models;
}
// If the query is requested to be sorted, we'll perform
// that here and return the resulting collection.
if ($this->builder->isSorted()) {
return $this->processSort($models);
}
// Otherwise, we'll return a regular unsorted collection.
return $this->newCollection($models);
}
/**
* Processes paginated LDAP results.
*
* @param array $pages
* @param int $perPage
* @param int $currentPage
*
* @return Paginator
*/
public function processPaginated(array $pages = [], $perPage = 50, $currentPage = 0)
{
$models = [];
foreach ($pages as $entries) {
// Go through each page and process the results into an objects array.
$models = array_merge($models, $this->process($entries));
}
$models = $this->processSort($models)->toArray();
return $this->newPaginator($models, $perPage, $currentPage, count($pages));
}
/**
* Returns a new LDAP Entry instance.
*
* @param array $attributes
*
* @return Entry
*/
public function newLdapEntry(array $attributes = [])
{
$objectClass = $this->schema->objectClass();
// We need to ensure the record contains an object class to be able to
// determine its type. Otherwise, we create a default Entry model.
if (array_key_exists($objectClass, $attributes) && array_key_exists(0, $attributes[$objectClass])) {
// Retrieve all of the object classes from the LDAP
// entry and lowercase them for comparisons.
$classes = array_map('strtolower', $attributes[$objectClass]);
// Retrieve the model mapping.
$models = $this->schema->objectClassModelMap();
// Retrieve the object class mappings (with strtolower keys).
$mappings = array_map('strtolower', array_keys($models));
// Retrieve the model from the map using the entry's object class.
$map = array_intersect($mappings, $classes);
if (count($map) > 0) {
// Retrieve the model using the object class.
$model = $models[current($map)];
// Construct and return a new model.
return $this->newModel([], $model)
->setRawAttributes($attributes);
}
}
// A default entry model if the object class isn't found.
return $this->newModel()->setRawAttributes($attributes);
}
/**
* Creates a new model instance.
*
* @param array $attributes
* @param string|null $model
*
* @throws InvalidArgumentException
*
* @return mixed|Entry
*/
public function newModel($attributes = [], $model = null)
{
$model = (class_exists($model) ? $model : $this->schema->entryModel());
if (!is_subclass_of($model, $base = Model::class)) {
throw new InvalidArgumentException("The given model class '{$model}' must extend the base model class '{$base}'");
}
return new $model($attributes, $this->builder->newInstance());
}
/**
* Returns a new Paginator object instance.
*
* @param array $models
* @param int $perPage
* @param int $currentPage
* @param int $pages
*
* @return Paginator
*/
public function newPaginator(array $models = [], $perPage = 25, $currentPage = 0, $pages = 1)
{
return new Paginator($models, $perPage, $currentPage, $pages);
}
/**
* Returns a new collection instance.
*
* @param array $items
*
* @return Collection
*/
public function newCollection(array $items = [])
{
return new Collection($items);
}
/**
* Sorts LDAP search results.
*
* @param array $models
*
* @return Collection
*/
protected function processSort(array $models = [])
{
$field = $this->builder->getSortByField();
$flags = $this->builder->getSortByFlags();
$direction = $this->builder->getSortByDirection();
$desc = ($direction === 'desc' ? true : false);
return $this->newCollection($models)->sortBy($field, $flags, $desc);
}
}
@@ -0,0 +1,94 @@
<?php
namespace Adldap\Schemas;
class ActiveDirectory extends Schema
{
/**
* {@inheritdoc}
*/
public function distinguishedName()
{
return 'distinguishedname';
}
/**
* {@inheritdoc}
*/
public function distinguishedNameSubKey()
{
return 0;
}
/**
* {@inheritdoc}
*/
public function filterEnabled()
{
return '(!(UserAccountControl:1.2.840.113556.1.4.803:=2))';
}
/**
* {@inheritdoc}
*/
public function filterDisabled()
{
return '(UserAccountControl:1.2.840.113556.1.4.803:=2)';
}
/**
* {@inheritdoc}
*/
public function lockoutTime()
{
return 'lockouttime';
}
/**
* {@inheritdoc}
*/
public function objectClassGroup()
{
return 'group';
}
/**
* {@inheritdoc}
*/
public function objectClassOu()
{
return 'organizationalunit';
}
/**
* {@inheritdoc}
*/
public function objectClassPerson()
{
return 'person';
}
/**
* {@inheritdoc}
*/
public function objectGuid()
{
return 'objectguid';
}
/**
* {@inheritdoc}
*/
public function objectGuidRequiresConversion()
{
return true;
}
/**
* {@inheritdoc}
*/
public function objectCategory()
{
return 'objectcategory';
}
}
@@ -0,0 +1,110 @@
<?php
namespace Adldap\Schemas;
class Directory389 extends Schema
{
/**
* {@inheritdoc}
*/
public function accountName()
{
return 'uid';
}
/**
* {@inheritdoc}
*/
public function distinguishedName()
{
return 'dn';
}
/**
* {@inheritdoc}
*/
public function distinguishedNameSubKey()
{
//
}
/**
* {@inheritdoc}
*/
public function filterEnabled()
{
return sprintf('(!(%s=*))', $this->lockoutTime());
}
/**
* {@inheritdoc}
*/
public function filterDisabled()
{
return sprintf('(%s=*)', $this->lockoutTime());
}
/**
* {@inheritdoc}
*/
public function lockoutTime()
{
return 'pwdAccountLockedTime';
}
/**
* {@inheritdoc}
*/
public function objectCategory()
{
return 'objectclass';
}
/**
* {@inheritdoc}
*/
public function objectClassGroup()
{
return 'groupofnames';
}
/**
* {@inheritdoc}
*/
public function objectClassOu()
{
return 'organizationalUnit';
}
/**
* {@inheritdoc}
*/
public function objectClassPerson()
{
return 'inetorgperson';
}
/**
* {@inheritdoc}
*/
public function objectClassUser()
{
return 'inetorgperson';
}
/**
* {@inheritdoc}
*/
public function objectGuid()
{
return 'nsuniqueid';
}
/**
* {@inheritdoc}
*/
public function objectGuidRequiresConversion()
{
return false;
}
}
@@ -0,0 +1,110 @@
<?php
namespace Adldap\Schemas;
class EDirectory extends Schema
{
/**
* {@inheritdoc}
*/
public function accountName()
{
return 'uid';
}
/**
* {@inheritdoc}
*/
public function distinguishedName()
{
return 'dn';
}
/**
* {@inheritdoc}
*/
public function distinguishedNameSubKey()
{
//
}
/**
* {@inheritdoc}
*/
public function filterEnabled()
{
return sprintf('(!(%s=*))', $this->lockoutTime());
}
/**
* {@inheritdoc}
*/
public function filterDisabled()
{
return sprintf('(%s=*)', $this->lockoutTime());
}
/**
* {@inheritdoc}
*/
public function lockoutTime()
{
return 'pwdAccountLockedTime';
}
/**
* {@inheritdoc}
*/
public function objectCategory()
{
return 'objectclass';
}
/**
* {@inheritdoc}
*/
public function objectClassGroup()
{
return 'groupofnames';
}
/**
* {@inheritdoc}
*/
public function objectClassOu()
{
return 'organizationalUnit';
}
/**
* {@inheritdoc}
*/
public function objectClassPerson()
{
return 'inetorgperson';
}
/**
* {@inheritdoc}
*/
public function objectClassUser()
{
return 'inetorgperson';
}
/**
* {@inheritdoc}
*/
public function objectGuid()
{
return 'guid';
}
/**
* {@inheritdoc}
*/
public function objectGuidRequiresConversion()
{
return false;
}
}
@@ -0,0 +1,126 @@
<?php
namespace Adldap\Schemas;
class FreeIPA extends Schema
{
/**
* {@inheritdoc}
*/
public function accountName()
{
return 'uid';
}
/**
* {@inheritdoc}
*/
public function distinguishedName()
{
return 'dn';
}
/**
* {@inheritdoc}
*/
public function objectCategory()
{
return 'objectclass';
}
/**
* {@inheritdoc}
*/
public function objectClassGroup()
{
return 'ipausergroup';
}
/**
* {@inheritdoc}
*/
public function userPrincipalName()
{
return 'krbCanonicalName';
}
/**
* {@inheritdoc}
*/
public function distinguishedNameSubKey()
{
return 0;
}
/**
* {@inheritdoc}
*/
public function filterEnabled()
{
return '(!(UserAccountControl:1.2.840.113556.1.4.803:=2))';
}
/**
* {@inheritdoc}
*/
public function filterDisabled()
{
return '(UserAccountControl:1.2.840.113556.1.4.803:=2)';
}
/**
* {@inheritdoc}
*/
public function lockoutTime()
{
return 'lockouttime';
}
/**
* {@inheritdoc}
*/
public function passwordLastSet()
{
return 'krbLastPwdChange';
}
/**
* {@inheritdoc}
*/
public function objectClassOu()
{
return 'organizationalunit';
}
/**
* {@inheritdoc}
*/
public function objectClassPerson()
{
return 'person';
}
/**
* {@inheritdoc}
*/
public function objectClassUser()
{
return 'organizationalPerson';
}
/**
* {@inheritdoc}
*/
public function objectGuid()
{
return 'ipaUniqueID';
}
/**
* {@inheritdoc}
*/
public function objectGuidRequiresConversion()
{
return false;
}
}
@@ -0,0 +1,110 @@
<?php
namespace Adldap\Schemas;
class OpenLDAP extends Schema
{
/**
* {@inheritdoc}
*/
public function accountName()
{
return 'uid';
}
/**
* {@inheritdoc}
*/
public function distinguishedName()
{
return 'dn';
}
/**
* {@inheritdoc}
*/
public function distinguishedNameSubKey()
{
//
}
/**
* {@inheritdoc}
*/
public function filterEnabled()
{
return sprintf('(!(%s=*))', $this->lockoutTime());
}
/**
* {@inheritdoc}
*/
public function filterDisabled()
{
return sprintf('(%s=*)', $this->lockoutTime());
}
/**
* {@inheritdoc}
*/
public function lockoutTime()
{
return 'pwdAccountLockedTime';
}
/**
* {@inheritdoc}
*/
public function objectCategory()
{
return 'objectclass';
}
/**
* {@inheritdoc}
*/
public function objectClassGroup()
{
return 'groupofnames';
}
/**
* {@inheritdoc}
*/
public function objectClassOu()
{
return 'organizationalUnit';
}
/**
* {@inheritdoc}
*/
public function objectClassPerson()
{
return 'inetorgperson';
}
/**
* {@inheritdoc}
*/
public function objectClassUser()
{
return 'inetorgperson';
}
/**
* {@inheritdoc}
*/
public function objectGuid()
{
return 'entryuuid';
}
/**
* {@inheritdoc}
*/
public function objectGuidRequiresConversion()
{
return false;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,232 @@
<?php
namespace Adldap;
class Utilities
{
/**
* Converts a DN string into an array of RDNs.
*
* This will also decode hex characters into their true
* UTF-8 representation embedded inside the DN as well.
*
* @param string $dn
* @param bool $removeAttributePrefixes
*
* @return array|false
*/
public static function explodeDn($dn, $removeAttributePrefixes = true)
{
$dn = ldap_explode_dn($dn, ($removeAttributePrefixes ? 1 : 0));
if (is_array($dn) && array_key_exists('count', $dn)) {
foreach ($dn as $rdn => $value) {
$dn[$rdn] = self::unescape($value);
}
}
return $dn;
}
/**
* Un-escapes a hexadecimal string into
* its original string representation.
*
* @param string $value
*
* @return string
*/
public static function unescape($value)
{
return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
return chr(hexdec($matches[1]));
}, $value);
}
/**
* Convert a binary SID to a string SID.
*
* @author Chad Sikorra
*
* @link https://github.com/ChadSikorra
* @link https://stackoverflow.com/questions/39533560/php-ldap-get-user-sid
*
* @param string $value The Binary SID
*
* @return string|null
*/
public static function binarySidToString($value)
{
// Revision - 8bit unsigned int (C1)
// Count - 8bit unsigned int (C1)
// 2 null bytes
// ID - 32bit unsigned long, big-endian order
$sid = @unpack('C1rev/C1count/x2/N1id', $value);
if (!isset($sid['id']) || !isset($sid['rev'])) {
return;
}
$revisionLevel = $sid['rev'];
$identifierAuthority = $sid['id'];
$subs = isset($sid['count']) ? $sid['count'] : 0;
$sidHex = $subs ? bin2hex($value) : '';
$subAuthorities = [];
// The sub-authorities depend on the count, so only get as
// many as the count, regardless of data beyond it.
for ($i = 0; $i < $subs; $i++) {
$data = implode('', array_reverse(
str_split(
substr($sidHex, 16 + ($i * 8), 8),
2
)
));
$subAuthorities[] = hexdec($data);
}
// Tack on the 'S-' and glue it all together...
return 'S-'.$revisionLevel.'-'.$identifierAuthority.implode(
preg_filter('/^/', '-', $subAuthorities)
);
}
/**
* Convert a binary GUID to a string GUID.
*
* @param string $binGuid
*
* @return string|null
*/
public static function binaryGuidToString($binGuid)
{
if (trim($binGuid) == '' || is_null($binGuid)) {
return;
}
$hex = unpack('H*hex', $binGuid)['hex'];
$hex1 = substr($hex, -26, 2).substr($hex, -28, 2).substr($hex, -30, 2).substr($hex, -32, 2);
$hex2 = substr($hex, -22, 2).substr($hex, -24, 2);
$hex3 = substr($hex, -18, 2).substr($hex, -20, 2);
$hex4 = substr($hex, -16, 4);
$hex5 = substr($hex, -12, 12);
$guid = sprintf('%s-%s-%s-%s-%s', $hex1, $hex2, $hex3, $hex4, $hex5);
return $guid;
}
/**
* Converts a string GUID to it's hex variant.
*
* @param string $string
*
* @return string
*/
public static function stringGuidToHex($string)
{
$hex = '\\'.substr($string, 6, 2).'\\'.substr($string, 4, 2).'\\'.substr($string, 2, 2).'\\'.substr($string, 0, 2);
$hex = $hex.'\\'.substr($string, 11, 2).'\\'.substr($string, 9, 2);
$hex = $hex.'\\'.substr($string, 16, 2).'\\'.substr($string, 14, 2);
$hex = $hex.'\\'.substr($string, 19, 2).'\\'.substr($string, 21, 2);
$hex = $hex.'\\'.substr($string, 24, 2).'\\'.substr($string, 26, 2).'\\'.substr($string, 28, 2).'\\'.substr($string, 30, 2).'\\'.substr($string, 32, 2).'\\'.substr($string, 34, 2);
return $hex;
}
/**
* Encode a password for transmission over LDAP.
*
* @param string $password The password to encode
*
* @return string
*/
public static function encodePassword($password)
{
return iconv('UTF-8', 'UTF-16LE', '"'.$password.'"');
}
/**
* Salt and hash a password to make its SSHA OpenLDAP version.
*
* @param string $password The password to create
*
* @return string
*/
public static function makeSSHAPassword($password)
{
mt_srand((float) microtime() * 1000000);
$salt = pack('CCCC', mt_rand(), mt_rand(), mt_rand(), mt_rand());
return '{SSHA}'.base64_encode(pack('H*', sha1($password.$salt)).$salt);
}
/**
* Round a Windows timestamp down to seconds and remove
* the seconds between 1601-01-01 and 1970-01-01.
*
* @param float $windowsTime
*
* @return float
*/
public static function convertWindowsTimeToUnixTime($windowsTime)
{
return round($windowsTime / 10000000) - 11644473600;
}
/**
* Convert a Unix timestamp to Windows timestamp.
*
* @param float $unixTime
*
* @return float
*/
public static function convertUnixTimeToWindowsTime($unixTime)
{
return ($unixTime + 11644473600) * 10000000;
}
/**
* Validates that the inserted string is an object SID.
*
* @param string $sid
*
* @return bool
*/
public static function isValidSid($sid)
{
return (bool) preg_match("/^S-\d(-\d{1,10}){1,16}$/i", $sid);
}
/**
* Validates that the inserted string is an object GUID.
*
* @param string $guid
*
* @return bool
*/
public static function isValidGuid($guid)
{
return (bool) preg_match('/^([0-9a-fA-F]){8}(-([0-9a-fA-F]){4}){3}-([0-9a-fA-F]){12}$|^([0-9a-fA-F]{8}-){3}[0-9a-fA-F]{8}$/', $guid);
}
/**
* Converts an ignore string into an array.
*
* @param string $ignore
*
* @return array
*/
protected static function ignoreStrToArray($ignore)
{
$ignore = trim($ignore);
return $ignore ? str_split($ignore) : [];
}
}