├── .gitignore ├── API ├── ConnectionInterface.php ├── DriverInterface.php ├── EntryInterface.php └── SearchInterface.php ├── Core ├── DiffTracker.php ├── Manager.php ├── Node.php ├── NodeAttribute.php └── SearchResult.php ├── Exception ├── BindException.php ├── ConnectionException.php ├── DeleteException.php ├── LdapException.php ├── MalformedFilterException.php ├── NoResultException.php ├── NodeNotFoundException.php ├── NotBoundException.php ├── OptionException.php ├── PersistenceException.php ├── RebaseException.php ├── SaveException.php ├── SearchException.php ├── SizeLimitException.php └── UnsupportedDriverException.php ├── LICENSE ├── Platform ├── Native │ ├── Connection.php │ ├── Driver.php │ ├── Entry.php │ └── Search.php └── Test │ ├── Connection.php │ ├── Driver.php │ ├── Entry.php │ └── Search.php ├── README.md ├── Tests ├── Core │ ├── DiffTrackerTest.php │ ├── Manager │ │ ├── ManagerConnectTest.php │ │ ├── ManagerReadTest.php │ │ ├── ManagerTest.php │ │ └── ManagerWriteTest.php │ ├── NodeAttributeTest.php │ ├── NodeTest.php │ └── SearchResultTest.php ├── Platform │ └── Test │ │ ├── ConnectionTest.php │ │ ├── DriverTest.php │ │ ├── EntryTest.php │ │ └── SearchTest.php └── TestCase.php ├── autoload.php-dist ├── composer.json ├── composer.lock └── phpunit.xml-dist /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | phpunit.xml 3 | autoload.php 4 | 5 | -------------------------------------------------------------------------------- /API/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\API; 13 | 14 | use Toyota\Component\Ldap\Exception\ConnectionException; 15 | use Toyota\Component\Ldap\Exception\OptionException; 16 | use Toyota\Component\Ldap\Exception\BindException; 17 | use Toyota\Component\Ldap\Exception\PersistenceException; 18 | use Toyota\Component\Ldap\Exception\NoResultException; 19 | use Toyota\Component\Ldap\Exception\SizeLimitException; 20 | use Toyota\Component\Ldap\Exception\MalformedFilterException; 21 | use Toyota\Component\Ldap\Exception\SearchException; 22 | use Toyota\Component\Ldap\API\SearchInterface; 23 | 24 | /** 25 | * Represents a class that enables interacting with a LDAP Connection 26 | * 27 | * @author Cyril Cottet 28 | */ 29 | interface ConnectionInterface 30 | { 31 | const OPT_DEREF = LDAP_OPT_DEREF; 32 | const OPT_SIZELIMIT = LDAP_OPT_SIZELIMIT; 33 | const OPT_TIMELIMIT = LDAP_OPT_TIMELIMIT; 34 | const OPT_NETWORK_TIMEOUT = LDAP_OPT_NETWORK_TIMEOUT; 35 | const OPT_PROTOCOL_VERSION = LDAP_OPT_PROTOCOL_VERSION; 36 | const OPT_REFERRALS = LDAP_OPT_REFERRALS; 37 | const OPT_RESTART = LDAP_OPT_RESTART; 38 | const OPT_SERVER_CONTROLS = LDAP_OPT_SERVER_CONTROLS; 39 | const OPT_CLIENT_CONTROLS = LDAP_OPT_CLIENT_CONTROLS; 40 | 41 | const DEREF_NEVER = LDAP_DEREF_NEVER; 42 | const DEREF_SEARCHING = LDAP_DEREF_SEARCHING; 43 | const DEREF_FINDING = LDAP_DEREF_FINDING; 44 | const DEREF_ALWAYS = LDAP_DEREF_ALWAYS; 45 | 46 | /** 47 | * Set an option 48 | * 49 | * @param int $option Ldap option name 50 | * @param mixed $value Value to set on Ldap option 51 | * 52 | * @return void 53 | * 54 | * @throws OptionException if option cannot be set 55 | */ 56 | public function setOption($option, $value); 57 | 58 | /** 59 | * Gets current value set for an option 60 | * 61 | * @param int $option Ldap option name 62 | * 63 | * @return mixed value set for the option 64 | * 65 | * @throws OptionException if option cannot be retrieved 66 | */ 67 | public function getOption($option); 68 | 69 | /** 70 | * Binds to the LDAP directory with specified RDN and password 71 | * 72 | * @param string $rdn Rdn to use for binding (Default: null) 73 | * @param string $password Plain or hashed password for binding (Default: null) 74 | * 75 | * @return void 76 | * 77 | * @throws BindException if binding fails 78 | */ 79 | public function bind($rdn = null, $password = null); 80 | 81 | /** 82 | * Closes the connection 83 | * 84 | * @return void 85 | * 86 | * @throws ConnectionException if connection could not be closed 87 | */ 88 | public function close(); 89 | 90 | /** 91 | * Adds a Ldap entry 92 | * 93 | * @param string $dn Distinguished name to register entry for 94 | * @param array $data Ldap attributes to save along with the entry 95 | * 96 | * @return void 97 | * 98 | * @throws PersistenceException if entry could not be added 99 | */ 100 | public function addEntry($dn, $data); 101 | 102 | /** 103 | * Deletes an existing Ldap entry 104 | * 105 | * @param string $dn Distinguished name of the entry to delete 106 | * 107 | * @return void 108 | * 109 | * @throws PersistenceException if entry could not be deleted 110 | */ 111 | public function deleteEntry($dn); 112 | 113 | /** 114 | * Adds some value(s) to some entry attribute(s) 115 | * 116 | * The data format for attributes is as follows: 117 | * array( 118 | * 'attribute_1' => array( 119 | * 'value_1', 120 | * 'value_2' 121 | * ), 122 | * 'attribute_2' => array( 123 | * 'value_1', 124 | * 'value_2' 125 | * ), 126 | * ... 127 | * ); 128 | * 129 | * @param string $dn Distinguished name of the entry to modify 130 | * @param array $data Values to be added for each attribute 131 | * 132 | * @return void 133 | * 134 | * @throws PersistenceException if entry could not be updated 135 | */ 136 | public function addAttributeValues($dn, $data); 137 | 138 | /** 139 | * Replaces value(s) for some entry attribute(s) 140 | * 141 | * The data format for attributes is as follows: 142 | * array( 143 | * 'attribute_1' => array( 144 | * 'value_1', 145 | * 'value_2' 146 | * ), 147 | * 'attribute_2' => array( 148 | * 'value_1', 149 | * 'value_2' 150 | * ), 151 | * ... 152 | * ); 153 | * 154 | * @param string $dn Distinguished name of the entry to modify 155 | * @param array $data Values to be set for each attribute 156 | * 157 | * @return void 158 | * 159 | * @throws PersistenceException if entry could not be updated 160 | */ 161 | public function replaceAttributeValues($dn, $data); 162 | 163 | /** 164 | * Delete value(s) for some entry attribute(s) 165 | * 166 | * The data format for attributes is as follows: 167 | * array( 168 | * 'attribute_1' => array( 169 | * 'value_1', 170 | * 'value_2' 171 | * ), 172 | * 'attribute_2' => array( 173 | * 'value_1', 174 | * 'value_2' 175 | * ), 176 | * ... 177 | * ); 178 | * 179 | * @param string $dn Distinguished name of the entry to modify 180 | * @param array $data Values to be removed for each attribute 181 | * 182 | * @return void 183 | * 184 | * @throws PersistenceException if entry could not be updated 185 | */ 186 | public function deleteAttributeValues($dn, $data); 187 | 188 | /** 189 | * Searches for entries in the directory 190 | * 191 | * @param int $scope Search scope (ALL, ONE or BASE) 192 | * @param string $baseDn Base distinguished name to look below 193 | * @param string $filter Filter for the search 194 | * @param array $attributes Names of attributes to retrieve (Default: All) 195 | * 196 | * @return SearchInterface Search result set 197 | * 198 | * @throws NoResultException if no result can be retrieved 199 | * @throws SizeLimitException if size limit got exceeded 200 | * @throws MalformedFilterException if filter is wrongly formatted 201 | * @throws SearchException if search failed otherwise 202 | */ 203 | public function search($scope, $baseDn, $filter, $attributes = null); 204 | } -------------------------------------------------------------------------------- /API/DriverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\API; 13 | 14 | use Toyota\Component\Ldap\API\ConnectionInterface; 15 | use Toyota\Component\Ldap\Exception\ConnectionException; 16 | 17 | /** 18 | * Represents a class that enables interacting with an LDAP server 19 | * 20 | * @author Cyril Cottet 21 | */ 22 | interface DriverInterface 23 | { 24 | /** 25 | * Connects to a Ldap directory without binding 26 | * 27 | * @param string $hostname Hostname to connect to 28 | * @param int $port Port to connect to (Default: 389) 29 | * @param boolean $withSSL Whether to connect with SSL support (Default: false) 30 | * @param boolean $withTLS Whether to connect with TLS support (Default: false) 31 | * 32 | * @return ConnectionInterface connection instance 33 | * 34 | * @throws ConnectionException if connection fails 35 | */ 36 | public function connect( 37 | $hostname, 38 | $port = 389, 39 | $withSSL = false, 40 | $withTLS = false 41 | ); 42 | } -------------------------------------------------------------------------------- /API/EntryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\API; 13 | 14 | /** 15 | * Represents a class that enables retrieving attributes for a LDAP entry 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | interface EntryInterface 20 | { 21 | /** 22 | * Retrieves entry distinguished name 23 | * 24 | * @return string Distinguished name 25 | */ 26 | public function getDn(); 27 | 28 | /** 29 | * Retrieves entry attributes 30 | * 31 | * @return array(attribute => array(values)) 32 | */ 33 | public function getAttributes(); 34 | } -------------------------------------------------------------------------------- /API/SearchInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\API; 13 | 14 | use Toyota\Component\Ldap\API\EntryInterface; 15 | 16 | /** 17 | * Represents a class that enables handling result set for a LDAP search 18 | * 19 | * @author Cyril Cottet 20 | */ 21 | interface SearchInterface 22 | { 23 | 24 | const SCOPE_ALL = 1; 25 | const SCOPE_ONE = 2; 26 | const SCOPE_BASE = 3; 27 | 28 | /** 29 | * Retrieves next available entry from the search result set 30 | * 31 | * @return EntryInterface next entry if available, null otherwise 32 | */ 33 | public function next(); 34 | 35 | /** 36 | * Resets entry iterator 37 | * 38 | * @return void 39 | */ 40 | public function reset(); 41 | 42 | /** 43 | * Frees memory for current result set 44 | * 45 | * @return void 46 | */ 47 | public function free(); 48 | 49 | } -------------------------------------------------------------------------------- /Core/DiffTracker.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Core; 13 | 14 | /** 15 | * Class to track differentials while maintaining Nodes & NodeAttributes 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class DiffTracker 20 | { 21 | protected $added = array(); 22 | 23 | protected $deleted = array(); 24 | 25 | protected $replaced = array(); 26 | 27 | protected $ignored = array(); 28 | 29 | protected $isOverridden = false; 30 | 31 | /** 32 | * Resets tracking 33 | * 34 | * @return void 35 | */ 36 | public function reset() 37 | { 38 | $this->added = array(); 39 | $this->deleted = array(); 40 | $this->replaced = array(); 41 | $this->ignored = array(); 42 | $this->isOverridden = false; 43 | } 44 | 45 | /** 46 | * Logs an addition in the diff tracker 47 | * 48 | * @param string $value Added value 49 | * 50 | * @return void 51 | */ 52 | public function logAddition($value) 53 | { 54 | if ($this->isOverridden()) { 55 | return; 56 | } 57 | if (isset($this->deleted[$value])) { 58 | unset($this->deleted[$value]); 59 | $this->replaced[$value] = $value; 60 | return; 61 | } 62 | if (! isset($this->replaced[$value])) { 63 | $this->added[$value] = $value; 64 | } 65 | } 66 | 67 | /** 68 | * Logs a deletion in the diff tracker 69 | * 70 | * @param string $value Added value 71 | * 72 | * @return void 73 | */ 74 | public function logDeletion($value) 75 | { 76 | if ($this->isOverridden()) { 77 | return; 78 | } 79 | if (isset($this->added[$value])) { 80 | unset($this->added[$value]); 81 | $this->ignored[$value] = $value; 82 | return; 83 | } 84 | if (isset($this->replaced[$value])) { 85 | unset($this->replaced[$value]); 86 | } 87 | if (! isset($this->ignored[$value])) { 88 | $this->deleted[$value] = $value; 89 | } 90 | } 91 | 92 | /** 93 | * Logs a replacement in the diff tracker 94 | * 95 | * @param string $value Replaced value 96 | * 97 | * @return void 98 | */ 99 | public function logReplacement($value) 100 | { 101 | if ($this->isOverridden()) { 102 | return; 103 | } 104 | if (isset($this->added[$value])) { 105 | unset($this->added[$value]); 106 | } 107 | if (isset($this->deleted[$value])) { 108 | unset($this->deleted[$value]); 109 | } 110 | $this->replaced[$value] = $value; 111 | } 112 | 113 | /** 114 | * Checks if complete item has been overriden in the change process 115 | * 116 | * @return boolean 117 | */ 118 | public function isOverridden() 119 | { 120 | return $this->isOverridden; 121 | } 122 | 123 | /** 124 | * Marks the object as overridden 125 | * 126 | * @return void 127 | */ 128 | public function markOverridden() 129 | { 130 | $this->reset(); 131 | $this->isOverridden = true; 132 | } 133 | 134 | /** 135 | * Retrieves additions tracked 136 | * 137 | * @return array Additions 138 | */ 139 | public function getAdditions() 140 | { 141 | return array_values($this->added); 142 | } 143 | 144 | /** 145 | * Retrieves deletions tracked 146 | * 147 | * @return array Deletions 148 | */ 149 | public function getDeletions() 150 | { 151 | return array_values($this->deleted); 152 | } 153 | 154 | /** 155 | * Retrieves replacements tracked 156 | * 157 | * @return array Replacements 158 | */ 159 | public function getReplacements() 160 | { 161 | return array_values($this->replaced); 162 | } 163 | } -------------------------------------------------------------------------------- /Core/Manager.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Core; 13 | 14 | use Toyota\Component\Ldap\API\DriverInterface; 15 | use Toyota\Component\Ldap\API\SearchInterface; 16 | use Toyota\Component\Ldap\Exception\NodeNotFoundException; 17 | use Toyota\Component\Ldap\Exception\NoResultException; 18 | use Toyota\Component\Ldap\Exception\NotBoundException; 19 | use Toyota\Component\Ldap\Exception\PersistenceException; 20 | use Toyota\Component\Ldap\Exception\DeleteException; 21 | use Toyota\Component\Ldap\Core\Node; 22 | 23 | /** 24 | * Class to handle Ldap operations 25 | * 26 | * @author Cyril Cottet 27 | */ 28 | class Manager 29 | { 30 | 31 | protected $connection = null; 32 | 33 | protected $isBound = false; 34 | 35 | protected $configuration = array(); 36 | 37 | protected $driver = null; 38 | 39 | /** 40 | * Default constructor 41 | * 42 | * Parameters structure: 43 | * Required: 44 | * hostname => ldap.example.com 45 | * base_dn => dc=example,dc=com 46 | * Optional: 47 | * port => 389 48 | * security => one of SSL or TLS 49 | * bind_dn => cn=admin,dc=example,dc=com (bind will be anonymous if not given) 50 | * bind_password => secret (can be plain or a hash) 51 | * options => array of options according to driver specifications 52 | * 53 | * @param array $params Connection parameters 54 | * @param DriverInterface $driver Driver to use for execution 55 | * 56 | * @return Manager 57 | * 58 | * @throws \InvalidArgumentException if parameters are incorrect 59 | */ 60 | public function __construct(array $params, DriverInterface $driver) 61 | { 62 | $this->configure($params); 63 | $this->driver = $driver; 64 | } 65 | 66 | /** 67 | * Connects to the Ldap store 68 | * 69 | * @return void 70 | * 71 | * @throws Toyota\Component\Ldap\Exception\ConnectionException if connection fails 72 | * @throws Toyota\Component\Ldap\Exception\OptionException if connection configuration fails 73 | */ 74 | public function connect() 75 | { 76 | $this->isBound = false; 77 | $this->connection = $this->driver->connect( 78 | $this->configuration['hostname'], 79 | $this->configuration['port'], 80 | $this->configuration['withSSL'], 81 | $this->configuration['withTLS'] 82 | ); 83 | 84 | foreach ($this->configuration['options'] as $key => $value) { 85 | $this->connection->setOption($key, $value); 86 | } 87 | } 88 | 89 | /** 90 | * Binds to the Ldap connection 91 | * 92 | * @param string $name Bind rdn (Default: null) 93 | * @param string $password Bind password (Default: null) 94 | * 95 | * @return void 96 | * 97 | * @throws Toyota\Component\Ldap\Exception\BindException if binding fails 98 | */ 99 | public function bind($name = null, $password = null) 100 | { 101 | if (strlen(trim($name)) > 0) { 102 | $password = (null === $password)?'':$password; 103 | $this->connection->bind($name, $password); 104 | $this->isBound = true; 105 | return; 106 | } 107 | 108 | if ($this->configuration['bind_anonymous']) { 109 | $this->connection->bind(); 110 | $this->isBound = true; 111 | return; 112 | } 113 | 114 | $this->connection->bind( 115 | $this->configuration['bind_dn'], 116 | $this->configuration['bind_password'] 117 | ); 118 | $this->isBound = true; 119 | } 120 | 121 | /** 122 | * Complete and validates given parameters with default settings 123 | * 124 | * @param array $params Parameters to be cleaned 125 | * 126 | * @return array Cleaned parameters 127 | * 128 | * @throws \InvalidArgumentException 129 | */ 130 | protected function configure(array $params) 131 | { 132 | $required = array('hostname', 'base_dn'); 133 | $missing = array(); 134 | foreach ($required as $key) { 135 | if (! array_key_exists($key, $params)) { 136 | $missing[] = $key; 137 | } 138 | } 139 | if (count($missing) > 0) { 140 | throw new \InvalidArgumentException( 141 | 'Required parameters missing: ' . implode(', ', $missing) 142 | ); 143 | } 144 | 145 | $idx = strpos($params['hostname'], '://'); 146 | $enforceSSL = false; 147 | if (false !== $idx) { 148 | $prefix = strtolower(substr($params['hostname'], 0, $idx)); 149 | if ($prefix === 'ldaps') { 150 | $enforceSSL = true; 151 | } 152 | $params['hostname'] = substr($params['hostname'], $idx+3); 153 | } 154 | 155 | $params['withSSL'] = false; 156 | $params['withTLS'] = false; 157 | if (array_key_exists('security', $params)) { 158 | switch($params['security']) { 159 | case 'SSL': 160 | $params['withSSL'] = true; 161 | break; 162 | case 'TLS': 163 | $params['withTLS'] = true; 164 | break; 165 | default: 166 | throw new \InvalidArgumentException( 167 | sprintf( 168 | 'Security mode %s not supported - only SSL or TLS are supported', 169 | $params['security'] 170 | ) 171 | ); 172 | } 173 | } elseif ($enforceSSL) { 174 | $params['withSSL'] = true; 175 | } 176 | 177 | if (! array_key_exists('port', $params)) { 178 | if ($params['withSSL']) { 179 | $params['port'] = 636; 180 | } else { 181 | $params['port'] = 389; 182 | } 183 | } 184 | 185 | if (! array_key_exists('options', $params)) { 186 | $params['options'] = array(); 187 | } 188 | 189 | $params['bind_anonymous'] = false; 190 | if ((! array_key_exists('bind_dn', $params)) || (strlen(trim($params['bind_dn'])) == 0)) { 191 | $params['bind_anonymous'] = true; 192 | $params['bind_dn'] = ''; 193 | $params['bind_password'] = ''; 194 | } 195 | if (! array_key_exists('bind_password', $params)) { 196 | $params['bind_password'] = ''; 197 | } 198 | 199 | $this->configuration = $params; 200 | } 201 | 202 | /** 203 | * Retrieve a node knowing its dn 204 | * 205 | * @param string $dn Distinguished name of the node to look for 206 | * @param array $attributes Filter attributes to be retrieved (Optional) 207 | * @param string $filter Ldap filter according to RFC4515 (Optional) 208 | * 209 | * @return Node 210 | * 211 | * @throws NodeNotFoundException if node cannot be retrieved 212 | */ 213 | public function getNode($dn, $attributes = null, $filter = null) 214 | { 215 | $this->validateBinding(); 216 | 217 | $attributes = (is_array($attributes))?$attributes:null; 218 | $filter = (null === $filter)?'(objectclass=*)':$filter; 219 | 220 | try { 221 | $search = $this->connection->search( 222 | SearchInterface::SCOPE_BASE, 223 | $dn, 224 | $filter, 225 | $attributes 226 | ); 227 | } catch (NoResultException $e) { 228 | throw new NodeNotFoundException(sprintf('Node %s not found', $dn)); 229 | } 230 | 231 | if (null === ($entry = $search->next())) { 232 | throw new NodeNotFoundException(sprintf('Node %s not found', $dn)); 233 | } 234 | 235 | $node = new Node(); 236 | $node->hydrateFromEntry($entry); 237 | 238 | return $node; 239 | } 240 | 241 | /** 242 | * Execites a search on the ldap 243 | * 244 | * @param string $baseDn Base distinguished name to search in (Default = configured dn) 245 | * @param string $filter Ldap filter according to RFC4515 (Default = null) 246 | * @param boolean $inDepth Whether to search through all subtree depth (Default = true) 247 | * @param array $attributes Filter attributes to be retrieved (Default: null) 248 | * 249 | * @return SearchResult 250 | */ 251 | public function search( 252 | $baseDn = null, 253 | $filter = null, 254 | $inDepth = true, 255 | $attributes = null 256 | ) { 257 | $this->validateBinding(); 258 | 259 | $result = new SearchResult(); 260 | 261 | $baseDn = (null === $baseDn)?$this->configuration['base_dn']:$baseDn; 262 | $filter = (null === $filter)?'(objectclass=*)':$filter; 263 | $attributes = (is_array($attributes))?$attributes:null; 264 | $scope = $inDepth?SearchInterface::SCOPE_ALL:SearchInterface::SCOPE_ONE; 265 | 266 | try { 267 | $search = $this->connection->search($scope, $baseDn, $filter, $attributes); 268 | } catch (NoResultException $e) { 269 | return $result; 270 | } 271 | $result->setSearch($search); 272 | 273 | return $result; 274 | } 275 | 276 | /** 277 | * Validates that Ldap is bound before performing some kind of operation 278 | * 279 | * @return void 280 | * 281 | * @throws NotBoundException if binding has not occured yet 282 | */ 283 | protected function validateBinding() 284 | { 285 | if (! $this->isBound) { 286 | throw new NotBoundException('You have to bind to the Ldap first'); 287 | } 288 | } 289 | 290 | /** 291 | * Saves a node to the Ldap store 292 | * 293 | * @param Node $node Node to be saved 294 | * 295 | * @return boolean True if node got created, false if it was updated 296 | * 297 | * @throws PersistenceException If saving operation fails writing to the Ldap 298 | */ 299 | public function save(Node $node) 300 | { 301 | $this->validateBinding(); 302 | if (strlen(trim($node->getDn())) == 0) { 303 | throw new PersistenceException('Cannot save: dn missing for the entry'); 304 | } 305 | 306 | if (! $node->isHydrated()) { 307 | try { 308 | $origin = $this->getNode($node->getDn()); 309 | $node->rebaseDiff($origin); 310 | } catch(NodeNotFoundException $e) { 311 | $this->connection->addEntry($node->getDn(), $node->getRawAttributes()); 312 | $node->snapshot(); 313 | return true; 314 | } 315 | } 316 | 317 | if (count($data = $node->getDiffAdditions()) > 0) { 318 | $this->connection->addAttributeValues($node->getDn(), $data); 319 | } 320 | if (count($data = $node->getDiffDeletions()) > 0) { 321 | $this->connection->deleteAttributeValues($node->getDn(), $data); 322 | } 323 | if (count($data = $node->getDiffReplacements()) > 0) { 324 | $this->connection->replaceAttributeValues($node->getDn(), $data); 325 | } 326 | 327 | $node->snapshot(); 328 | return false; 329 | } 330 | 331 | /** 332 | * Retrieves immediate children for the given node 333 | * 334 | * @param Node $node Node to retrieve children for 335 | * 336 | * @return array(Node) a set of Nodes 337 | */ 338 | public function getChildrenNodes(Node $node) 339 | { 340 | $result = $this->search($node->getDn(), null, false); 341 | 342 | $nodes = array(); 343 | foreach ($result as $node) { 344 | $nodes[] = $node; 345 | } 346 | 347 | return $nodes; 348 | } 349 | 350 | /** 351 | * Deletes a node from the Ldap store 352 | * 353 | * @param Node $node Node to delete 354 | * @param boolean $isRecursive Whether to delete node with its children (Default: false) 355 | * 356 | * @return void 357 | * 358 | * @throws DeletionException If node to delete has some children and recursion disabled 359 | */ 360 | public function delete(Node $node, $isRecursive = false) 361 | { 362 | if (! $node->isHydrated()) { 363 | $node = $this->getNode($node->getDn()); 364 | } 365 | $children = $this->getChildrenNodes($node); 366 | if (count($children) > 0) { 367 | if (! $isRecursive) { 368 | throw new DeleteException( 369 | sprintf('%s cannot be deleted - it has some children left', $node->getDn()) 370 | ); 371 | } 372 | foreach ($children as $child) { 373 | $this->delete($child, true); 374 | } 375 | } 376 | $this->connection->deleteEntry($node->getDn()); 377 | 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /Core/Node.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Core; 13 | 14 | use Toyota\Component\Ldap\API\EntryInterface; 15 | use Toyota\Component\Ldap\Exception\RebaseException; 16 | 17 | /** 18 | * Class to handle Ldap nodes (entries) 19 | * 20 | * @author Cyril Cottet 21 | */ 22 | class Node 23 | { 24 | protected $dn; 25 | 26 | protected $attributes = array(); 27 | 28 | public $tracker = null; 29 | 30 | protected $isHydrated = false; 31 | 32 | /** 33 | * Default Node Constructor 34 | * 35 | * @param DiffTracker $tracker Utility for tracking changes (Optional) 36 | * 37 | * @return Node 38 | */ 39 | public function __construct(DiffTracker $tracker = null) 40 | { 41 | $this->tracker = (null === $tracker)?(new DiffTracker()):$tracker; 42 | } 43 | 44 | /** 45 | * Hydrate from a LDAP entry 46 | * 47 | * @param EntryInterface $entry Entry to use for loading 48 | * 49 | * @return void 50 | */ 51 | public function hydrateFromEntry(EntryInterface $entry) 52 | { 53 | $this->dn = $entry->getDn(); 54 | 55 | $this->attributes = array(); 56 | 57 | foreach ($entry->getAttributes() as $name => $data) { 58 | $attr = new NodeAttribute($name); 59 | $attr->add($data); 60 | $this->mergeAttribute($attr); 61 | } 62 | 63 | $this->snapshot(); 64 | } 65 | 66 | /** 67 | * Getter for distinguished name 68 | * 69 | * @return string distinguished name 70 | */ 71 | public function getDn() 72 | { 73 | return $this->dn; 74 | } 75 | 76 | /** 77 | * Setter for distinguished name 78 | * 79 | * @param string $dn Distinguished name 80 | * @param boolean $force Whether to force dn change (Default: false) 81 | * 82 | * @return void 83 | * 84 | * @throws InvalidArgumentException If entry is bound 85 | */ 86 | public function setDn($dn, $force = false) 87 | { 88 | if (($this->isHydrated) && (! $force)) { 89 | throw new \InvalidArgumentException('Dn cannot be updated manually'); 90 | } 91 | $this->dn = $dn; 92 | } 93 | 94 | /** 95 | * Getter for attributes 96 | * 97 | * @return array attributes 98 | */ 99 | public function getAttributes() 100 | { 101 | return $this->attributes; 102 | } 103 | 104 | /** 105 | * Sets an attribute in the node store (replace existing ones with same name) 106 | * 107 | * @param NodeAttribute $attribute Attribute to be set 108 | * 109 | * @return void 110 | */ 111 | public function setAttribute(NodeAttribute $attribute) 112 | { 113 | $this->attributes[$attribute->getName()] = $attribute; 114 | $this->tracker->logReplacement($attribute->getName()); 115 | } 116 | 117 | /** 118 | * Merges an attribute in the node store (add if it is new) 119 | * 120 | * @param NodeAttribute $attribute Attribute to be added 121 | * 122 | * @return void 123 | */ 124 | public function mergeAttribute(NodeAttribute $attribute) 125 | { 126 | if (! $this->has($attribute->getName())) { 127 | $this->attributes[$attribute->getName()] = $attribute; 128 | $this->tracker->logAddition($attribute->getName()); 129 | return; 130 | } 131 | $backup = $attribute; 132 | $attribute = $this->get($attribute->getName()); 133 | $attribute->add($backup->getValues()); 134 | $this->attributes[$attribute->getName()] = $attribute; 135 | } 136 | 137 | /** 138 | * Removes an attribute from the node store 139 | * 140 | * @param string $name Attribute name 141 | * 142 | * @return boolean true on success 143 | */ 144 | public function removeAttribute($name) 145 | { 146 | if (! $this->has($name)) { 147 | return false; 148 | } 149 | unset($this->attributes[$name]); 150 | $this->tracker->logDeletion($name); 151 | return true; 152 | } 153 | 154 | /** 155 | * Retrieves an attribute from its name 156 | * 157 | * @param string $name Name of the attribute to look for 158 | * @param boolean $create Whether to create a new instance if it is not set (Default: false) 159 | * 160 | * @return NodeAttribute or null if it does not exist and $create is false 161 | */ 162 | public function get($name, $create = false) 163 | { 164 | if (! $this->has($name)) { 165 | if (! $create) { 166 | return null; 167 | } 168 | $this->mergeAttribute(new NodeAttribute($name)); 169 | } 170 | return $this->attributes[$name]; 171 | } 172 | 173 | /** 174 | * Checks if an attribute is set in the store 175 | * 176 | * @param string $name Name of the attribute to look for 177 | * 178 | * @return boolean true if it is set 179 | */ 180 | public function has($name) 181 | { 182 | return isset($this->attributes[$name]); 183 | } 184 | 185 | /** 186 | * Retrieves diff additions for the attribute 187 | * 188 | * @return array Added values 189 | */ 190 | public function getDiffAdditions() 191 | { 192 | $data = $this->getValueDiffData('getDiffAdditions'); 193 | foreach ($this->tracker->getAdditions() as $name) { 194 | $data[$name] = $this->get($name)->getValues(); 195 | } 196 | return $data; 197 | } 198 | 199 | /** 200 | * Retrieves diff deletions for the attribute 201 | * 202 | * @return array Deleted values 203 | */ 204 | public function getDiffDeletions() 205 | { 206 | $data = $this->getValueDiffData('getDiffDeletions'); 207 | foreach ($this->tracker->getDeletions() as $name) { 208 | $data[$name] = array(); 209 | } 210 | return $data; 211 | } 212 | 213 | /** 214 | * Retrieve attribute value level diff information 215 | * 216 | * @param string $method Name of the diff method to use on the attribute 217 | * 218 | * @return array Diff data 219 | */ 220 | protected function getValueDiffData($method) 221 | { 222 | $data = array(); 223 | foreach ($this->getSafeAttributes() as $attribute) { 224 | $buffer = call_user_func(array($attribute, $method)); 225 | if (count($buffer) > 0) { 226 | $data[$attribute->getName()] = $buffer; 227 | } 228 | } 229 | return $data; 230 | } 231 | 232 | /** 233 | * Retrieves safe attributes (those which have not been changed at node level) 234 | * 235 | * @return array(NodeAttribute) 236 | */ 237 | protected function getSafeAttributes() 238 | { 239 | $ignored = array_merge( 240 | $this->tracker->getAdditions(), 241 | $this->tracker->getDeletions(), 242 | $this->tracker->getReplacements() 243 | ); 244 | $attributes = array(); 245 | foreach ($this->getAttributes() as $attribute) { 246 | if (in_array($attribute->getName(), $ignored)) { 247 | continue; 248 | } 249 | $attributes[$attribute->getName()] = $attribute; 250 | } 251 | return $attributes; 252 | } 253 | 254 | /** 255 | * Retrieves diff replacements for the attribute 256 | * 257 | * @return array Replaced values 258 | */ 259 | public function getDiffReplacements() 260 | { 261 | $data = array(); 262 | foreach ($this->getSafeAttributes() as $attribute) { 263 | if ($attribute->isReplaced()) { 264 | $data[$attribute->getName()] = $attribute->getValues(); 265 | } 266 | } 267 | foreach ($this->tracker->getReplacements() as $name) { 268 | $data[$name] = $this->get($name)->getValues(); 269 | } 270 | return $data; 271 | } 272 | 273 | /** 274 | * Snapshot resets diff tracking 275 | * 276 | * @param boolean $isHydrated Whether snapshot is due to hydration (Default: true) 277 | * 278 | * @return void 279 | */ 280 | public function snapshot($isHydrated = true) 281 | { 282 | $this->tracker->reset(); 283 | foreach ($this->attributes as $attribute) { 284 | $attribute->snapshot(); 285 | } 286 | if ($isHydrated) { 287 | $this->isHydrated = true; 288 | } 289 | } 290 | 291 | /** 292 | * Is this node hydrated with the relevant Ldap entry 293 | * 294 | * @return boolean 295 | */ 296 | public function isHydrated() 297 | { 298 | return $this->isHydrated; 299 | } 300 | 301 | /** 302 | * Retrieves attribute data in a raw format for persistence operations 303 | * 304 | * @return array Raw data of attributes 305 | */ 306 | public function getRawAttributes() 307 | { 308 | $data = array(); 309 | foreach ($this->attributes as $name => $attribute) { 310 | $data[$name] = $attribute->getValues(); 311 | } 312 | return $data; 313 | } 314 | 315 | /** 316 | * Rebase diff based on source node as an origin 317 | * 318 | * @param Node $node Node to use as a source for origin 319 | * 320 | * @return void 321 | * 322 | * @throws RebaseException If source of origin node has uncommitted changes 323 | */ 324 | public function rebaseDiff(Node $node) 325 | { 326 | $changes = array_merge( 327 | $node->getDiffAdditions(), 328 | $node->getDiffDeletions(), 329 | $node->getDiffReplacements() 330 | ); 331 | if (count($changes) > 0) { 332 | throw new RebaseException( 333 | sprintf( 334 | '%s has some uncommitted changes - Cannot rebase %s on %s', 335 | $node->getDn(), 336 | $this->getDn(), 337 | $node->getDn() 338 | ) 339 | ); 340 | } 341 | $additions = $this->getDiffAdditions(); 342 | $deletions = $this->getDiffDeletions(); 343 | $replacements = $this->getDiffReplacements(); 344 | $this->snapshot(); 345 | $this->attributes = $node->getAttributes(); 346 | foreach ($additions as $attribute => $values) { 347 | $this->get($attribute, true)->add($values); 348 | } 349 | foreach ($deletions as $attribute => $values) { 350 | if (count($values) == 0) { 351 | $this->removeAttribute($attribute); 352 | } else { 353 | if ($this->has($attribute)) { 354 | $this->get($attribute)->remove($values); 355 | } 356 | } 357 | } 358 | foreach ($replacements as $attribute => $values) { 359 | $this->get($attribute, true)->set($values); 360 | } 361 | } 362 | } -------------------------------------------------------------------------------- /Core/NodeAttribute.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Core; 13 | 14 | use Toyota\Component\Ldap\API\SearchInterface; 15 | use Toyota\Component\Ldap\Core\Node; 16 | 17 | /** 18 | * Class to handle Ldap entries attributes 19 | * 20 | * @author Cyril Cottet 21 | */ 22 | class NodeAttribute implements \Iterator, \Countable, \ArrayAccess 23 | { 24 | 25 | protected $values = array(); 26 | 27 | protected $name; 28 | 29 | protected $iterator = 0; 30 | 31 | protected $tracker = null; 32 | 33 | /** 34 | * Default constructor 35 | * 36 | * @param string $name Name of the attribute 37 | * @param DiffTracker $tracker Utility to track diff (Optional) 38 | * 39 | * @return NodeAttribute 40 | */ 41 | public function __construct($name, DiffTracker $tracker = null) 42 | { 43 | $this->name = $name; 44 | 45 | $this->tracker = (null === $tracker)?(new DiffTracker()):$tracker; 46 | } 47 | 48 | /** 49 | * Getter for name 50 | * 51 | * @return string name 52 | */ 53 | public function getName() 54 | { 55 | return $this->name; 56 | } 57 | 58 | /** 59 | * Getter for values array 60 | * 61 | * @return array values 62 | */ 63 | public function getValues() 64 | { 65 | return $this->values; 66 | } 67 | 68 | /** 69 | * Add a value as an instance of this attribute 70 | * 71 | * @param mixed $value Value to add to the attribute instances 72 | * 73 | * @return boolean true if success 74 | */ 75 | public function add($value) 76 | { 77 | if (is_array($value)) { 78 | return $this->handleArray('add', $value); 79 | } 80 | if (is_null($value) || strlen($value) == 0) { 81 | return false; 82 | } 83 | if (false !== array_search($value, $this->values)) { 84 | return false; 85 | } 86 | $this->offsetSet(null, $value); 87 | return true; 88 | } 89 | 90 | /** 91 | * Sets a set of value replacing any existing values registered 92 | * 93 | * @param mixed $values Values to set for the attribute instances 94 | * 95 | * @return boolean true if success 96 | */ 97 | public function set($values) 98 | { 99 | if (! is_array($values)) { 100 | $values = array($values); 101 | } 102 | $this->values = $values; 103 | $this->snapshot(); 104 | $this->tracker->markOverridden(); 105 | return true; 106 | } 107 | 108 | /** 109 | * Checks if the whole attribute was replaced in the diff 110 | * 111 | * @return boolean True if replaced 112 | */ 113 | public function isReplaced() 114 | { 115 | return $this->tracker->isOverridden(); 116 | } 117 | 118 | /** 119 | * Handle action for an array of values to the attribute 120 | * 121 | * @param string $method Name of the method to use for handling 122 | * @param array $values Values to be added 123 | * 124 | * @return boolean True if success 125 | */ 126 | protected function handleArray($method, array $values) 127 | { 128 | $result = false; 129 | foreach ($values as $value) { 130 | $flag = call_user_func(array($this, $method), $value); 131 | $result = $result || $flag; 132 | } 133 | return $result; 134 | } 135 | 136 | /** 137 | * Removes a value from the attribute stack 138 | * 139 | * @param mixed $value Value to be removed 140 | * 141 | * @return boolean True if success 142 | */ 143 | public function remove($value) 144 | { 145 | if (is_array($value)) { 146 | return $this->handleArray('remove', $value); 147 | } 148 | $key = array_search($value, $this->values); 149 | if (false === $key) { 150 | return false; 151 | } 152 | $this->offsetUnset($key); 153 | return true; 154 | } 155 | 156 | /** 157 | * Iterator rewind 158 | * 159 | * @return void 160 | */ 161 | public function rewind() 162 | { 163 | reset($this->values); 164 | } 165 | 166 | /** 167 | * Iterator key 168 | * 169 | * @return string dn of currently pointed at entry 170 | */ 171 | public function key() 172 | { 173 | if ($this->valid()) { 174 | return key($this->values); 175 | } 176 | return null; 177 | } 178 | 179 | /** 180 | * Iterator current 181 | * 182 | * @return Node or false 183 | */ 184 | public function current() 185 | { 186 | if ($this->valid()) { 187 | return current($this->values); 188 | } 189 | return false; 190 | } 191 | 192 | /** 193 | * Iterator next 194 | * 195 | * @return void 196 | */ 197 | public function next() 198 | { 199 | next($this->values); 200 | } 201 | 202 | /** 203 | * Iterator valid 204 | * 205 | * @return boolean 206 | */ 207 | public function valid() 208 | { 209 | return (null !== key($this->values)); 210 | } 211 | 212 | /** 213 | * Count implementation 214 | * 215 | * @return int number of values stored 216 | */ 217 | public function count() 218 | { 219 | return count($this->values); 220 | } 221 | 222 | /** 223 | * Arrayaccess setter 224 | * 225 | * @param int $offset Offset (index) 226 | * @param mixed $value Value to add as an instance 227 | * 228 | * @return void 229 | */ 230 | public function offsetSet($offset, $value) { 231 | if (is_null($offset)) { 232 | $this->values[] = $value; 233 | } else { 234 | $this->values[$offset] = $value; 235 | } 236 | $this->tracker->logAddition($value); 237 | } 238 | 239 | /** 240 | * Arrayaccess exists 241 | * 242 | * @param int $offset Offset (index) 243 | * 244 | * @return boolean 245 | */ 246 | public function offsetExists($offset) { 247 | return isset($this->values[$offset]); 248 | } 249 | 250 | /** 251 | * Arrayaccess remove 252 | * 253 | * @param int $offset Offset (index) 254 | * 255 | * @return void 256 | */ 257 | public function offsetUnset($offset) { 258 | if (! $this->offsetExists($offset)) { 259 | return; 260 | } 261 | $value = $this->offsetGet($offset); 262 | unset($this->values[$offset]); 263 | $this->tracker->logDeletion($value); 264 | } 265 | 266 | /** 267 | * Arrayaccess retrieve 268 | * 269 | * @param int $offset Offset (index) 270 | * 271 | * @return boolean 272 | */ 273 | public function offsetGet($offset) { 274 | return ($this->offsetExists($offset)) ? $this->values[$offset] : null; 275 | } 276 | 277 | /** 278 | * Retrieves diff additions for the attribute 279 | * 280 | * @return array Added values 281 | */ 282 | public function getDiffAdditions() 283 | { 284 | return $this->tracker->getAdditions(); 285 | } 286 | 287 | /** 288 | * Retrieves diff deletions for the attribute 289 | * 290 | * @return array Deleted values 291 | */ 292 | public function getDiffDeletions() 293 | { 294 | return $this->tracker->getDeletions(); 295 | } 296 | 297 | /** 298 | * Retrieves diff replacements for the attribute 299 | * 300 | * @return array Replaced values 301 | */ 302 | public function getDiffReplacements() 303 | { 304 | return $this->tracker->getReplacements(); 305 | } 306 | 307 | /** 308 | * Snapshot resets diff tracking 309 | * 310 | * @return void 311 | */ 312 | public function snapshot() 313 | { 314 | $this->tracker->reset(); 315 | } 316 | } -------------------------------------------------------------------------------- /Core/SearchResult.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Core; 13 | 14 | use Toyota\Component\Ldap\API\SearchInterface; 15 | use Toyota\Component\Ldap\Core\Node; 16 | 17 | /** 18 | * Class to handle Ldap queries 19 | * 20 | * @author Cyril Cottet 21 | */ 22 | class SearchResult implements \Iterator 23 | { 24 | 25 | protected $search = null; 26 | 27 | protected $current = null; 28 | 29 | /** 30 | * Setter for search 31 | * 32 | * @param SearchInterface $search Backend search result set 33 | * 34 | * @return void 35 | */ 36 | public function setSearch(SearchInterface $search) 37 | { 38 | if (null !== $this->search) { 39 | $this->search->free(); 40 | } 41 | $this->search = $search; 42 | $this->rewind(); 43 | } 44 | 45 | /** 46 | * Iterator rewind 47 | * 48 | * @return void 49 | */ 50 | public function rewind() 51 | { 52 | $this->current = null; 53 | if (null !== $this->search) { 54 | $this->search->reset(); 55 | $this->next(); 56 | } 57 | } 58 | 59 | /** 60 | * Iterator key 61 | * 62 | * @return string dn of currently pointed at entry 63 | */ 64 | public function key() 65 | { 66 | if ($this->valid()) { 67 | return $this->current->getDn(); 68 | } 69 | return; 70 | } 71 | 72 | /** 73 | * Iterator current 74 | * 75 | * @return Node or false 76 | */ 77 | public function current() 78 | { 79 | if ($this->valid()) { 80 | return $this->current; 81 | } 82 | return false; 83 | } 84 | 85 | /** 86 | * Iterator next 87 | * 88 | * @return void 89 | */ 90 | public function next() 91 | { 92 | $this->current = null; 93 | if (null !== $this->search) { 94 | $entry = $this->search->next(); 95 | if (null !== $entry) { 96 | $this->current = new Node(); 97 | $this->current->hydrateFromEntry($entry); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Iterator valid 104 | * 105 | * @return boolean 106 | */ 107 | public function valid() 108 | { 109 | return (null !== $this->current); 110 | } 111 | } -------------------------------------------------------------------------------- /Exception/BindException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap Bind Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class BindException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/ConnectionException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap Connection Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class ConnectionException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/DeleteException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap Delete Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class DeleteException extends PersistenceException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/LdapException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Basic Ldap Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | abstract class LdapException extends \Exception 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/MalformedFilterException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap search malformed filter Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class MalformedFilterException extends SearchException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/NoResultException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap search no result Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class NoResultException extends SearchException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/NodeNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap node search exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class NodeNotFoundException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/NotBoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap not bound exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class NotBoundException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/OptionException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap Option Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class OptionException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/PersistenceException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap Persistence Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class PersistenceException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/RebaseException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap Node rebase Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class RebaseException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/SaveException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap node saving exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class SaveException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/SearchException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap search Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class SearchException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/SizeLimitException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap search size limit Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class SizeLimitException extends SearchException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /Exception/UnsupportedDriverException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Exception; 13 | 14 | /** 15 | * Ldap Driver Exception 16 | * 17 | * @author Cyril Cottet 18 | */ 19 | class UnsupportedDriverException extends LdapException 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Toyota Industrial Equipment SA 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Platform/Native/Connection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Native; 13 | 14 | use Toyota\Component\Ldap\Exception\ConnectionException; 15 | use Toyota\Component\Ldap\Exception\OptionException; 16 | use Toyota\Component\Ldap\Exception\BindException; 17 | use Toyota\Component\Ldap\Exception\PersistenceException; 18 | use Toyota\Component\Ldap\Exception\NoResultException; 19 | use Toyota\Component\Ldap\Exception\SizeLimitException; 20 | use Toyota\Component\Ldap\Exception\MalformedFilterException; 21 | use Toyota\Component\Ldap\Exception\SearchException; 22 | use Toyota\Component\Ldap\API\SearchInterface; 23 | use Toyota\Component\Ldap\API\ConnectionInterface; 24 | 25 | /** 26 | * Connection implementing interface for php ldap native extension 27 | * 28 | * @author Cyril Cottet 29 | */ 30 | class Connection implements ConnectionInterface 31 | { 32 | 33 | protected $connection = null; 34 | 35 | /** 36 | * Default constructor for native connection 37 | * 38 | * @param resource $id Link resource identifier for Ldap Connection 39 | * 40 | * @return Connection 41 | */ 42 | public function __construct($id) 43 | { 44 | $this->connection = $id; 45 | } 46 | 47 | /** 48 | * Set an option 49 | * 50 | * @param int $option Ldap option name 51 | * @param mixed $value Value to set on Ldap option 52 | * 53 | * @return void 54 | * 55 | * @throws OptionException if option cannot be set 56 | */ 57 | public function setOption($option, $value) 58 | { 59 | if (! (@ldap_set_option($this->connection, $option, $value))) { 60 | $code = @ldap_errno($this->connection); 61 | throw new OptionException( 62 | sprintf( 63 | 'Could not change option %s value: Ldap Error Code=%s - %s', 64 | $code, 65 | ldap_err2str($code) 66 | ) 67 | ); 68 | } 69 | } 70 | 71 | /** 72 | * Gets current value set for an option 73 | * 74 | * @param int $option Ldap option name 75 | * 76 | * @return mixed value set for the option 77 | * 78 | * @throws OptionException if option cannot be retrieved 79 | */ 80 | public function getOption($option) 81 | { 82 | $value = null; 83 | if (! (@ldap_get_option($this->connection, $option, $value))) { 84 | $code = @ldap_errno($this->connection); 85 | throw new OptionException( 86 | sprintf( 87 | 'Could not retrieve option %s value: Ldap Error Code=%s - %s', 88 | $code, 89 | ldap_err2str($code) 90 | ) 91 | ); 92 | } 93 | return $value; 94 | } 95 | 96 | /** 97 | * Binds to the LDAP directory with specified RDN and password 98 | * 99 | * @param string $rdn Rdn to use for binding (Default: null) 100 | * @param string $password Plain or hashed password for binding (Default: null) 101 | * 102 | * @return void 103 | * 104 | * @throws BindException if binding fails 105 | */ 106 | public function bind($rdn = null, $password = null) 107 | { 108 | $isAnonymous = false; 109 | if ((null === $rdn) || (null === $password)) { 110 | if ((null !== $rdn) || (null !== $password)) { 111 | throw new BindException( 112 | 'For an anonymous binding, both rdn & passwords have to be null' 113 | ); 114 | } 115 | $isAnonymous = true; 116 | } 117 | 118 | if (! (@ldap_bind($this->connection, $rdn, $password))) { 119 | $code = @ldap_errno($this->connection); 120 | throw new BindException( 121 | sprintf( 122 | 'Could not bind %s user: Ldap Error Code=%s - %s', 123 | $isAnonymous?'anonymous':'privileged', 124 | $code, 125 | ldap_err2str($code) 126 | ) 127 | ); 128 | } 129 | } 130 | 131 | /** 132 | * Closes the connection 133 | * 134 | * @return void 135 | * 136 | * @throws ConnectionException if connection could not be closed 137 | */ 138 | public function close() 139 | { 140 | if (! (@ldap_unbind($this->connection))) { 141 | $code = @ldap_errno($this->connection); 142 | throw new ConnectionException( 143 | sprintf( 144 | 'Could not close the connection: Ldap Error Code=%s - %s', 145 | $code, 146 | ldap_err2str($code) 147 | ) 148 | ); 149 | } 150 | 151 | $this->connection = null; 152 | } 153 | 154 | /** 155 | * Adds a Ldap entry 156 | * 157 | * @param string $dn Distinguished name to register entry for 158 | * @param array $data Ldap attributes to save along with the entry 159 | * 160 | * @return void 161 | * 162 | * @throws PersistenceException if entry could not be added 163 | */ 164 | public function addEntry($dn, $data) 165 | { 166 | $data = $this->normalizeData($data); 167 | 168 | if (! (@ldap_add($this->connection, $dn, $data))) { 169 | $code = @ldap_errno($this->connection); 170 | throw new PersistenceException( 171 | sprintf( 172 | 'Could not add entry %s: Ldap Error Code=%s - %s', 173 | $dn, 174 | $code, 175 | ldap_err2str($code) 176 | ) 177 | ); 178 | } 179 | } 180 | 181 | /** 182 | * Deletes an existing Ldap entry 183 | * 184 | * @param string $dn Distinguished name of the entry to delete 185 | * 186 | * @return void 187 | * 188 | * @throws PersistenceException if entry could not be deleted 189 | */ 190 | public function deleteEntry($dn) 191 | { 192 | if (! (@ldap_delete($this->connection, $dn))) { 193 | $code = @ldap_errno($this->connection); 194 | throw new PersistenceException( 195 | sprintf( 196 | 'Could not delete entry %s: Ldap Error Code=%s - %s', 197 | $dn, 198 | $code, 199 | ldap_err2str($code) 200 | ) 201 | ); 202 | } 203 | } 204 | 205 | /** 206 | * Adds some value(s) to some entry attribute(s) 207 | * 208 | * The data format for attributes is as follows: 209 | * array( 210 | * 'attribute_1' => array( 211 | * 'value_1', 212 | * 'value_2' 213 | * ), 214 | * 'attribute_2' => array( 215 | * 'value_1', 216 | * 'value_2' 217 | * ), 218 | * ... 219 | * ); 220 | * 221 | * @param string $dn Distinguished name of the entry to modify 222 | * @param array $data Values to be added for each attribute 223 | * 224 | * @return void 225 | * 226 | * @throws PersistenceException if entry could not be updated 227 | */ 228 | public function addAttributeValues($dn, $data) 229 | { 230 | $data = $this->normalizeData($data); 231 | 232 | if (! (@ldap_mod_add($this->connection, $dn, $data))) { 233 | $code = @ldap_errno($this->connection); 234 | throw new PersistenceException( 235 | sprintf( 236 | 'Could not add attribute values for entry %s: Ldap Error Code=%s - %s', 237 | $dn, 238 | $code, 239 | ldap_err2str($code) 240 | ) 241 | ); 242 | } 243 | } 244 | 245 | /** 246 | * Replaces value(s) for some entry attribute(s) 247 | * 248 | * The data format for attributes is as follows: 249 | * array( 250 | * 'attribute_1' => array( 251 | * 'value_1', 252 | * 'value_2' 253 | * ), 254 | * 'attribute_2' => array( 255 | * 'value_1', 256 | * 'value_2' 257 | * ), 258 | * ... 259 | * ); 260 | * 261 | * @param string $dn Distinguished name of the entry to modify 262 | * @param array $data Values to be set for each attribute 263 | * 264 | * @return void 265 | * 266 | * @throws PersistenceException if entry could not be updated 267 | */ 268 | public function replaceAttributeValues($dn, $data) 269 | { 270 | $data = $this->normalizeData($data); 271 | 272 | if (! (@ldap_mod_replace($this->connection, $dn, $data))) { 273 | $code = @ldap_errno($this->connection); 274 | throw new PersistenceException( 275 | sprintf( 276 | 'Could not replace attribute values for entry %s: Ldap Error Code=%s - %s', 277 | $dn, 278 | $code, 279 | ldap_err2str($code) 280 | ) 281 | ); 282 | } 283 | } 284 | 285 | /** 286 | * Delete value(s) for some entry attribute(s) 287 | * 288 | * The data format for attributes is as follows: 289 | * array( 290 | * 'attribute_1' => array( 291 | * 'value_1', 292 | * 'value_2' 293 | * ), 294 | * 'attribute_2' => array( 295 | * 'value_1', 296 | * 'value_2' 297 | * ), 298 | * ... 299 | * ); 300 | * 301 | * @param string $dn Distinguished name of the entry to modify 302 | * @param array $data Values to be removed for each attribute 303 | * 304 | * @return void 305 | * 306 | * @throws PersistenceException if entry could not be updated 307 | */ 308 | public function deleteAttributeValues($dn, $data) 309 | { 310 | $data = $this->normalizeData($data); 311 | 312 | if (! (@ldap_mod_del($this->connection, $dn, $data))) { 313 | $code = @ldap_errno($this->connection); 314 | throw new PersistenceException( 315 | sprintf( 316 | 'Could not delete attribute values for entry %s: Ldap Error Code=%s - %s', 317 | $dn, 318 | $code, 319 | ldap_err2str($code) 320 | ) 321 | ); 322 | } 323 | } 324 | 325 | /** 326 | * Searches for entries in the directory 327 | * 328 | * @param int $scope Search scope (ALL, ONE or BASE) 329 | * @param string $baseDn Base distinguished name to look below 330 | * @param string $filter Filter for the search 331 | * @param array $attributes Names of attributes to retrieve (Default: All) 332 | * 333 | * @return SearchInterface Search result set 334 | * 335 | * @throws NoResultException if no result can be retrieved 336 | * @throws SizeLimitException if size limit got exceeded 337 | * @throws MalformedFilterException if filter is wrongly formatted 338 | * @throws SearchException if search failed otherwise 339 | */ 340 | public function search($scope, $baseDn, $filter, $attributes = null) 341 | { 342 | switch ($scope) { 343 | case SearchInterface::SCOPE_BASE: 344 | $function = 'ldap_read'; 345 | break; 346 | case SearchInterface::SCOPE_ONE: 347 | $function = 'ldap_list'; 348 | break; 349 | case SearchInterface::SCOPE_ALL: 350 | $function = 'ldap_search'; 351 | break; 352 | default: 353 | throw new SearchException(sprintf('Scope %s not supported', $scope)); 354 | } 355 | 356 | $params = array($this->connection, $baseDn, $filter); 357 | if (is_array($attributes)) { 358 | $params[] = $attributes; 359 | } 360 | 361 | if (false === ($search = @call_user_func_array($function, $params))) { 362 | $code = @ldap_errno($this->connection); 363 | switch ($code) { 364 | 365 | case 32: 366 | throw new NoResultException('No result retrieved for the given search'); 367 | break; 368 | case 4: 369 | throw new SizeLimitException( 370 | 'Size limit reached while performing the expected search' 371 | ); 372 | break; 373 | case 87: 374 | throw new MalformedFilterException( 375 | sprintf('Search for filter %s fails for a malformed filter', $filter) 376 | ); 377 | break; 378 | default: 379 | throw new SearchException( 380 | sprintf( 381 | 'Search on %s with filter %s failed. Ldap Error Code:%s - %s', 382 | $baseDn, 383 | $filter, 384 | $code, 385 | ldap_err2str($code) 386 | ) 387 | ); 388 | } 389 | } 390 | 391 | return new Search($this->connection, $search); 392 | } 393 | 394 | /** 395 | * Normalizes data for Ldap storage 396 | * 397 | * @param array $data Ldap data to store 398 | * 399 | * @return array Normalized data 400 | */ 401 | protected function normalizeData($data) 402 | { 403 | foreach ($data as $attribute => $info) { 404 | if (is_array($info)) { 405 | if (count($info) == 1) { 406 | $data[$attribute] = $info[0]; 407 | continue; 408 | } 409 | } 410 | } 411 | return $data; 412 | } 413 | } -------------------------------------------------------------------------------- /Platform/Native/Driver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Native; 13 | 14 | use Toyota\Component\Ldap\Exception\ConnectionException; 15 | use Toyota\Component\Ldap\API\DriverInterface; 16 | 17 | /** 18 | * Driver implementing interface for php ldap native extension 19 | * 20 | * @author Cyril Cottet 21 | */ 22 | class Driver implements DriverInterface 23 | { 24 | /** 25 | * Connects to a Ldap directory without binding 26 | * 27 | * @param string $hostname Hostname to connect to 28 | * @param int $port Port to connect to (Default: 389) 29 | * @param boolean $withSSL Whether to connect with SSL support (Default: false) 30 | * @param boolean $withTLS Whether to connect with TLS support (Default: false) 31 | * 32 | * @return ConnectionInterface connection instance 33 | * 34 | * @throws ConnectionException if connection fails 35 | */ 36 | public function connect( 37 | $hostname, 38 | $port = 389, 39 | $withSSL = false, 40 | $withTLS = false 41 | ) { 42 | if ($withSSL && $withTLS) { 43 | throw new ConnectionException('Cannot support both TLS & SSL for a given Ldap Connection'); 44 | } 45 | 46 | if (! extension_loaded('ldap') && ! @dl('ldap.' . PHP_SHLIB_SUFFIX)) { 47 | throw new ConnectionException( 48 | 'You do not have the required ldap-extension installed' 49 | ); 50 | } 51 | 52 | if ($withSSL) { 53 | $hostname = 'ldaps://' . $hostname; 54 | } 55 | 56 | $connection = @ldap_connect($hostname, $port); 57 | if (false === $connection) { 58 | throw new ConnectionException('Could not successfully connect to the LDAP server'); 59 | } 60 | 61 | if ($withTLS) { 62 | if (! (@ldap_start_tls($connection))) { 63 | $code = @ldap_errno($connection); 64 | throw new ConnectionException( 65 | sprintf('Could not start TLS: Ldap Error Code=%s - %s', $code, ldap_err2str($code)) 66 | ); 67 | } 68 | } 69 | 70 | return new Connection($connection); 71 | } 72 | } -------------------------------------------------------------------------------- /Platform/Native/Entry.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Native; 13 | 14 | use Toyota\Component\Ldap\API\EntryInterface; 15 | 16 | /** 17 | * Implementation of the entry interface for php ldap extension 18 | * 19 | * @author Cyril Cottet 20 | */ 21 | class Entry implements EntryInterface 22 | { 23 | 24 | protected $entry = null; 25 | 26 | protected $connection = null; 27 | 28 | /** 29 | * Default constructor 30 | * 31 | * @param resource $connection Resource link identifier for Ldap connection 32 | * @param resource $entry Resource link identifier for Ldap entry 33 | * 34 | * @return Entry 35 | */ 36 | public function __construct($connection, $entry) 37 | { 38 | $this->connection = $connection; 39 | $this->entry = $entry; 40 | } 41 | 42 | /** 43 | * Retrieves entry distinguished name 44 | * 45 | * @return string Distinguished name 46 | */ 47 | public function getDn() 48 | { 49 | return @ldap_get_dn($this->connection, $this->entry); 50 | } 51 | 52 | /** 53 | * Retrieves entry attributes 54 | * 55 | * @return array(attribute => array(values)) 56 | */ 57 | public function getAttributes() 58 | { 59 | $data = @ldap_get_attributes($this->connection, $this->entry); 60 | 61 | $result = array(); 62 | 63 | for ($i = 0; $i < $data['count']; $i++) { 64 | $key = $data[$i]; 65 | $result[$key] = array(); 66 | for ($j = 0; $j < $data[$key]['count']; $j++) { 67 | $result[$key][] = $data[$key][$j]; 68 | } 69 | } 70 | 71 | return $result; 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /Platform/Native/Search.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Native; 13 | 14 | use Toyota\Component\Ldap\API\SearchInterface; 15 | 16 | /** 17 | * Implementation of the search interface for php ldap extension 18 | * 19 | * @author Cyril Cottet 20 | */ 21 | class Search implements SearchInterface 22 | { 23 | 24 | protected $resultSet = null; 25 | 26 | protected $connection = null; 27 | 28 | protected $previous = null; 29 | 30 | protected $isEndReached = false; 31 | 32 | /** 33 | * Default constructor 34 | * 35 | * @param resource $connection Resource link identifier for Ldap connection 36 | * @param resource $set Resource link identifier for Ldap search result set 37 | * 38 | * @return Search 39 | */ 40 | public function __construct($connection, $set) 41 | { 42 | $this->connection = $connection; 43 | $this->resultSet = $set; 44 | $this->previous = null; 45 | $this->isEndReached = false; 46 | } 47 | 48 | /** 49 | * Retrieves next available entry from the search result set 50 | * 51 | * @return EntryInterface next entry if available, null otherwise 52 | */ 53 | public function next() 54 | { 55 | if ($this->isEndReached) { 56 | return null; 57 | } 58 | if (null === $this->previous) { 59 | $this->previous = @ldap_first_entry($this->connection, $this->resultSet); 60 | } else { 61 | $this->previous = @ldap_next_entry($this->connection, $this->previous); 62 | } 63 | if (false === $this->previous) { 64 | $this->previous = null; 65 | $this->isEndReached = true; 66 | return null; 67 | } 68 | return new Entry($this->connection, $this->previous); 69 | } 70 | 71 | /** 72 | * Resets entry iterator 73 | * 74 | * @return void 75 | */ 76 | public function reset() 77 | { 78 | $this->previous = null; 79 | $this->isEndReached = false; 80 | } 81 | 82 | /** 83 | * Frees memory for current result set 84 | * 85 | * @return void 86 | */ 87 | public function free() 88 | { 89 | @ldap_free_result($this->resultSet); 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /Platform/Test/Connection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Test; 13 | 14 | use Toyota\Component\Ldap\Exception\ConnectionException; 15 | use Toyota\Component\Ldap\Exception\OptionException; 16 | use Toyota\Component\Ldap\Exception\BindException; 17 | use Toyota\Component\Ldap\Exception\PersistenceException; 18 | use Toyota\Component\Ldap\Exception\NoResultException; 19 | use Toyota\Component\Ldap\Exception\SizeLimitException; 20 | use Toyota\Component\Ldap\Exception\MalformedFilterException; 21 | use Toyota\Component\Ldap\Exception\SearchException; 22 | use Toyota\Component\Ldap\API\ConnectionInterface; 23 | 24 | /** 25 | * Connection implementing interface for test 26 | * 27 | * @author Cyril Cottet 28 | */ 29 | class Connection implements ConnectionInterface 30 | { 31 | const ERR_NO_RESULT = 1; 32 | const ERR_SIZE_LIMIT = 2; 33 | const ERR_MALFORMED_FILTER = 3; 34 | const ERR_DEFAULT = 9; 35 | 36 | const FAIL_COND_SEARCH = 1; 37 | const FAIL_COND_PERSIST = 2; 38 | const FAIL_COND_NONE = 9; 39 | 40 | protected $failure = null; 41 | 42 | protected $logs = array(); 43 | 44 | protected $stack = array(); 45 | 46 | protected $options = array(); 47 | 48 | protected $bindDn; 49 | 50 | protected $bindPassword; 51 | 52 | protected $isBound = false; 53 | 54 | /** 55 | * Set an option 56 | * 57 | * @param int $option Ldap option name 58 | * @param mixed $value Value to set on Ldap option 59 | * 60 | * @return void 61 | * 62 | * @throws OptionException if option cannot be set 63 | */ 64 | public function setOption($option, $value) 65 | { 66 | if ($this->popFailure()) { 67 | throw new OptionException('could not set option'); 68 | } 69 | $this->options[$option] = $value; 70 | } 71 | 72 | /** 73 | * Gets current value set for an option 74 | * 75 | * @param int $option Ldap option name 76 | * 77 | * @return mixed value set for the option 78 | * 79 | * @throws OptionException if option cannot be retrieved 80 | */ 81 | public function getOption($option) 82 | { 83 | if ($this->popFailure()) { 84 | throw new OptionException('could not retrieve option'); 85 | } 86 | return $this->options[$option]; 87 | } 88 | 89 | /** 90 | * Binds to the LDAP directory with specified RDN and password 91 | * 92 | * @param string $rdn Rdn to use for binding (Default: null) 93 | * @param string $password Plain or hashed password for binding (Default: null) 94 | * 95 | * @return void 96 | * 97 | * @throws BindException if binding fails 98 | */ 99 | public function bind($rdn = null, $password = null) 100 | { 101 | if ($this->popFailure()) { 102 | throw new BindException('could not bind user'); 103 | } 104 | 105 | if ((null === $rdn) || (null === $password)) { 106 | if ((null !== $rdn) || (null !== $password)) { 107 | throw new BindException( 108 | 'For an anonymous binding, both rdn & passwords have to be null' 109 | ); 110 | } 111 | } 112 | 113 | $this->bindDn = $rdn; 114 | $this->bindPassword = $password; 115 | $this->isBound = true; 116 | } 117 | 118 | /** 119 | * Closes the connection 120 | * 121 | * @return void 122 | * 123 | * @throws ConnectionException if connection could not be closed 124 | */ 125 | public function close() 126 | { 127 | if ($this->popFailure()) { 128 | throw new BindException('could not unbind user'); 129 | } 130 | 131 | $this->bindDn = null; 132 | $this->bindPassword = null; 133 | $this->isBound = false; 134 | } 135 | 136 | /** 137 | * Checks if user is bound with the connection 138 | * 139 | * @return boolean 140 | */ 141 | public function isBound() 142 | { 143 | return $this->isBound; 144 | } 145 | 146 | /** 147 | * Retrieve bound user dn 148 | * 149 | * @return string 150 | */ 151 | public function getBindDn() 152 | { 153 | return $this->bindDn; 154 | } 155 | 156 | /** 157 | * Retrieve bound user password 158 | * 159 | * @return string 160 | */ 161 | public function getBindPassword() 162 | { 163 | return $this->bindPassword; 164 | } 165 | 166 | /** 167 | * Adds a Ldap entry 168 | * 169 | * @param string $dn Distinguished name to register entry for 170 | * @param array $data Ldap attributes to save along with the entry 171 | * 172 | * @return void 173 | * 174 | * @throws PersistenceException if entry could not be added 175 | */ 176 | public function addEntry($dn, $data) 177 | { 178 | if ($this->popFailure(self::FAIL_COND_PERSIST)) { 179 | throw new PersistenceException('could not add entry'); 180 | } 181 | 182 | $this->logPersistence('create', $dn, $data); 183 | } 184 | 185 | /** 186 | * Deletes an existing Ldap entry 187 | * 188 | * @param string $dn Distinguished name of the entry to delete 189 | * 190 | * @return void 191 | * 192 | * @throws PersistenceException if entry could not be deleted 193 | */ 194 | public function deleteEntry($dn) 195 | { 196 | if ($this->popFailure(self::FAIL_COND_PERSIST)) { 197 | throw new PersistenceException('could not delete entry'); 198 | } 199 | $this->logPersistence('delete', $dn); 200 | } 201 | 202 | /** 203 | * Adds some value(s) to some entry attribute(s) 204 | * 205 | * The data format for attributes is as follows: 206 | * array( 207 | * 'attribute_1' => array( 208 | * 'value_1', 209 | * 'value_2' 210 | * ), 211 | * 'attribute_2' => array( 212 | * 'value_1', 213 | * 'value_2' 214 | * ), 215 | * ... 216 | * ); 217 | * 218 | * @param string $dn Distinguished name of the entry to modify 219 | * @param array $data Values to be added for each attribute 220 | * 221 | * @return void 222 | * 223 | * @throws PersistenceException if entry could not be updated 224 | */ 225 | public function addAttributeValues($dn, $data) 226 | { 227 | if ($this->popFailure(self::FAIL_COND_PERSIST)) { 228 | throw new PersistenceException('could not add attributes'); 229 | } 230 | $this->logPersistence('attr_add', $dn, $data); 231 | } 232 | 233 | /** 234 | * Replaces value(s) for some entry attribute(s) 235 | * 236 | * The data format for attributes is as follows: 237 | * array( 238 | * 'attribute_1' => array( 239 | * 'value_1', 240 | * 'value_2' 241 | * ), 242 | * 'attribute_2' => array( 243 | * 'value_1', 244 | * 'value_2' 245 | * ), 246 | * ... 247 | * ); 248 | * 249 | * @param string $dn Distinguished name of the entry to modify 250 | * @param array $data Values to be set for each attribute 251 | * 252 | * @return void 253 | * 254 | * @throws PersistenceException if entry could not be updated 255 | */ 256 | public function replaceAttributeValues($dn, $data) 257 | { 258 | if ($this->popFailure(self::FAIL_COND_PERSIST)) { 259 | throw new PersistenceException('could not replace attributes'); 260 | } 261 | $this->logPersistence('attr_rep', $dn, $data); 262 | } 263 | 264 | /** 265 | * Delete value(s) for some entry attribute(s) 266 | * 267 | * The data format for attributes is as follows: 268 | * array( 269 | * 'attribute_1' => array( 270 | * 'value_1', 271 | * 'value_2' 272 | * ), 273 | * 'attribute_2' => array( 274 | * 'value_1', 275 | * 'value_2' 276 | * ), 277 | * ... 278 | * ); 279 | * 280 | * @param string $dn Distinguished name of the entry to modify 281 | * @param array $data Values to be removed for each attribute 282 | * 283 | * @return void 284 | * 285 | * @throws PersistenceException if entry could not be updated 286 | */ 287 | public function deleteAttributeValues($dn, $data) 288 | { 289 | if ($this->popFailure(self::FAIL_COND_PERSIST)) { 290 | throw new PersistenceException('could not delete attributes'); 291 | } 292 | $this->logPersistence('attr_del', $dn, $data); 293 | } 294 | 295 | /** 296 | * Searches for entries in the directory 297 | * 298 | * @param int $scope Search scope (ALL, ONE or BASE) 299 | * @param string $baseDn Base distinguished name to look below 300 | * @param string $filter Filter for the search 301 | * @param array $attributes Names of attributes to retrieve (Default: All) 302 | * 303 | * @return SearchInterface Search result set 304 | * 305 | * @throws NoResultException if no result can be retrieved 306 | * @throws SizeLimitException if size limit got exceeded 307 | * @throws MalformedFilterException if filter is wrongly formatted 308 | * @throws SearchException if search failed otherwise 309 | */ 310 | public function search($scope, $baseDn, $filter, $attributes = null) 311 | { 312 | $this->processSearchFailure(); 313 | 314 | $search = new Search(); 315 | $search->setBaseDn($baseDn); 316 | $search->setFilter($filter); 317 | $search->setAttributes($attributes); 318 | $search->setScope($scope); 319 | $search->setEntries($this->shiftResults()); 320 | 321 | $this->logSearch($search); 322 | 323 | return $search; 324 | } 325 | 326 | /** 327 | * Stacks a result set for next searches 328 | * 329 | * @param array(Entry) $entries An array of Ldap entries 330 | * 331 | * @return void 332 | */ 333 | public function stackResults($entries) 334 | { 335 | $this->stack[] = $entries; 336 | } 337 | 338 | /** 339 | * Shifts a set of entries from the top to feed a search result set 340 | * 341 | * @return array(Entry) or null 342 | */ 343 | public function shiftResults() 344 | { 345 | return array_shift($this->stack); 346 | } 347 | 348 | /** 349 | * Sets a failure for the next method calls 350 | * 351 | * @param int $code Code of the failure to trigger (Optional) 352 | * @param int $scope Scope of the failure to trigger (Optional) 353 | * 354 | * @return void 355 | */ 356 | public function setFailure($code = null, $scope = null) 357 | { 358 | $this->failure = array( 359 | 'code' => (null === $code)?self::ERR_DEFAULT:$code, 360 | 'scope' => (null === $scope)?self::FAIL_COND_NONE:$scope 361 | ); 362 | } 363 | 364 | /** 365 | * Is a failure expected for the next method call 366 | * 367 | * @param int $scope Scope of the failure to trigger (Optional) 368 | * 369 | * @return boolean 370 | */ 371 | public function isFailureExpected($scope = null) 372 | { 373 | if ((self::FAIL_COND_NONE !== $this->failure['scope']) 374 | && ($scope != $this->failure['scope'])) { 375 | return false; 376 | } 377 | return is_array($this->failure); 378 | } 379 | 380 | /** 381 | * Shifts next logged action from the log stack 382 | * 383 | * @return array Log data (null if none available) 384 | */ 385 | public function shiftLog() 386 | { 387 | return array_shift($this->logs); 388 | } 389 | 390 | /** 391 | * Shared exception handling method 392 | * 393 | * @return void 394 | * 395 | * @throws NoResultException if no result can be retrieved 396 | * @throws SizeLimitException if size limit got exceeded 397 | * @throws MalformedFilterException if filter is wrongly formatted 398 | * @throws SearchException if search failed otherwise 399 | */ 400 | protected function processSearchFailure() 401 | { 402 | if (! ($code = $this->popFailure(self::FAIL_COND_SEARCH))) { 403 | return; 404 | } 405 | 406 | switch ($code) { 407 | 408 | case self::ERR_NO_RESULT: 409 | throw new NoResultException('No result retrieved for the given search'); 410 | break; 411 | case self::ERR_SIZE_LIMIT: 412 | throw new SizeLimitException('Size limit reached while performing the expected search'); 413 | break; 414 | case self::ERR_MALFORMED_FILTER: 415 | throw new MalformedFilterException('Malformed filter while searching'); 416 | break; 417 | default: 418 | throw new SearchException('Search failed'); 419 | } 420 | } 421 | 422 | /** 423 | * Retrieves a failure code if failure is expected 424 | * 425 | * @param int $scope Failure scope (Optional) 426 | * 427 | * @return int failure code or false if no failure is expected 428 | */ 429 | protected function popFailure($scope = null) 430 | { 431 | if (! $this->isFailureExpected($scope)) { 432 | return false; 433 | } 434 | $code = $this->failure['code']; 435 | $this->failure = null; 436 | return $code; 437 | } 438 | 439 | /** 440 | * Logs a search for tests results analysis 441 | * 442 | * @param Search $search Search to log 443 | * 444 | * @return void 445 | */ 446 | protected function logSearch(Search $search) 447 | { 448 | $this->logs[] = array( 449 | 'type' => 'search', 450 | 'data' => $search 451 | ); 452 | } 453 | 454 | /** 455 | * Logs a persistence action for tests results analysis 456 | * 457 | * @param string $action Name of action logged 458 | * @param string $dn Distinguished name of the entry on which action is performed 459 | * @param array $attributes Attributes used for the action (Optional) 460 | * 461 | * @return void 462 | */ 463 | protected function logPersistence($action, $dn, $attributes = null) 464 | { 465 | $data = array( 466 | 'dn' => $dn 467 | ); 468 | if (null !== $attributes) { 469 | $data['attributes'] = $attributes; 470 | } 471 | $this->logs[] = array( 472 | 'type' => $action, 473 | 'data' => $data 474 | ); 475 | } 476 | 477 | } -------------------------------------------------------------------------------- /Platform/Test/Driver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Test; 13 | 14 | use Toyota\Component\Ldap\Exception\ConnectionException; 15 | use Toyota\Component\Ldap\API\DriverInterface; 16 | 17 | /** 18 | * Driver implementing interface for test purpose 19 | * 20 | * @author Cyril Cottet 21 | */ 22 | class Driver implements DriverInterface 23 | { 24 | protected $failureFlag = false; 25 | 26 | protected $connection; 27 | 28 | protected $hostname; 29 | 30 | protected $port; 31 | 32 | protected $withSSL; 33 | 34 | protected $withTLS; 35 | 36 | /** 37 | * Connects to a Ldap directory without binding 38 | * 39 | * @param string $hostname Hostname to connect to 40 | * @param int $port Port to connect to (Default: 389) 41 | * @param boolean $withSSL Whether to connect with SSL support (Default: false) 42 | * @param boolean $withTLS Whether to connect with TLS support (Default: false) 43 | * 44 | * @return ConnectionInterface connection instance 45 | * 46 | * @throws ConnectionException if connection fails 47 | */ 48 | public function connect( 49 | $hostname, 50 | $port = 389, 51 | $withSSL = false, 52 | $withTLS = false 53 | ) { 54 | if ($this->failureFlag) { 55 | throw new ConnectionException('Cannot connect'); 56 | } 57 | 58 | $this->hostname = $hostname; 59 | $this->port = $port; 60 | $this->withSSL = $withSSL; 61 | $this->withTLS = $withTLS; 62 | $this->connection = new Connection(); 63 | 64 | return $this->connection; 65 | } 66 | 67 | /** 68 | * Sets connection failure flag 69 | * 70 | * @param boolean $enabled True for enabling failure, false otherwise (Default: true) 71 | * 72 | * @return void 73 | */ 74 | public function setFailureFlag($enabled = true) 75 | { 76 | $this->failureFlag = $enabled; 77 | } 78 | 79 | /** 80 | * Accessor for hostname 81 | * 82 | * @return string Hostname 83 | */ 84 | public function getHostname() 85 | { 86 | return $this->hostname; 87 | } 88 | 89 | /** 90 | * Accessor for port 91 | * 92 | * @return int Port 93 | */ 94 | public function getPort() 95 | { 96 | return $this->port; 97 | } 98 | 99 | /** 100 | * Checks if SSL is active 101 | * 102 | * @return boolean 103 | */ 104 | public function hasSSL() 105 | { 106 | return $this->withSSL; 107 | } 108 | 109 | /** 110 | * Checks if TLS is active 111 | * 112 | * @return boolean 113 | */ 114 | public function hasTLS() 115 | { 116 | return $this->withTLS; 117 | } 118 | 119 | /** 120 | * Retrieves built connection 121 | * 122 | * @return Connection 123 | */ 124 | public function getConnection() 125 | { 126 | return $this->connection; 127 | } 128 | } -------------------------------------------------------------------------------- /Platform/Test/Entry.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Test; 13 | 14 | use Toyota\Component\Ldap\API\EntryInterface; 15 | 16 | /** 17 | * Implementation of the entry interface for test 18 | * 19 | * @author Cyril Cottet 20 | */ 21 | class Entry implements EntryInterface 22 | { 23 | 24 | protected $dn; 25 | 26 | protected $data = array(); 27 | 28 | /** 29 | * Default constructor 30 | * 31 | * @param string $dn Dn for the entry 32 | * @param array $data Attributes 33 | * 34 | * @return Entry 35 | */ 36 | public function __construct($dn, $data = array()) 37 | { 38 | $this->dn = $dn; 39 | $this->data = $data; 40 | } 41 | 42 | /** 43 | * Retrieves entry distinguished name 44 | * 45 | * @return string Distinguished name 46 | */ 47 | public function getDn() 48 | { 49 | return $this->dn; 50 | } 51 | 52 | /** 53 | * Retrieves entry attributes 54 | * 55 | * @return array(attribute => array(values)) 56 | */ 57 | public function getAttributes() 58 | { 59 | return $this->data; 60 | } 61 | 62 | /** 63 | * Setter for entry distinguished name 64 | * 65 | * @param string $dn Dn to set 66 | * 67 | * @return void 68 | */ 69 | public function setDn($dn) 70 | { 71 | $this->dn = $dn; 72 | } 73 | 74 | /** 75 | * Setter for entry attributes 76 | * 77 | * @param array $attributes Attributes to set 78 | * 79 | * @return void 80 | */ 81 | public function setAttributes($attributes) 82 | { 83 | $this->data = $attributes; 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /Platform/Test/Search.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Platform\Test; 13 | 14 | use Toyota\Component\Ldap\API\SearchInterface; 15 | 16 | /** 17 | * Implementation of the search interface for test 18 | * 19 | * @author Cyril Cottet 20 | */ 21 | class Search implements SearchInterface 22 | { 23 | protected $baseDn; 24 | 25 | protected $filter; 26 | 27 | protected $attributes; 28 | 29 | protected $scope; 30 | 31 | protected $entries = array(); 32 | 33 | /** 34 | * Retrieves next available entry from the search result set 35 | * 36 | * @return EntryInterface next entry if available, null otherwise 37 | */ 38 | public function next() 39 | { 40 | list($key, $entry) = each($this->entries); 41 | if (false === $entry) { 42 | return null; 43 | } 44 | return $entry; 45 | } 46 | 47 | /** 48 | * Resets entry iterator 49 | * 50 | * @return void 51 | */ 52 | public function reset() 53 | { 54 | reset($this->entries); 55 | } 56 | 57 | /** 58 | * Frees memory for current result set 59 | * 60 | * @return void 61 | */ 62 | public function free() 63 | { 64 | $this->entries = array(); 65 | } 66 | 67 | /** 68 | * Setter for search returned entries 69 | * 70 | * @param array(Entry) $entries Entries to be tight to the search 71 | * 72 | * @return void 73 | */ 74 | public function setEntries($entries) 75 | { 76 | if (null === $entries) { 77 | $entries = array(); 78 | } 79 | $this->entries = $entries; 80 | } 81 | 82 | /** 83 | * Getter for search returned entries 84 | * 85 | * @return array(Entry) 86 | */ 87 | public function getEntries() 88 | { 89 | return $this->entries; 90 | } 91 | 92 | /** 93 | * Setter for base dn 94 | * 95 | * @param string $dn Base dn 96 | * 97 | * @return void 98 | */ 99 | public function setBaseDn($dn) 100 | { 101 | $this->baseDn = $dn; 102 | } 103 | 104 | /** 105 | * Accessor for base dn 106 | * 107 | * @return string base dn 108 | */ 109 | public function getBaseDn() 110 | { 111 | return $this->baseDn; 112 | } 113 | 114 | /** 115 | * Setter for filter 116 | * 117 | * @param string $filter Filter 118 | * 119 | * @return void 120 | */ 121 | public function setFilter($filter) 122 | { 123 | $this->filter = $filter; 124 | } 125 | 126 | /** 127 | * Accessor for filter 128 | * 129 | * @return string filter 130 | */ 131 | public function getFilter() 132 | { 133 | return $this->filter; 134 | } 135 | 136 | /** 137 | * Setter for attributes 138 | * 139 | * @param array $attributes Attributes 140 | * 141 | * @return void 142 | */ 143 | public function setAttributes($attributes) 144 | { 145 | $this->attributes = $attributes; 146 | } 147 | 148 | /** 149 | * Accessor for searched attributes 150 | * 151 | * @return array attributes 152 | */ 153 | public function getAttributes() 154 | { 155 | return $this->attributes; 156 | } 157 | 158 | /** 159 | * Setter for scope 160 | * 161 | * @param int $scope Scope 162 | * 163 | * @return void 164 | */ 165 | public function setScope($scope) 166 | { 167 | $this->scope = $scope; 168 | } 169 | 170 | /** 171 | * Accessor for scope 172 | * 173 | * @return int scope 174 | */ 175 | public function getScope() 176 | { 177 | return $this->scope; 178 | } 179 | 180 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TIESA Ldap Component 2 | ==================== 3 | 4 | Install 5 | ======= 6 | 7 | Installing the component in your project is as simple as installing [Composer](http://getcomposer.org/download/) 8 | if not already done so yet. 9 | 10 | Then, under your project root folder, create a new file called composer.json and paste the following into it: 11 | 12 | { 13 | "require": { 14 | "tiesa/ldap": "dev-master" 15 | } 16 | } 17 | 18 | To select the component names & versions, you shall refer to information found at [Packagist](http://packagist.org) 19 | 20 | Then you are ready to download all the vendor libraries and generate the autoloading configuration file: 21 | 22 | $ php composer.phar install 23 | 24 | From there, loading Ldap component classes as well as any other package supported in your composer.json configuration 25 | is as easy as adding the following code at the top of your script: 26 | 27 | require_once '/path/to/project/vendor/autoload.php'; 28 | 29 | Usage 30 | ===== 31 | 32 | Connecting & Binding 33 | -------------------- 34 | 35 | A typical sequence for connecting to a LDAP involves not only establishing the connection to the server 36 | but also binding a user. This is done as follows: 37 | 38 | 'ldap.example.com', 45 | 'base_dn' => 'dc=example,dc=com' 46 | ); 47 | $manager = new Manager($params, new Driver()); 48 | 49 | $manager->connect(); 50 | 51 | // Anonymous binding 52 | $manager->bind(); 53 | 54 | // Ready for searching & persisting information 55 | 56 | Most of the times, you will have to use a privileged user to bind to the LDAP in order to perform 57 | persistence operations: 58 | 59 | $manager->bind('cn=user,dc=example,dc=com', 'myTopSecretPassword'); 60 | 61 | Connection Parameters 62 | --------------------- 63 | 64 | We have seen a minimal set of parameters in the connection & binding introduction. However, there 65 | are lots of configuration possibilities available: 66 | - hostname: FQDN for the LDAP server 67 | - port: Port to connect to on the LDAP server (default to 389 for regular and 636 for SSL connection) 68 | - security: One of SSL or TLS. When security is not set, the connection will be a plain one 69 | - bind_dn: Default distinguished name to use for binding (in that case, $manager->bind() will not be anonymous anymore) 70 | - bind_password: Default password to use for binding 71 | - options: LDAP options to enable by default for the connection (Refer to Toyota\Component\Ldap\API\ConnectionInterface) 72 | 73 | Error Handling 74 | -------------- 75 | 76 | All Ldap error codes and messages which are usually quite inconvenient to track (and are easily forgotten about) are 77 | handled with convenient exceptions for all LDAP operations. 78 | 79 | Hence, for instance, you can write the following: 80 | 81 | connect(); 87 | } catch (ConnectionException $e) { 88 | // Do something about it 89 | } 90 | 91 | try { 92 | $manager->bind(); 93 | } catch (BindingException $e) { 94 | // Do something about it 95 | } 96 | 97 | // ... 98 | 99 | All exceptions available are found in Toyota\Component\Ldap\Exception namespace 100 | 101 | Search the LDAP 102 | --------------- 103 | 104 | The most basic search as well as the most complex ones are all handled through a unique API. This is the end of the 105 | ldap_read or ldap_list or ldap_search dilemma: 106 | 107 | search(Search::SCOPE_ALL, 'ou=comp,dc=example,dc=com', '(objectclass=*)'); 111 | 112 | // A search result instance is retrieved which provides iteration capability for a convenient use 113 | 114 | foreach ($results as $node) { 115 | echo $node->getDn(); 116 | foreach ($node->getAttributes() as $attribute) { 117 | echo sprintf('%s => %s', $attribute->getName(), implode(',', $attribute->getValues())); 118 | } 119 | } 120 | 121 | SCOPE_ALL will let you search through the whole subtree including the base node with the distinguished name 122 | you gave for the search. Other options are: 123 | - SCOPE_BASE: Will only search for the one node which matches the given distinguished name 124 | - SCOPE_ONE: Will search for nodes just below the one that matches the given distinguished name 125 | 126 | Also for more convenience, the component offers a direct method to retrieve one node when you know its 127 | distinguished name: 128 | 129 | getNode('cn=my,ou=node,dc=example,dc=com'); 131 | 132 | Persist information to the LDAP 133 | ------------------------------- 134 | 135 | Forget about all the ldap_mod_add, ldap_mod_del, ldap_mod_replace, ldap_add and ldap_delete. The only things you'll 136 | need to remember about now are save() and delete(). The component will track all changes you make on a LDAP entry 137 | and will automagically issue the right function calls for just performing those changes in your directory: 138 | 139 | getNode('cn=node,ou=to,ou=update,dc=example,dc=com'); 142 | $node->get('username')->set('test_user'); 143 | $node->get('objectClass')->add('inetOrgPerson'); 144 | $node->get('sn')->set('Doe'); 145 | $node->removeAttribute('whatever'); 146 | 147 | $manager->save($node); 148 | 149 | // Update done 150 | 151 | $node = new Node() 152 | $node->setDn('ou=create',dc=example,dc=com'); 153 | $node->get('objectClass', true)->add(array('top', 'organizationalUnit')); 154 | // The true param creates the attribute on the fly 155 | $node->get('ou', true)->set('create'); 156 | 157 | $manager->save($node); 158 | 159 | // New Ldap entry saved 160 | 161 | $manager->delete($node); 162 | 163 | // Now it's gone 164 | 165 | Resources 166 | ========= 167 | 168 | To run the test suite, clone the project in your working environment from github 169 | Package your own version of autoload.php and phpunit.xml starting from the distributed versions 170 | You are ready to go: 171 | 172 | $ php composer.phar install --dev 173 | $ vendor/bin/phpunit 174 | 175 | About 176 | ===== 177 | 178 | Requirements 179 | ------------ 180 | 181 | - PHP >= 5.3.3 with ldap extension 182 | - Composer 183 | 184 | License 185 | ------- 186 | 187 | TIESA Ldap Component is licensed under the MIT License - see the LICENSE file for details 188 | -------------------------------------------------------------------------------- /Tests/Core/DiffTrackerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Core; 13 | 14 | use Toyota\Component\Ldap\Tests\TestCase; 15 | use Toyota\Component\Ldap\Core\DiffTracker; 16 | 17 | class DiffTrackerTest extends TestCase 18 | { 19 | /** 20 | * Tests basic tracking 21 | * 22 | * @return void 23 | */ 24 | public function testBasicTracking() 25 | { 26 | $tracker = new DiffTracker(); 27 | $this->assertDiff( 28 | $tracker, 29 | array(), 30 | array(), 31 | array(), 32 | false, 33 | 'To begin with, no diff has been tracked yet' 34 | ); 35 | 36 | $tracker->logAddition('test'); 37 | $this->assertDiff( 38 | $tracker, 39 | array('test'), 40 | array(), 41 | array(), 42 | false, 43 | 'Our first addition got tracked' 44 | ); 45 | 46 | $tracker->logDeletion('other'); 47 | $this->assertDiff( 48 | $tracker, 49 | array('test'), 50 | array('other'), 51 | array(), 52 | false, 53 | 'Our first deletion got tracked' 54 | ); 55 | 56 | $tracker->logAddition('new_test'); 57 | $tracker->logDeletion('del_more'); 58 | $this->assertDiff( 59 | $tracker, 60 | array('test', 'new_test'), 61 | array('other', 'del_more'), 62 | array(), 63 | false, 64 | 'Additions & Deletions do pile up in the stacks' 65 | ); 66 | 67 | $tracker->logAddition('test'); 68 | $tracker->logDeletion('other'); 69 | $this->assertDiff( 70 | $tracker, 71 | array('test', 'new_test'), 72 | array('other', 'del_more'), 73 | array(), 74 | false, 75 | 'Diff duplications get ignored' 76 | ); 77 | 78 | $tracker->logDeletion('test'); 79 | $tracker->logAddition('test'); 80 | $this->assertDiff( 81 | $tracker, 82 | array('new_test', 'test'), 83 | array('other', 'del_more'), 84 | array(), 85 | false, 86 | 'Add + Del + Add = Add' 87 | ); 88 | 89 | $tracker->logDeletion('test'); 90 | $this->assertDiff( 91 | $tracker, 92 | array('new_test'), 93 | array('other', 'del_more'), 94 | array(), 95 | false, 96 | 'Add + Del + Add + Del = None' 97 | ); 98 | 99 | $tracker->logAddition('other'); 100 | $tracker->logDeletion('other'); 101 | $this->assertDiff( 102 | $tracker, 103 | array('new_test'), 104 | array('del_more','other'), 105 | array(), 106 | false, 107 | 'Del + Add + Del = Del' 108 | ); 109 | 110 | $tracker->logAddition('other'); 111 | $this->assertDiff( 112 | $tracker, 113 | array('new_test'), 114 | array('del_more'), 115 | array('other'), 116 | false, 117 | 'Del + Add + Del + Add = Rep' 118 | ); 119 | } 120 | 121 | /** 122 | * Tests tracking replacements 123 | * 124 | * @return void 125 | */ 126 | public function testReplacementsTracking() 127 | { 128 | 129 | $tracker = new DiffTracker(); 130 | $tracker->logAddition('test'); 131 | $tracker->logAddition('new_test'); 132 | $tracker->logAddition('++'); 133 | $tracker->logDeletion('del_more'); 134 | $tracker->logDeletion('other'); 135 | $tracker->logDeletion('--'); 136 | 137 | $this->assertDiff( 138 | $tracker, 139 | array('test', 'new_test', '++'), 140 | array('del_more', 'other', '--'), 141 | array(), 142 | false, 143 | 'Starting diff is as expected' 144 | ); 145 | 146 | $tracker->logAddition('other'); 147 | $this->assertDiff( 148 | $tracker, 149 | array('test', 'new_test', '++'), 150 | array('del_more', '--'), 151 | array('other'), 152 | false, 153 | 'Adding a value which was removed results in a replacement' 154 | ); 155 | 156 | $tracker->logAddition('other'); 157 | $this->assertDiff( 158 | $tracker, 159 | array('test', 'new_test', '++'), 160 | array('del_more', '--'), 161 | array('other'), 162 | false, 163 | 'Adding again a replaced item does not make a change' 164 | ); 165 | 166 | $tracker->logDeletion('new_test'); 167 | $this->assertDiff( 168 | $tracker, 169 | array('test', '++'), 170 | array('del_more', '--'), 171 | array('other'), 172 | false, 173 | 'Deleting an added item results in simplification of the diff' 174 | ); 175 | 176 | $tracker->logDeletion('new_test'); 177 | $this->assertDiff( 178 | $tracker, 179 | array('test', '++'), 180 | array('del_more', '--'), 181 | array('other'), 182 | false, 183 | 'Trying to delete again an item which was originally added is not a deletion' 184 | ); 185 | 186 | $tracker->logDeletion('other'); 187 | $this->assertDiff( 188 | $tracker, 189 | array('test', '++'), 190 | array('del_more', '--', 'other'), 191 | array(), 192 | false, 193 | 'Deleting a replaced item is equivalent to deleting the original value' 194 | ); 195 | 196 | $tracker->logDeletion('new_test'); 197 | $this->assertDiff( 198 | $tracker, 199 | array('test', '++'), 200 | array('del_more', '--', 'other'), 201 | array(), 202 | false, 203 | 'The deletion is still not registered until we reset tracking' 204 | ); 205 | 206 | $tracker->reset(); 207 | $this->assertDiff( 208 | $tracker, 209 | array(), 210 | array(), 211 | array(), 212 | false, 213 | 'We are back with an empty diff as expected' 214 | ); 215 | 216 | $tracker->logAddition('other'); 217 | $tracker->logDeletion('new_test'); 218 | $this->assertDiff( 219 | $tracker, 220 | array('other'), 221 | array('new_test'), 222 | array(), 223 | false, 224 | 'Now basic addition & deletion behaviour are restored' 225 | ); 226 | 227 | $tracker->logReplacement('repl1'); 228 | $tracker->logReplacement('repl2'); 229 | $tracker->logReplacement('repl3'); 230 | $tracker->logReplacement('new_test'); 231 | $tracker->logReplacement('other'); 232 | $tracker->logDeletion('repl2'); 233 | $tracker->logAddition('repl1'); 234 | $this->assertDiff( 235 | $tracker, 236 | array(), 237 | array('repl2'), 238 | array('repl1', 'repl3', 'new_test', 'other'), 239 | false, 240 | 'Direct replacements are also tracked as they should' 241 | ); 242 | } 243 | 244 | /** 245 | * Tests object overriding 246 | * 247 | * @return void 248 | */ 249 | public function testOverridesLogging() 250 | { 251 | $tracker = new DiffTracker(); 252 | $tracker->logAddition('added'); 253 | $tracker->logDeletion('deleted'); 254 | $tracker->logReplacement('replaced'); 255 | $this->assertDiff( 256 | $tracker, 257 | array('added'), 258 | array('deleted'), 259 | array('replaced'), 260 | false, 261 | 'We start with basic diff tracking' 262 | ); 263 | 264 | $tracker->markOverridden(); 265 | $this->assertDiff( 266 | $tracker, 267 | array(), 268 | array(), 269 | array(), 270 | true, 271 | 'Marking as overridden changes the flag and resets diff tracking' 272 | ); 273 | $tracker->logAddition('added'); 274 | $tracker->logDeletion('deleted'); 275 | $tracker->logReplacement('replaced'); 276 | $this->assertDiff( 277 | $tracker, 278 | array(), 279 | array(), 280 | array(), 281 | true, 282 | 'An overriden item is not tracked anymore as its final state is the truth' 283 | ); 284 | $tracker->reset(); 285 | $tracker->logAddition('added'); 286 | $tracker->logDeletion('deleted'); 287 | $tracker->logReplacement('replaced'); 288 | $this->assertDiff( 289 | $tracker, 290 | array('added'), 291 | array('deleted'), 292 | array('replaced'), 293 | false, 294 | 'Diff tracking is active again' 295 | ); 296 | } 297 | 298 | /** 299 | * Tests resetting 300 | * 301 | * @return void 302 | */ 303 | public function testReset() 304 | { 305 | $tracker = new DiffTracker(); 306 | $this->assertDiff( 307 | $tracker, 308 | array(), 309 | array(), 310 | array(), 311 | false, 312 | 'To begin with, no diff has been tracked yet' 313 | ); 314 | 315 | $tracker->reset(); 316 | $this->assertDiff( 317 | $tracker, 318 | array(), 319 | array(), 320 | array(), 321 | false, 322 | 'Snapshotting an empty diff does not make a change' 323 | ); 324 | 325 | 326 | $tracker->logAddition('test'); 327 | $tracker->logAddition('new_test'); 328 | $tracker->logAddition('++'); 329 | $tracker->logDeletion('del_more'); 330 | $tracker->logDeletion('other'); 331 | $tracker->logDeletion('--'); 332 | $tracker->logAddition('other'); 333 | $this->assertDiff( 334 | $tracker, 335 | array('test', 'new_test', '++'), 336 | array('del_more', '--'), 337 | array('other'), 338 | false, 339 | 'Diff got tracked correctly' 340 | ); 341 | 342 | $tracker->reset(); 343 | $this->assertDiff( 344 | $tracker, 345 | array(), 346 | array(), 347 | array(), 348 | false, 349 | 'Diff is back to its initial setup' 350 | ); 351 | 352 | $tracker->markOverridden(); 353 | $tracker->logAddition('test'); 354 | $tracker->logAddition('new_test'); 355 | $this->assertDiff( 356 | $tracker, 357 | array(), 358 | array(), 359 | array(), 360 | true, 361 | 'We now have an object overridden with 2 values in its final state not tracked' 362 | ); 363 | 364 | $tracker->reset(); 365 | $this->assertDiff( 366 | $tracker, 367 | array(), 368 | array(), 369 | array(), 370 | false, 371 | 'Even overriding flag is reset' 372 | ); 373 | } 374 | 375 | /** 376 | * Asserts diff is as follows 377 | * 378 | * @param DiffTracker $tracker Tracker being tested 379 | * @param array $additions Expected result for added items 380 | * @param array $deletions Expected result for removed items 381 | * @param array $replacements Expected result for replaced items 382 | * @param boolean $isOverridden Whether the complete object got overriden (Default: false) 383 | * @param string $info Logged message along with subsequent assertions (Optional) 384 | * 385 | * @return void 386 | */ 387 | protected function assertDiff( 388 | DiffTracker $tracker, 389 | array $additions, 390 | array $deletions, 391 | array $replacements, 392 | $isOverridden = false, 393 | $info = null 394 | ) { 395 | $msg = (null === $info)?'':$info.' - '; 396 | 397 | $this->assertEquals( 398 | $additions, 399 | $tracker->getAdditions(), 400 | $msg . 'The expected additions are retrieved' 401 | ); 402 | 403 | $this->assertEquals( 404 | $deletions, 405 | $tracker->getDeletions(), 406 | $msg . 'The expected deletions are retrieved' 407 | ); 408 | 409 | $this->assertEquals( 410 | $replacements, 411 | $tracker->getReplacements(), 412 | $msg . 'The expected replacements are retrieved' 413 | ); 414 | 415 | $this->assertEquals( 416 | $isOverridden, 417 | $tracker->isOverridden(), 418 | sprintf($msg . 'The object %s marked as overriden', $isOverridden?'is':'is not') 419 | ); 420 | } 421 | 422 | } -------------------------------------------------------------------------------- /Tests/Core/Manager/ManagerConnectTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Core\Manager; 13 | 14 | use Toyota\Component\Ldap\Core\Manager; 15 | use Toyota\Component\Ldap\API\ConnectionInterface; 16 | 17 | class ManagerConnectTest extends ManagerTest 18 | { 19 | /** 20 | * Tests error handling for missing parameters at construction 21 | * 22 | * @return void 23 | */ 24 | public function testConstructionErrorHandling() 25 | { 26 | try { 27 | $manager = new Manager(array(), $this->driver); 28 | $this->fail('hostname and base_dn are required parameters'); 29 | } catch (\InvalidArgumentException $e) { 30 | $this->assertRegExp('/hostname/', $e->getMessage()); 31 | $this->assertRegExp('/base_dn/', $e->getMessage()); 32 | $this->assertRegExp('/parameters missing/', $e->getMessage()); 33 | } 34 | 35 | try { 36 | $manager = new Manager(array('hostname' => 'ldap.example.com'), $this->driver); 37 | $this->fail('hostname alone is not enough are required parameters'); 38 | } catch (\InvalidArgumentException $e) { 39 | $this->assertNotRegExp('/hostname/', $e->getMessage()); 40 | $this->assertRegExp('/base_dn/', $e->getMessage()); 41 | $this->assertRegExp('/parameters missing/', $e->getMessage()); 42 | } 43 | 44 | try { 45 | $manager = new Manager(array('base_dn' => 'dc=example,dc=com'), $this->driver); 46 | $this->fail('base_dn alone is not enough are required parameters'); 47 | } catch (\InvalidArgumentException $e) { 48 | $this->assertRegExp('/hostname/', $e->getMessage()); 49 | $this->assertNotRegExp('/base_dn/', $e->getMessage()); 50 | $this->assertRegExp('/parameters missing/', $e->getMessage()); 51 | } 52 | 53 | $params = $this->minimal; 54 | $params['security'] = 'UNSUPPORTED'; 55 | try { 56 | $manager = new Manager($params, $this->driver); 57 | $this->fail('Only TLS or SSL are supported for security setting'); 58 | } catch (\InvalidArgumentException $e) { 59 | $this->assertRegExp('/UNSUPPORTED not supported/', $e->getMessage()); 60 | $this->assertRegExp('/only SSL or TLS/', $e->getMessage()); 61 | } 62 | 63 | $manager = new Manager($this->minimal, $this->driver); 64 | $manager->connect(); 65 | $this->assertInstanceOf( 66 | 'Toyota\Component\Ldap\API\ConnectionInterface', 67 | $this->driver->getConnection(), 68 | 'Connection has been started' 69 | ); 70 | } 71 | 72 | /** 73 | * Tests connection sequence with all parameters 74 | * 75 | * @return void 76 | */ 77 | public function testConnectionSequence() 78 | { 79 | $params = $this->minimal; 80 | $params['port'] = 999; 81 | $params['security'] = 'SSL'; 82 | $params['bind_dn'] = 'cn=admin,dc=example,dc=com'; 83 | $params['bind_password'] = 'secret'; 84 | $options = array( 85 | ConnectionInterface::OPT_REFERRALS => 0, 86 | ConnectionInterface::OPT_TIMELIMIT => 100 87 | ); 88 | $params['options'] = $options; 89 | 90 | $manager = new Manager($params, $this->driver); 91 | $manager->connect(); 92 | 93 | $this->assertEquals('ldap.example.com', $this->driver->getHostname()); 94 | $this->assertEquals(999, $this->driver->getPort()); 95 | $this->assertTrue($this->driver->hasSSL()); 96 | $this->assertFalse($this->driver->hasTLS()); 97 | 98 | $instance = $this->driver->getConnection(); 99 | $this->assertInstanceOf( 100 | 'Toyota\Component\Ldap\API\ConnectionInterface', 101 | $instance, 102 | 'Connection has been started' 103 | ); 104 | $this->assertEquals(0, $instance->getOption(ConnectionInterface::OPT_REFERRALS)); 105 | $this->assertEquals(100, $instance->getOption(ConnectionInterface::OPT_TIMELIMIT)); 106 | 107 | $this->assertFalse($instance->isBound()); 108 | $manager->bind(); 109 | $this->assertTrue($instance->isBound()); 110 | $this->assertEquals('cn=admin,dc=example,dc=com', $instance->getBindDn()); 111 | $this->assertEquals('secret', $instance->getBindPassword()); 112 | } 113 | 114 | /** 115 | * Tests cleaning protocol configuration parameters 116 | * 117 | * @return void 118 | */ 119 | public function testCleaningProtocolParameters() 120 | { 121 | $params = $this->minimal; 122 | $params['hostname'] = 'ldap://ldap.example.com'; 123 | $this->assertConfiguration($params, 'ldap.example.com', 389, false, false); 124 | 125 | $params['security'] = 'SSL'; 126 | $this->assertConfiguration($params, 'ldap.example.com', 636, true, false); 127 | 128 | $params['port'] = 999; 129 | $this->assertConfiguration($params, 'ldap.example.com', 999, true, false); 130 | 131 | $params = $this->minimal; 132 | $params['hostname'] = 'ldaps://ldap.example.com'; 133 | $this->assertConfiguration($params, 'ldap.example.com', 636, true, false); 134 | 135 | $params['port'] = 389; 136 | $this->assertConfiguration($params, 'ldap.example.com', 389, true, false); 137 | 138 | $params['security'] = 'SSL'; 139 | $this->assertConfiguration($params, 'ldap.example.com', 389, true, false); 140 | 141 | $params['security'] = 'TLS'; 142 | $this->assertConfiguration($params, 'ldap.example.com', 389, false, true); 143 | 144 | $params = $this->minimal; 145 | $params['hostname'] = 'ldap://ldap.example.com'; 146 | $params['security'] = 'TLS'; 147 | $this->assertConfiguration($params, 'ldap.example.com', 389, false, true); 148 | 149 | $params['port'] = 999; 150 | $this->assertConfiguration($params, 'ldap.example.com', 999, false, true); 151 | } 152 | 153 | /** 154 | * Tests cleaning binding parameters 155 | * 156 | * @return void 157 | */ 158 | public function testCleaningBindingParameters() 159 | { 160 | $params = $this->minimal; 161 | $this->assertBinding($params, true, true); 162 | 163 | $params['bind_password'] = ''; 164 | $this->assertBinding($params, true, true); 165 | 166 | $params['bind_password'] = 'test'; 167 | $this->assertBinding($params, true, true); 168 | 169 | $params['bind_dn'] = ''; 170 | $this->assertBinding($params, true, true); 171 | 172 | $params['bind_dn'] = 'bind_test'; 173 | $this->assertBinding($params, true, false, 'bind_test', 'test'); 174 | 175 | $params['bind_password'] = ''; 176 | $this->assertBinding($params, true, false, 'bind_test', ''); 177 | 178 | unset($params['bind_password']); 179 | $this->assertBinding($params, true, false, 'bind_test', ''); 180 | } 181 | 182 | /** 183 | * Tests alternative binding 184 | * 185 | * @return void 186 | */ 187 | public function testAlternativeBinding() 188 | { 189 | $params = $this->minimal; 190 | $params['bind_dn'] = 'default_dn'; 191 | $params['bind_password'] = 'default_password'; 192 | $manager = new Manager($params, $this->driver); 193 | $manager->connect(); 194 | 195 | $instance = $this->driver->getConnection(); 196 | 197 | $manager->bind(); 198 | $this->assertTrue($instance->isBound(), 'Binding occured'); 199 | $this->assertEquals( 200 | 'default_dn', 201 | $instance->getBindDn(), 202 | 'Default credential got used' 203 | ); 204 | $this->assertEquals( 205 | 'default_password', 206 | $instance->getBindPassword(), 207 | 'Default credential got used' 208 | ); 209 | 210 | $manager->bind(null, ''); 211 | $this->assertTrue($instance->isBound(), 'Binding occured'); 212 | $this->assertEquals( 213 | 'default_dn', 214 | $instance->getBindDn(), 215 | 'Default credential got used' 216 | ); 217 | $this->assertEquals( 218 | 'default_password', 219 | $instance->getBindPassword(), 220 | 'Default credential got used' 221 | ); 222 | 223 | $manager->bind(null, 'alt_pass'); 224 | $this->assertTrue($instance->isBound(), 'Binding occured'); 225 | $this->assertEquals( 226 | 'default_dn', 227 | $instance->getBindDn(), 228 | 'Default credential got used' 229 | ); 230 | $this->assertEquals( 231 | 'default_password', 232 | $instance->getBindPassword(), 233 | 'Default credential got used' 234 | ); 235 | 236 | $manager->bind('', 'alt_pass'); 237 | $this->assertTrue($instance->isBound(), 'Binding occured'); 238 | $this->assertEquals( 239 | 'default_dn', 240 | $instance->getBindDn(), 241 | 'Default credential got used' 242 | ); 243 | $this->assertEquals( 244 | 'default_password', 245 | $instance->getBindPassword(), 246 | 'Default credential got used' 247 | ); 248 | 249 | $manager->bind('alt_dn', 'alt_pass'); 250 | $this->assertTrue($instance->isBound(), 'Binding occured'); 251 | $this->assertEquals( 252 | 'alt_dn', 253 | $instance->getBindDn(), 254 | 'Now alternative binding occurs' 255 | ); 256 | $this->assertEquals( 257 | 'alt_pass', 258 | $instance->getBindPassword(), 259 | 'Alternative password got used' 260 | ); 261 | 262 | $manager->bind('alt_dn', ''); 263 | $this->assertTrue($instance->isBound(), 'Binding occured'); 264 | $this->assertEquals( 265 | 'alt_dn', 266 | $instance->getBindDn(), 267 | 'Now alternative binding occurs' 268 | ); 269 | $this->assertEquals( 270 | '', 271 | $instance->getBindPassword(), 272 | 'Alternative password got used' 273 | ); 274 | 275 | $manager->bind('alt_dn'); 276 | $this->assertTrue($instance->isBound(), 'Binding occured'); 277 | $this->assertEquals( 278 | 'alt_dn', 279 | $instance->getBindDn(), 280 | 'Now alternative binding occurs' 281 | ); 282 | $this->assertEquals( 283 | '', 284 | $instance->getBindPassword(), 285 | 'Default empty password got used' 286 | ); 287 | } 288 | } -------------------------------------------------------------------------------- /Tests/Core/Manager/ManagerReadTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Core\Manager; 13 | 14 | use Toyota\Component\Ldap\Core\Manager; 15 | use Toyota\Component\Ldap\Core\Node; 16 | use Toyota\Component\Ldap\API\SearchInterface; 17 | use Toyota\Component\Ldap\Platform\Test\Connection; 18 | use Toyota\Component\Ldap\Platform\Test\Entry; 19 | use Toyota\Component\Ldap\Platform\Test\Search; 20 | use Toyota\Component\Ldap\Exception\NotBoundException; 21 | use Toyota\Component\Ldap\Exception\SizeLimitException; 22 | use Toyota\Component\Ldap\Exception\MalformedFilterException; 23 | use Toyota\Component\Ldap\Exception\SearchException; 24 | use Toyota\Component\Ldap\Exception\NodeNotFoundException; 25 | 26 | class ManagerReadTest extends ManagerTest 27 | { 28 | /** 29 | * Tests retrieving node with their Dn 30 | * 31 | * @return void 32 | */ 33 | public function testGetNode() 34 | { 35 | $manager = new Manager($this->minimal, $this->driver); 36 | 37 | // Exception handling 38 | $this->assertBindingFirst($manager, 'getNode', array('test')); 39 | $manager->connect(); 40 | $this->assertBindingFirst($manager, 'getNode', array('test')); 41 | $manager->bind(); 42 | 43 | $this->driver->getConnection()->setFailure(Connection::ERR_SIZE_LIMIT); 44 | try { 45 | $manager->getNode('test'); 46 | $this->fail('Size limit exception should be populated'); 47 | } catch (SizeLimitException $e) { 48 | $this->assertRegExp('/Size limit reached/', $e->getMessage()); 49 | } 50 | 51 | $this->driver->getConnection()->setFailure(Connection::ERR_MALFORMED_FILTER); 52 | try { 53 | $manager->getNode('test'); 54 | $this->fail('Malformed filter exception should be populated'); 55 | } catch (MalformedFilterException $e) { 56 | $this->assertRegExp('/Malformed filter/', $e->getMessage()); 57 | } 58 | 59 | $this->driver->getConnection()->setFailure(Connection::ERR_DEFAULT); 60 | try { 61 | $manager->getNode('test'); 62 | $this->fail('Any other search exception should be populated'); 63 | } catch (SearchException $e) { 64 | $this->assertEquals('Toyota\Component\Ldap\Exception\SearchException', get_class($e)); 65 | $this->assertRegExp('/Search failed/', $e->getMessage()); 66 | } 67 | 68 | // Empty result set handling 69 | $this->driver->getConnection()->setFailure(Connection::ERR_NO_RESULT); 70 | try { 71 | $manager->getNode('test'); 72 | $this->fail('Node cannot be retrieved as search result set is empty'); 73 | } catch (NodeNotFoundException $e) { 74 | $this->assertRegExp('/test not found/', $e->getMessage()); 75 | } 76 | 77 | try { 78 | $node = $manager->getNode('CN=TEST,DC=EXAMPLE,DC=COM'); 79 | $this->fail('No exception get thrown by backend but no entry is retrieved either'); 80 | } catch (NodeNotFoundException $e) { 81 | $this->assertSearchLog( 82 | $this->driver->getConnection()->shiftLog(), 83 | 'CN=TEST,DC=EXAMPLE,DC=COM', 84 | '(objectclass=*)', 85 | Search::SCOPE_BASE 86 | ); 87 | $this->assertRegExp('/CN=TEST,DC=EXAMPLE,DC=COM not found/', $e->getMessage()); 88 | } 89 | 90 | $this->driver->getConnection()->stackResults(null); 91 | try { 92 | $node = $manager->getNode('CN=TEST,DC=EXAMPLE,DC=COM'); 93 | $this->fail('A null entry is not a valid entry'); 94 | } catch (NodeNotFoundException $e) { 95 | $this->assertSearchLog( 96 | $this->driver->getConnection()->shiftLog(), 97 | 'CN=TEST,DC=EXAMPLE,DC=COM', 98 | '(objectclass=*)', 99 | Search::SCOPE_BASE 100 | ); 101 | $this->assertRegExp('/CN=TEST,DC=EXAMPLE,DC=COM not found/', $e->getMessage()); 102 | } 103 | 104 | // Basic search 105 | $entry = new Entry('cn=test,dc=example,dc=com', array('attr' => array('value'))); 106 | $this->driver->getConnection()->stackResults(array($entry)); 107 | 108 | $node = $manager->getNode('CN=TEST,DC=EXAMPLE,DC=COM'); 109 | $this->assertSearchLog( 110 | $this->driver->getConnection()->shiftLog(), 111 | 'CN=TEST,DC=EXAMPLE,DC=COM', 112 | '(objectclass=*)', 113 | Search::SCOPE_BASE, 114 | null, 115 | array($entry) 116 | ); 117 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\Node', $node); 118 | 119 | $this->assertEquals( 120 | 'cn=test,dc=example,dc=com', 121 | $node->getDn(), 122 | 'The right node got retrieved and hydrated' 123 | ); 124 | $this->assertNull( 125 | $this->driver->getConnection()->shiftResults(), 126 | 'Node got pulled from the stack' 127 | ); 128 | 129 | // Alternative parameters search 130 | $entry = new Entry('cn=cyril,dc=example,dc=com', array('attr' => array('value2'))); 131 | $this->driver->getConnection()->stackResults(array($entry)); 132 | 133 | $node = $manager->getNode( 134 | 'CN=OTHER,DC=EXAMPLE,DC=COM', 135 | array('*', '+'), 136 | '(objectclass=other)' 137 | ); 138 | $this->assertSearchLog( 139 | $this->driver->getConnection()->shiftLog(), 140 | 'CN=OTHER,DC=EXAMPLE,DC=COM', 141 | '(objectclass=other)', 142 | Search::SCOPE_BASE, 143 | array('*', '+'), 144 | array($entry) 145 | ); 146 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\Node', $node); 147 | 148 | $this->assertEquals( 149 | 'cn=cyril,dc=example,dc=com', 150 | $node->getDn(), 151 | 'The right node got retrieved and hydrated' 152 | ); 153 | $this->assertNull( 154 | $this->driver->getConnection()->shiftResults(), 155 | 'Node got pulled from the stack' 156 | ); 157 | } 158 | 159 | /** 160 | * Tests querying a Ldap directory 161 | * 162 | * @return void 163 | */ 164 | public function testSearch() 165 | { 166 | $manager = new Manager($this->minimal, $this->driver); 167 | 168 | // Exception handling 169 | $this->assertBindingFirst($manager, 'search'); 170 | $manager->connect(); 171 | $this->assertBindingFirst($manager, 'search'); 172 | $manager->bind(); 173 | 174 | $this->driver->getConnection()->setFailure(Connection::ERR_MALFORMED_FILTER); 175 | try { 176 | $res = $manager->search(); 177 | $this->fail('Filter malformed, query shall fail'); 178 | } catch (MalformedFilterException $e) { 179 | $this->assertRegExp('/Malformed filter/', $e->getMessage()); 180 | } 181 | 182 | // Basic search 183 | $set = array(new Entry('a'), new Entry('b'), new Entry('c')); 184 | $this->driver->getConnection()->stackResults($set); 185 | $result = $manager->search(); 186 | 187 | $this->assertSearchLog( 188 | $this->driver->getConnection()->shiftLog(), 189 | 'dc=example,dc=com', 190 | '(objectclass=*)', 191 | SearchInterface::SCOPE_ALL, 192 | null, 193 | $set 194 | ); 195 | 196 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\SearchResult', $result); 197 | 198 | $data = array(); 199 | foreach ($result as $key => $value) { 200 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\Node', $value); 201 | $data[$key] = $value->getAttributes(); 202 | } 203 | 204 | $this->assertArrayHasKey('a', $data); 205 | $this->assertArrayHasKey('b', $data); 206 | $this->assertArrayHasKey('c', $data); 207 | $this->assertEquals( 208 | 3, 209 | count($data), 210 | 'The right search result got retrieved' 211 | ); 212 | 213 | // Empty result set search 214 | $this->driver->getConnection()->setFailure(Connection::ERR_NO_RESULT); 215 | $this->driver->getConnection()->stackResults($set); 216 | $result = $manager->search(); 217 | $this->assertInstanceOf( 218 | 'Toyota\Component\Ldap\Core\SearchResult', 219 | $result, 220 | 'Query did not fail - Exception got handled' 221 | ); 222 | 223 | $data = array(); 224 | foreach ($result as $key => $value) { 225 | $data[$key] = $value->getAttributes(); 226 | } 227 | $this->assertEquals( 228 | 0, 229 | count($data), 230 | 'The exception got handled and the search result set has not been set in the query' 231 | ); 232 | 233 | // Alternative parameters search 234 | $result = $manager->search( 235 | 'ou=other,dc=example,dc=com', 236 | '(objectclass=test)', 237 | false, 238 | array('attr1', 'attr2') 239 | ); 240 | $this->assertSearchLog( 241 | $this->driver->getConnection()->shiftLog(), 242 | 'ou=other,dc=example,dc=com', 243 | '(objectclass=test)', 244 | SearchInterface::SCOPE_ONE, 245 | array('attr1', 'attr2'), 246 | $set 247 | ); 248 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\SearchResult', $result); 249 | } 250 | 251 | /** 252 | * Retrieve children Ldap nodes 253 | * 254 | * @return void 255 | */ 256 | public function testGetChildren() 257 | { 258 | $manager = new Manager($this->minimal, $this->driver); 259 | 260 | $node = new Node(); 261 | $node->setDn('test_node'); 262 | 263 | $set = array(); 264 | $set[] = new Entry('ent1', array('val1')); 265 | $set[] = new Entry('ent2', array('val2')); 266 | $set[] = new Entry('ent3', array('val3')); 267 | 268 | // Binding exception handling 269 | $this->assertBindingFirst($manager, 'getChildrenNodes', array($node)); 270 | $manager->connect(); 271 | $this->assertBindingFirst($manager, 'getChildrenNodes', array($node)); 272 | $manager->bind(); 273 | 274 | // Basic behaviour 275 | $this->driver->getConnection()->stackResults($set); 276 | 277 | $nodes = $manager->getChildrenNodes($node); 278 | $this->assertSearchLog( 279 | $this->driver->getConnection()->shiftLog(), 280 | 'test_node', 281 | '(objectclass=*)', 282 | SearchInterface::SCOPE_ONE, 283 | null, 284 | $set 285 | ); 286 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'No other log'); 287 | 288 | $this->assertTrue(is_array($nodes), 'An array of nodes is retrieved'); 289 | $this->assertCount(3, $nodes); 290 | $this->assertEquals('ent1', $nodes[0]->getDn()); 291 | $this->assertEquals('ent2', $nodes[1]->getDn()); 292 | $this->assertEquals('ent3', $nodes[2]->getDn()); 293 | 294 | // Successful search with no entry in the result set 295 | $nodes = $manager->getChildrenNodes($node); 296 | 297 | $this->assertSearchLog( 298 | $this->driver->getConnection()->shiftLog(), 299 | 'test_node', 300 | '(objectclass=*)', 301 | SearchInterface::SCOPE_ONE 302 | ); 303 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'No other log'); 304 | 305 | $this->assertTrue(is_array($nodes), 'An array of nodes is retrieved'); 306 | $this->assertCount(0, $nodes); 307 | 308 | // Handling of NoResultException 309 | $this->driver->getConnection()->setFailure(Connection::ERR_NO_RESULT); 310 | 311 | $nodes = $manager->getChildrenNodes($node); 312 | $this->assertTrue(is_array($nodes), 'An array of nodes is retrieved'); 313 | $this->assertCount(0, $nodes); 314 | 315 | // Handling of other search exceptions 316 | $this->driver->getConnection()->setFailure(); 317 | 318 | try { 319 | $nodes = $manager->getChildrenNodes($node); 320 | $this->fail('Other search exceptions do not get processed and are populated'); 321 | } catch (SearchException $e) { 322 | $this->assertRegExp('/Search failed/', $e->getMessage()); 323 | } 324 | } 325 | } -------------------------------------------------------------------------------- /Tests/Core/Manager/ManagerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Core\Manager; 13 | 14 | use Toyota\Component\Ldap\Core\Manager; 15 | use Toyota\Component\Ldap\Core\Node; 16 | use Toyota\Component\Ldap\Core\NodeAttribute; 17 | use Toyota\Component\Ldap\Exception\NotBoundException; 18 | use Toyota\Component\Ldap\Platform\Test\Driver; 19 | use Toyota\Component\Ldap\Tests\TestCase; 20 | 21 | abstract class ManagerTest extends TestCase 22 | { 23 | /** 24 | * This method is called before a test is executed 25 | * 26 | * @return void 27 | * 28 | */ 29 | protected function setUp() 30 | { 31 | $this->driver = new Driver(); 32 | 33 | $this->minimal = array( 34 | 'hostname' => 'ldap.example.com', 35 | 'base_dn' => 'dc=example,dc=com' 36 | ); 37 | } 38 | 39 | /** 40 | * Asserts exceptions get thrown when user is not bound before performing the 41 | * given method 42 | * 43 | * @param Manager $manager Instance of the manager to use 44 | * @param string $method Name of the Manager method to use 45 | * @param array $params Parameters to give on the method call (Optional) 46 | * 47 | * @return void 48 | */ 49 | protected function assertBindingFirst($manager, $method, $params = array()) 50 | { 51 | try { 52 | call_user_func_array(array($manager, $method), $params); 53 | $this->fail('Connection has to be active before working with the LDAP'); 54 | } catch (NotBoundException $e) { 55 | $this->assertRegExp('/have to bind/', $e->getMessage()); 56 | } 57 | } 58 | 59 | /** 60 | * Asserts manager parameters 61 | * 62 | * @param array $params Given parameters 63 | * @param string $hostname Expected hostname 64 | * @param int $port Expected port 65 | * @param boolean $withSSL Whether SSL is active 66 | * @param boolean $withTLS Whether TLS is active 67 | * 68 | * @return void 69 | */ 70 | protected function assertConfiguration($params, $hostname, $port, $withSSL, $withTLS) 71 | { 72 | $manager = new Manager($params, $this->driver); 73 | $manager->connect(); 74 | 75 | $this->assertEquals($hostname, $this->driver->getHostname(), 'URL prefix is removed'); 76 | $this->assertEquals($port, $this->driver->getPort(), 'Default LDAP port is applied'); 77 | $this->assertEquals($withSSL, $this->driver->hasSSL()); 78 | $this->assertEquals($withTLS, $this->driver->hasTLS()); 79 | } 80 | 81 | /** 82 | * Asserts binding parameters 83 | * 84 | * @param array $params Given parameters 85 | * @param boolean $isBound Expected hostname 86 | * @param boolean $isAnonymous Expected port 87 | * @param string $dn Bind dn (Default: null) 88 | * @param string $password Bind password (Default: null) 89 | * 90 | * @return void 91 | */ 92 | protected function assertBinding($params, $isBound, $isAnonymous, $dn = null, $password = null) 93 | { 94 | $manager = new Manager($params, $this->driver); 95 | $manager->connect(); 96 | $manager->bind(); 97 | $instance = $this->driver->getConnection(); 98 | 99 | $this->assertEquals($isBound, $instance->isBound()); 100 | 101 | if ($isAnonymous) { 102 | $this->assertNull($instance->getBindDn(), 'Anonymous bind Dn'); 103 | $this->assertNull($instance->getBindPassword(), 'Anonymous bind Password'); 104 | } else { 105 | $this->assertEquals($dn, $instance->getBindDn(), 'Privileged bind Dn'); 106 | $this->assertEquals($password, $instance->getBindPassword(), 'Privileged bind Password'); 107 | } 108 | } 109 | 110 | /** 111 | * Asserts given log is the expected search log 112 | * 113 | * @param array $log Tested log entry 114 | * @param string $dn Base dn for the search 115 | * @param string $filter Search filter 116 | * @param int $scope Search scope (ALL, BASE, ONE) 117 | * @param array $attributes Attributes searched (Optional) 118 | * @param array $entries Entries expected in the result set (Optional) 119 | * 120 | * @return void 121 | */ 122 | protected function assertSearchLog( 123 | $log, 124 | $dn, 125 | $filter, 126 | $scope, 127 | $attributes = null, 128 | $entries = null) { 129 | 130 | $this->assertEquals('search', $log['type']); 131 | $this->assertInstanceOf('Toyota\Component\Ldap\Platform\Test\Search', $log['data']); 132 | $this->assertEquals($dn, $log['data']->getBaseDn()); 133 | $this->assertEquals($filter, $log['data']->getFilter()); 134 | $this->assertEquals($scope, $log['data']->getScope()); 135 | if (null === $attributes) { 136 | $this->assertNull($log['data']->getAttributes()); 137 | } else { 138 | $this->assertEquals($attributes, $log['data']->getAttributes()); 139 | } 140 | if (null === $entries) { 141 | $this->assertEquals(array(), $log['data']->getEntries()); 142 | } else { 143 | $this->assertEquals($entries, $log['data']->getEntries()); 144 | } 145 | } 146 | 147 | /** 148 | * Asserts given log is the expected persistence action log 149 | * 150 | * @param array $log Tested log entry 151 | * @param string $action Name of persistence action (create, delete, attr_(add/rep/del)) 152 | * @param string $dn Dn of the entry subject for the action 153 | * @param array $data Attributes data passed along with the action (Optional) 154 | * 155 | * @return void 156 | */ 157 | protected function assertActionLog($log, $action, $dn, $data = null) 158 | { 159 | $this->assertEquals($action, $log['type']); 160 | $this->assertEquals($dn, $log['data']['dn']); 161 | if (null === $data) { 162 | $this->assertArrayNotHasKey('attributes', $log['data']); 163 | } else { 164 | $this->assertEquals($data, $log['data']['attributes']); 165 | } 166 | } 167 | 168 | /** 169 | * Asserts a node got snapshot 170 | * 171 | * @param Node $node Node to test 172 | * @param string $msg Message logged with assertion (Optional) 173 | * 174 | * @return void 175 | */ 176 | protected function assertSnapshot(Node $node, $msg = null) 177 | { 178 | $this->assertEquals( 179 | array(), 180 | array_merge( 181 | $node->getDiffAdditions(), 182 | $node->getDiffDeletions(), 183 | $node->getDiffReplacements() 184 | ), 185 | $msg 186 | ); 187 | } 188 | 189 | /** 190 | * Node factory 191 | * 192 | * @param string $dn Distinguished name for the node 193 | * @param array $attributes Array of attributes 194 | * 195 | * @return Node node 196 | */ 197 | protected function buildNode($dn, $attributes) 198 | { 199 | $node = new Node(); 200 | $node->setDn($dn); 201 | foreach ($attributes as $name => $data) { 202 | $attr = new NodeAttribute($name); 203 | $attr->add($data); 204 | $node->mergeAttribute($attr); 205 | } 206 | return $node; 207 | } 208 | } -------------------------------------------------------------------------------- /Tests/Core/Manager/ManagerWriteTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Core\Manager; 13 | 14 | use Toyota\Component\Ldap\Core\Manager; 15 | use Toyota\Component\Ldap\Core\Node; 16 | use Toyota\Component\Ldap\Core\NodeAttribute; 17 | use Toyota\Component\Ldap\API\SearchInterface; 18 | use Toyota\Component\Ldap\Platform\Test\Entry; 19 | use Toyota\Component\Ldap\Platform\Test\Search; 20 | use Toyota\Component\Ldap\Platform\Test\Connection; 21 | use Toyota\Component\Ldap\Exception\NotBoundException; 22 | use Toyota\Component\Ldap\Exception\PersistenceException; 23 | use Toyota\Component\Ldap\Exception\NodeNotFoundException; 24 | use Toyota\Component\Ldap\Exception\DeleteException; 25 | 26 | class ManagerWriteTest extends ManagerTest 27 | { 28 | /** 29 | * Tests exceptions handling while saving a Ldap node 30 | * 31 | * @return void 32 | */ 33 | public function testSaveExceptionHandling() 34 | { 35 | $manager = new Manager($this->minimal, $this->driver); 36 | 37 | $node = new Node(); 38 | $attr = new NodeAttribute('attr'); 39 | $attr->add('value'); 40 | $node->mergeAttribute($attr); 41 | 42 | $this->assertBindingFirst($manager, 'save', array($node)); 43 | $manager->connect(); 44 | $this->assertBindingFirst($manager, 'save', array($node)); 45 | $manager->bind(); 46 | 47 | try { 48 | $manager->save($node); 49 | $this->fail('Node Dn has to be set for saving it'); 50 | } catch (PersistenceException $e) { 51 | $this->assertRegExp('/Cannot save: dn missing for the entry/', $e->getMessage()); 52 | } 53 | 54 | $node->setDn('test'); 55 | $this->driver->getConnection()->setFailure( 56 | Connection::ERR_DEFAULT, 57 | Connection::FAIL_COND_PERSIST 58 | ); 59 | try { 60 | $manager->save($node); 61 | $this->fail('Underlying Ldap connection failed to add the entry'); 62 | } catch (PersistenceException $e) { 63 | $this->assertRegExp('/could not add entry/', $e->getMessage()); 64 | } 65 | 66 | $this->assertEquals( 67 | array('attr' => array('value')), 68 | $node->getDiffAdditions(), 69 | 'In case any failure happens while persisting, snapshot is not performed' 70 | ); 71 | 72 | $this->driver->getConnection()->setFailure( 73 | Connection::ERR_DEFAULT, 74 | Connection::FAIL_COND_PERSIST 75 | ); 76 | $entry = new Entry('test', array('other' => array('value2'))); 77 | $this->driver->getConnection()->stackResults(array($entry)); 78 | try { 79 | $manager->save($node); 80 | $this->fail('Underlying Ldap connection failed to update the entry'); 81 | } catch (PersistenceException $e) { 82 | $this->assertRegExp('/could not add attributes/', $e->getMessage()); 83 | } 84 | 85 | $this->assertEquals( 86 | array('attr' => array('value')), 87 | $node->getDiffAdditions(), 88 | 'In case any failure happens while updating, snapshot is not performed' 89 | ); 90 | 91 | $this->assertFalse( 92 | $manager->save($node), 93 | 'No more Ldap failure - A correct node is created and saved in the Ldap store' 94 | ); 95 | 96 | $this->assertEquals( 97 | array(), 98 | $node->getDiffAdditions(), 99 | 'Save occured so snapshot took place' 100 | ); 101 | } 102 | 103 | /** 104 | * Tests saving new nodes to the Ldap 105 | * 106 | * @return void 107 | */ 108 | public function testSaveNewNodes() 109 | { 110 | $manager = new Manager($this->minimal, $this->driver); 111 | $manager->connect(); 112 | $manager->bind(); 113 | 114 | $data = array( 115 | 'attr1' => array('value1'), 116 | 'attr2' => array('value1', 'value2'), 117 | 'attr3' => array('value3') 118 | ); 119 | $node = $this->buildNode('test_dn', $data); 120 | 121 | $this->assertTrue( 122 | $manager->save($node), 123 | 'A correct node is created and saved in the Ldap store' 124 | ); 125 | $this->assertSearchLog( 126 | $this->driver->getConnection()->shiftLog(), 127 | 'test_dn', 128 | '(objectclass=*)', 129 | SearchInterface::SCOPE_BASE 130 | ); 131 | $this->assertActionLog( 132 | $this->driver->getConnection()->shiftLog(), 133 | 'create', 134 | 'test_dn', 135 | $data 136 | ); 137 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'No other log'); 138 | $this->assertSnapshot($node, 'A node is snapshot after save'); 139 | 140 | try { 141 | $node->setDn('other'); 142 | $this->fail('Saving is like hydrating and so dn should be locked'); 143 | } catch (\InvalidArgumentException $e) { 144 | $this->assertRegExp('/Dn cannot be updated manually/', $e->getMessage()); 145 | } 146 | 147 | $node = $this->buildNode('test_dn', array()); 148 | $this->assertTrue( 149 | $manager->save($node), 150 | 'Empty nodes get saved as well' 151 | ); 152 | $this->assertSearchLog( 153 | $this->driver->getConnection()->shiftLog(), 154 | 'test_dn', 155 | '(objectclass=*)', 156 | SearchInterface::SCOPE_BASE 157 | ); 158 | $this->assertActionLog( 159 | $this->driver->getConnection()->shiftLog(), 160 | 'create', 161 | 'test_dn', 162 | array() 163 | ); 164 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'No other log'); 165 | } 166 | 167 | /** 168 | * Tests saving existing nodes to the Ldap 169 | * 170 | * @return void 171 | */ 172 | public function testSaveExistingNodes() 173 | { 174 | $manager = new Manager($this->minimal, $this->driver); 175 | $manager->connect(); 176 | $manager->bind(); 177 | 178 | // Basic node update with search first 179 | $data = array( 180 | 'attr1' => array('value1'), 181 | 'attr2' => array('value1', 'value2'), 182 | 'attr3' => array('value3') 183 | ); 184 | $node = $this->buildNode('test_dn', $data); 185 | 186 | $entry = new Entry('test_dn', array('attr' => array('value2'))); 187 | $this->driver->getConnection()->stackResults(array($entry)); 188 | 189 | $this->assertFalse( 190 | $manager->save($node), 191 | 'Node persistence resulted in an update' 192 | ); 193 | $this->assertSearchLog( 194 | $this->driver->getConnection()->shiftLog(), 195 | 'test_dn', 196 | '(objectclass=*)', 197 | SearchInterface::SCOPE_BASE, 198 | null, 199 | array($entry) 200 | ); 201 | $this->assertActionLog( 202 | $this->driver->getConnection()->shiftLog(), 203 | 'attr_add', 204 | 'test_dn', 205 | $data 206 | ); 207 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'No other log'); 208 | $this->assertSnapshot($node, 'A node is snapshot after update'); 209 | 210 | try { 211 | $node->setDn('other'); 212 | $this->fail('Saving is like hydrating and so dn should be locked'); 213 | } catch (\InvalidArgumentException $e) { 214 | $this->assertRegExp('/Dn cannot be updated manually/', $e->getMessage()); 215 | } 216 | 217 | // Node updated again without underlying search 218 | $node->removeAttribute('attr1'); 219 | $node->get('attr2')->add('value4'); 220 | $node->get('attr2')->remove('value1'); 221 | 222 | $this->assertFalse( 223 | $manager->save($node), 224 | 'Node has been marked hydrated so it is always an update - No need to feed a new entry' 225 | ); 226 | 227 | $this->assertActionLog( 228 | $this->driver->getConnection()->shiftLog(), 229 | 'attr_add', 230 | 'test_dn', 231 | array('attr2' => array('value4')) 232 | ); 233 | $this->assertActionLog( 234 | $this->driver->getConnection()->shiftLog(), 235 | 'attr_del', 236 | 'test_dn', 237 | array('attr1' => array(), 'attr2' => array('value1')) 238 | ); 239 | $this->assertNull( 240 | $this->driver->getConnection()->shiftLog(), 241 | 'No search performed, node was already hydrated' 242 | ); 243 | $this->assertSnapshot($node, 'A node is snapshot after update'); 244 | 245 | // Support for all kinds of attributes manipulation 246 | $node->removeAttribute('attr2'); 247 | $attr = new NodeAttribute('attr2'); 248 | $attr->add(array('new1', 'new2')); 249 | $node->mergeAttribute($attr); 250 | $node->get('attr3')->add('new3'); 251 | $node->get('attr3')->remove('value3'); 252 | 253 | $this->assertFalse($manager->save($node), 'Node got updated'); 254 | $this->assertActionLog( 255 | $this->driver->getConnection()->shiftLog(), 256 | 'attr_add', 257 | 'test_dn', 258 | array('attr3' => array('new3')) 259 | ); 260 | $this->assertActionLog( 261 | $this->driver->getConnection()->shiftLog(), 262 | 'attr_del', 263 | 'test_dn', 264 | array('attr3' => array('value3')) 265 | ); 266 | $this->assertActionLog( 267 | $this->driver->getConnection()->shiftLog(), 268 | 'attr_rep', 269 | 'test_dn', 270 | array('attr2' => array('new1', 'new2')) 271 | ); 272 | $this->assertNull( 273 | $this->driver->getConnection()->shiftLog(), 274 | 'No search performed, node was already hydrated' 275 | ); 276 | $this->assertSnapshot($node, 'A node is snapshot after update'); 277 | } 278 | 279 | /** 280 | * Tests complex updates with changeset merging when saving 281 | * 282 | * @return void 283 | */ 284 | public function testSaveMergesChanges() 285 | { 286 | $manager = new Manager($this->minimal, $this->driver); 287 | $manager->connect(); 288 | $manager->bind(); 289 | 290 | $entry = new Entry( 291 | 'test_dn', 292 | array( 293 | 'a' => array('a1', 'a2'), 294 | 'b' => array('b1', 'b2'), 295 | 'c' => array('c1', 'c2'), 296 | 'd' => array('d1', 'd2'), 297 | 'e' => array('e1', 'e2') 298 | ) 299 | ); 300 | $this->driver->getConnection()->stackResults(array($entry)); 301 | 302 | $node = new Node(); 303 | $node->setDn('test_dn'); 304 | $node->get('a', true)->add(array('a2', 'a4')); 305 | $node->get('b', true)->add(array('b1', 'b3')); 306 | $node->get('c', true)->add(array('c1', 'c3')); 307 | $node->get('d', true)->add(array('d1', 'd2', 'd3', 'd4')); 308 | $node->get('g', true)->add('g1'); 309 | $node->get('h', true)->add(array('h1', 'h2')); 310 | $node->get('i', true)->add(array('i1', 'i2')); 311 | $node->snapshot(false); 312 | 313 | $node->get('a')->add(array('a1', 'a3')); 314 | $node->removeAttribute('b'); 315 | $node->get('c')->set(array('c4', 'c5')); 316 | $node->get('d')->remove('d2'); 317 | $node->get('d')->remove('d3'); 318 | $node->get('d')->add('d5'); 319 | $node->get('f', true)->add(array('f1', 'f2')); 320 | $node->removeAttribute('g'); 321 | $node->get('h')->set(array('h1', 'h3')); 322 | $node->get('i')->remove('i2'); 323 | 324 | $this->assertFalse( 325 | $manager->save($node), 326 | 'Node persistence resulted in an update' 327 | ); 328 | $this->assertSearchLog( 329 | $this->driver->getConnection()->shiftLog(), 330 | 'test_dn', 331 | '(objectclass=*)', 332 | SearchInterface::SCOPE_BASE, 333 | null, 334 | array($entry) 335 | ); 336 | $this->assertActionLog( 337 | $this->driver->getConnection()->shiftLog(), 338 | 'attr_add', 339 | 'test_dn', 340 | array( 341 | 'a' => array('a3'), 342 | 'd' => array('d5'), 343 | 'f' => array('f1', 'f2'), 344 | 'h' => array('h1', 'h3') 345 | ) 346 | ); 347 | $this->assertActionLog( 348 | $this->driver->getConnection()->shiftLog(), 349 | 'attr_del', 350 | 'test_dn', 351 | array( 352 | 'b' => array(), 353 | 'd' => array('d2') 354 | ) 355 | ); 356 | $this->assertActionLog( 357 | $this->driver->getConnection()->shiftLog(), 358 | 'attr_rep', 359 | 'test_dn', 360 | array( 361 | 'c' => array('c4', 'c5') 362 | ) 363 | ); 364 | $this->assertNull( 365 | $this->driver->getConnection()->shiftLog(), 366 | 'All logs have been parsed' 367 | ); 368 | $this->assertSnapshot($node, 'A node is snapshot after update'); 369 | } 370 | 371 | /** 372 | * Tests deletion of nodes 373 | * 374 | * @return void 375 | */ 376 | public function testDelete() 377 | { 378 | $manager = new Manager($this->minimal, $this->driver); 379 | 380 | $node = $this->buildNode('ent1', array()); 381 | 382 | $this->assertBindingFirst($manager, 'delete', array($node)); 383 | $manager->connect(); 384 | $this->assertBindingFirst($manager, 'delete', array($node)); 385 | $manager->bind(); 386 | 387 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'Nothing happenned yet'); 388 | 389 | // No corresponding entry in the Ldap 390 | try { 391 | $manager->delete($node); 392 | $this->fail('This entry is not in the Ldap store'); 393 | } catch (NodeNotFoundException $e) { 394 | $this->assertRegExp('/ent1 not found/', $e->getMessage()); 395 | } 396 | $this->assertSearchLog( 397 | $this->driver->getConnection()->shiftLog(), 398 | 'ent1', 399 | '(objectclass=*)', 400 | SearchInterface::SCOPE_BASE 401 | ); 402 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'Nothing else'); 403 | 404 | // Basic deletion 405 | $set = array(new Entry('ent1')); 406 | $this->driver->getConnection()->stackResults($set); 407 | 408 | $manager->delete($node); 409 | 410 | $this->assertNull($this->driver->getConnection()->shiftResults(), 'Node got pulled'); 411 | 412 | $this->assertSearchLog( 413 | $this->driver->getConnection()->shiftLog(), 414 | 'ent1', 415 | '(objectclass=*)', 416 | SearchInterface::SCOPE_BASE, 417 | null, 418 | $set 419 | ); // Deleted node search 420 | $this->assertSearchLog( 421 | $this->driver->getConnection()->shiftLog(), 422 | 'ent1', 423 | '(objectclass=*)', 424 | SearchInterface::SCOPE_ONE 425 | ); // Deleted node children search 426 | $this->assertActionLog( 427 | $this->driver->getConnection()->shiftLog(), 428 | 'delete', 429 | 'ent1' 430 | ); 431 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'nothing else'); 432 | 433 | // Deletion does not search for the entry if the node is already hydrated 434 | $node =new Node(); 435 | $node->hydrateFromEntry(new Entry('ent1', array())); 436 | $this->assertNull($this->driver->getConnection()->shiftResults(), 'No node in the stack'); 437 | $manager->delete($node); 438 | $this->assertSearchLog( 439 | $this->driver->getConnection()->shiftLog(), 440 | 'ent1', 441 | '(objectclass=*)', 442 | SearchInterface::SCOPE_ONE 443 | ); // Only one children search 444 | $this->assertActionLog( 445 | $this->driver->getConnection()->shiftLog(), 446 | 'delete', 447 | 'ent1' 448 | ); 449 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'nothing else'); 450 | 451 | // Exception with node children and no recursion configured 452 | $node = $this->buildNode('ref', array()); 453 | $sets = array(); 454 | $sets[] = array(new Entry('ref')); 455 | $sets[] = array(new Entry('a-ref'), new Entry('b-ref'), new Entry('c-ref')); 456 | $this->driver->getConnection()->stackResults($sets[0]); // The node we want to delete 457 | $this->driver->getConnection()->stackResults($sets[1]); // Search for children nodes 458 | 459 | try { 460 | $manager->delete($node); 461 | $this->fail('Cannot delete the node, it has children'); 462 | } catch (DeleteException $e) { 463 | $this->assertRegExp('/ref cannot be deleted/', $e->getMessage()); 464 | $this->assertRegExp('/it has some children left/', $e->getMessage()); 465 | } 466 | $this->assertSearchLog( 467 | $this->driver->getConnection()->shiftLog(), 468 | 'ref', 469 | '(objectclass=*)', 470 | SearchInterface::SCOPE_BASE, 471 | null, 472 | $sets[0] 473 | ); 474 | $this->assertSearchLog( 475 | $this->driver->getConnection()->shiftLog(), 476 | 'ref', 477 | '(objectclass=*)', 478 | SearchInterface::SCOPE_ONE, 479 | null, 480 | $sets[1] 481 | ); 482 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'nothing else'); 483 | 484 | // Recursive mode deletion 485 | $node = $this->buildNode('tst', array()); 486 | 487 | $sets = array( 488 | array(new Entry('tst')), // root node to delete 489 | array(new Entry('a-tst'), new Entry('b-tst'), new Entry('c-tst')), // 1st level children 490 | array(), // a-tst 2nd level children 491 | array(new Entry('a-b-tst')), // b-tst 2nd level children 492 | array(), // a-b-tst 3rd level children 493 | array() // c-tst 2nd level children 494 | ); 495 | 496 | for ($i=0;$i < count($sets);$i++) { 497 | $this->driver->getConnection()->stackResults($sets[$i]); 498 | } 499 | 500 | $manager->delete($node, true); 501 | 502 | $this->assertSearchLog( 503 | $this->driver->getConnection()->shiftLog(), 504 | 'tst', 505 | '(objectclass=*)', 506 | SearchInterface::SCOPE_BASE, 507 | null, 508 | $sets[0] 509 | ); // Deleted node search 510 | $this->assertSearchLog( 511 | $this->driver->getConnection()->shiftLog(), 512 | 'tst', 513 | '(objectclass=*)', 514 | SearchInterface::SCOPE_ONE, 515 | null, 516 | $sets[1] 517 | ); // Deleted node children search 518 | $this->assertSearchLog( 519 | $this->driver->getConnection()->shiftLog(), 520 | 'a-tst', 521 | '(objectclass=*)', 522 | SearchInterface::SCOPE_ONE, 523 | null, 524 | $sets[2] 525 | ); // a-tst node children search 526 | $this->assertActionLog( 527 | $this->driver->getConnection()->shiftLog(), 528 | 'delete', 529 | 'a-tst' 530 | ); 531 | $this->assertSearchLog( 532 | $this->driver->getConnection()->shiftLog(), 533 | 'b-tst', 534 | '(objectclass=*)', 535 | SearchInterface::SCOPE_ONE, 536 | null, 537 | $sets[3] 538 | ); // b-tst node children search 539 | $this->assertSearchLog( 540 | $this->driver->getConnection()->shiftLog(), 541 | 'a-b-tst', 542 | '(objectclass=*)', 543 | SearchInterface::SCOPE_ONE, 544 | null, 545 | $sets[4] 546 | ); // a-b-tst node children search 547 | $this->assertActionLog( 548 | $this->driver->getConnection()->shiftLog(), 549 | 'delete', 550 | 'a-b-tst' 551 | ); 552 | $this->assertActionLog( 553 | $this->driver->getConnection()->shiftLog(), 554 | 'delete', 555 | 'b-tst' 556 | ); 557 | $this->assertSearchLog( 558 | $this->driver->getConnection()->shiftLog(), 559 | 'c-tst', 560 | '(objectclass=*)', 561 | SearchInterface::SCOPE_ONE, 562 | null, 563 | $sets[5] 564 | ); // b-tst node children search 565 | $this->assertActionLog( 566 | $this->driver->getConnection()->shiftLog(), 567 | 'delete', 568 | 'c-tst' 569 | ); 570 | $this->assertActionLog( 571 | $this->driver->getConnection()->shiftLog(), 572 | 'delete', 573 | 'tst' 574 | ); 575 | $this->assertNull($this->driver->getConnection()->shiftLog(), 'nothing else'); 576 | } 577 | } -------------------------------------------------------------------------------- /Tests/Core/NodeAttributeTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Core; 13 | 14 | use Toyota\Component\Ldap\Tests\TestCase; 15 | use Toyota\Component\Ldap\Core\NodeAttribute; 16 | 17 | class NodeAttributeTest extends TestCase 18 | { 19 | /** 20 | * Tests iterator implementation 21 | * 22 | * @return void 23 | */ 24 | public function testIteratorImplementation() 25 | { 26 | $attribute = new NodeAttribute('test'); 27 | 28 | $this->assertInstanceOf('\Iterator', $attribute); 29 | 30 | $attribute->rewind(); 31 | $this->assertFalse($attribute->valid()); 32 | $this->assertFalse($attribute->current()); 33 | $this->assertNull($attribute->key()); 34 | $attribute->next(); 35 | $this->assertFalse($attribute->valid()); 36 | 37 | $attribute->add('value1'); 38 | $attribute->add('value2'); 39 | $attribute->add('value3'); 40 | 41 | $attribute->rewind(); 42 | $this->assertTrue($attribute->valid()); 43 | $this->assertEquals('value1', $attribute->current()); 44 | $this->assertEquals(0, $attribute->key()); 45 | $attribute->next(); 46 | 47 | $this->assertTrue($attribute->valid()); 48 | $this->assertEquals('value2', $attribute->current()); 49 | $this->assertEquals(1, $attribute->key()); 50 | $attribute->next(); 51 | 52 | $this->assertTrue($attribute->valid()); 53 | $this->assertEquals('value3', $attribute->current()); 54 | $this->assertEquals(2, $attribute->key()); 55 | $attribute->next(); 56 | 57 | $this->assertFalse($attribute->valid()); 58 | $this->assertFalse($attribute->current()); 59 | $this->assertNull($attribute->key()); 60 | 61 | $attribute->add('value4'); 62 | $this->assertTrue($attribute->valid()); 63 | $this->assertEquals('value4', $attribute->current()); 64 | $this->assertEquals(3, $attribute->key()); 65 | $attribute->next(); 66 | 67 | $this->assertFalse($attribute->valid()); 68 | $this->assertFalse($attribute->current()); 69 | $this->assertNull($attribute->key()); 70 | 71 | $attribute->rewind(); 72 | $this->assertTrue($attribute->valid()); 73 | $this->assertEquals('value1', $attribute->current()); 74 | $this->assertEquals(0, $attribute->key()); 75 | } 76 | 77 | /** 78 | * Tests countable interface implementation 79 | * 80 | * @return void 81 | */ 82 | public function testCountableImplementation() 83 | { 84 | $attribute = new NodeAttribute('test'); 85 | 86 | $this->assertInstanceOf('\Countable', $attribute); 87 | 88 | $this->assertEquals(0, $attribute->count()); 89 | 90 | $attribute->add('value1'); 91 | $this->assertEquals(1, $attribute->count()); 92 | } 93 | 94 | /** 95 | * Tests array access implementation 96 | * 97 | * @return void 98 | */ 99 | public function testArrayAccessImplementation() 100 | { 101 | $attribute = new NodeAttribute('test'); 102 | 103 | $this->assertInstanceOf('\ArrayAccess', $attribute); 104 | 105 | $this->assertNull($attribute->offsetGet(0)); 106 | $this->assertFalse($attribute->offsetExists(0)); 107 | $attribute->offsetUnset(0); 108 | $this->assertNull($attribute->offsetGet(0)); 109 | 110 | $attribute->add('value1'); 111 | $this->assertEquals('value1', $attribute->offsetGet(0)); 112 | $this->assertTrue($attribute->offsetExists(0)); 113 | $this->assertFalse($attribute->offsetExists(1)); 114 | 115 | $attribute->offsetSet(1, 'value2'); 116 | $attribute->offsetSet(0, 'value3'); 117 | $this->assertEquals(2, $attribute->count(), 'value1 should have been replaced'); 118 | 119 | $this->assertEquals('value3', $attribute->offsetGet(0)); 120 | $this->assertTrue($attribute->offsetExists(0)); 121 | $this->assertEquals('value2', $attribute->offsetGet(1)); 122 | $this->assertTrue($attribute->offsetExists(1)); 123 | 124 | $attribute->offsetUnset(0); 125 | $this->assertNull($attribute->offsetGet(0)); 126 | $this->assertFalse($attribute->offsetExists(0)); 127 | $this->assertEquals('value2', $attribute->offsetGet(1)); 128 | $this->assertTrue($attribute->offsetExists(1)); 129 | $this->assertEquals(1, $attribute->count(), 'value3 has really been removed'); 130 | 131 | $attribute->offsetSet(null, 'value4'); 132 | $attribute->offsetSet(null, 'value5'); 133 | $this->assertEquals(3, $attribute->count(), 'values have been added'); 134 | $this->assertEquals('value2', $attribute->offsetGet(1)); 135 | $this->assertEquals('value4', $attribute->offsetGet(2)); 136 | $this->assertEquals('value5', $attribute->offsetGet(3)); 137 | } 138 | 139 | /** 140 | * Tests adding & removing basic scalar values 141 | * 142 | * @return void 143 | */ 144 | public function testBasicValuesHandling() 145 | { 146 | $attribute = new NodeAttribute('test'); 147 | 148 | $this->assertTrue($attribute->add('value1')); 149 | $this->assertTrue($attribute->add('value2')); 150 | $this->assertTrue($attribute->add('value3')); 151 | $this->assertFalse($attribute->add(''), 'Empty string is not valid'); 152 | $this->assertFalse($attribute->add(null), 'null is not valid'); 153 | $this->assertEquals('value1', $attribute[0]); 154 | $this->assertEquals('value2', $attribute[1]); 155 | $this->assertEquals('value3', $attribute[2]); 156 | $this->assertEquals(3, count($attribute), 'All values have been checked'); 157 | 158 | $this->assertFalse( 159 | $attribute->add('value2'), 160 | 'Value2 has not been added as it is a duplicate' 161 | ); 162 | $this->assertEquals(3, count($attribute)); 163 | 164 | $this->assertTrue($attribute->remove('value2')); 165 | 166 | $this->assertEquals('value1', $attribute[0]); 167 | $this->assertEquals('value3', $attribute[2]); 168 | $this->assertEquals(2, count($attribute), 'All values have been checked'); 169 | 170 | $this->assertFalse($attribute->remove('value2')); 171 | $this->assertEquals(2, count($attribute), 'No change in value set'); 172 | 173 | $this->assertFalse($attribute->remove(null), 'Null is not relevant'); 174 | $this->assertFalse($attribute->remove(''), 'Empty string is not relevant'); 175 | $this->assertEquals('value1', $attribute[0]); 176 | $this->assertEquals('value3', $attribute[2]); 177 | $this->assertEquals(2, count($attribute), 'All values have been checked'); 178 | 179 | $attribute = new NodeAttribute('test'); 180 | $this->assertFalse($attribute->add(null)); 181 | $this->assertEquals(0, count($attribute), 'No values have been stored'); 182 | } 183 | 184 | /** 185 | * Tests adding removing arrays of values 186 | * 187 | * @return void 188 | */ 189 | public function testArrayValuesHandling() 190 | { 191 | $attribute = new NodeAttribute('test'); 192 | 193 | $this->assertTrue($attribute->add(array('value1', 'value2', 'value3'))); 194 | $this->assertEquals('value1', $attribute[0]); 195 | $this->assertEquals('value2', $attribute[1]); 196 | $this->assertEquals('value3', $attribute[2]); 197 | $this->assertEquals(3, count($attribute), 'All values have been checked'); 198 | 199 | $this->assertFalse( 200 | $attribute->add(array('value3', 'value1')), 201 | 'When none of the array values are added, false is returned' 202 | ); 203 | $this->assertEquals(3, count($attribute), 'No change in the values'); 204 | 205 | $this->assertFalse( 206 | $attribute->add(array()), 207 | 'No values have been added again' 208 | ); 209 | $this->assertEquals(3, count($attribute), 'No change in the values'); 210 | 211 | $this->assertFalse( 212 | $attribute->add('value2'), 213 | 'Value2 has not been added as it is a duplicate' 214 | ); 215 | $this->assertEquals(3, count($attribute)); 216 | 217 | $this->assertFalse( 218 | $attribute->add(array('', null)), 219 | 'Empty string and null are not valid values' 220 | ); 221 | $this->assertEquals(3, count($attribute), 'No change in the values'); 222 | 223 | $this->assertTrue( 224 | $attribute->add(array('value3', 'value1', 'value4', null, '')), 225 | 'When at least one value gets added, true is returned' 226 | ); 227 | $this->assertEquals('value1', $attribute[0]); 228 | $this->assertEquals('value2', $attribute[1]); 229 | $this->assertEquals('value3', $attribute[2]); 230 | $this->assertEquals('value4', $attribute[3]); 231 | $this->assertEquals(4, count($attribute), 'All values have been checked'); 232 | 233 | $this->assertFalse( 234 | $attribute->remove(array()), 235 | 'No value removed so false is returned' 236 | ); 237 | $this->assertEquals(4, count($attribute), 'No change in the values'); 238 | 239 | $this->assertFalse( 240 | $attribute->remove(array('value5', '', null, 'value6')), 241 | 'No value removed so false is returned' 242 | ); 243 | $this->assertEquals(4, count($attribute), 'No change in the values'); 244 | 245 | $this->assertTrue( 246 | $attribute->remove(array('value3', '', 'value1')), 247 | 'Some values have been removed so true is returned' 248 | ); 249 | $this->assertEquals('value2', $attribute[1]); 250 | $this->assertEquals('value4', $attribute[3]); 251 | $this->assertEquals(2, count($attribute), 'All values have been checked'); 252 | 253 | $this->assertTrue($attribute->remove('value4')); 254 | $this->assertEquals('value2', $attribute[1]); 255 | $this->assertEquals(1, count($attribute), 'All values have been checked'); 256 | 257 | $this->assertTrue( 258 | $attribute->remove(array('value1', 'value4', 'value2')), 259 | 'Some values have been removed so true is returned' 260 | ); 261 | $this->assertEquals(0, count($attribute), 'No values are stored anymore'); 262 | } 263 | 264 | /** 265 | * Tests setting values 266 | * 267 | * @return void 268 | */ 269 | public function testSet() 270 | { 271 | $test = new NodeAttribute('test'); 272 | 273 | $this->assertTrue($test->set('v1')); 274 | $this->assertEquals( 275 | array('v1'), 276 | $test->getValues(), 277 | 'Our value got added' 278 | ); 279 | $this->assertEquals( 280 | array(), 281 | array_merge( 282 | $test->getDiffAdditions(), 283 | $test->getDiffDeletions(), 284 | $test->getDiffReplacements() 285 | ), 286 | 'Attribute is marked as overridden, changes are not tracked anymore' 287 | ); 288 | 289 | $this->assertTrue($test->set(array('v2', 'v3'))); 290 | $this->assertEquals( 291 | array(), 292 | array_merge( 293 | $test->getDiffAdditions(), 294 | $test->getDiffDeletions(), 295 | $test->getDiffReplacements() 296 | ), 297 | 'Attribute is still marked as overridden, no changes got tracked' 298 | ); 299 | 300 | $test->add('v4'); 301 | $test->remove('v3'); 302 | $test->remove('v2'); 303 | $test->add('v3'); 304 | $this->assertEquals( 305 | array(), 306 | array_merge( 307 | $test->getDiffAdditions(), 308 | $test->getDiffDeletions(), 309 | $test->getDiffReplacements() 310 | ), 311 | 'Even regular add and remove operations get ignored' 312 | ); 313 | 314 | $test->snapshot(); 315 | $test->add('v5'); 316 | $this->assertEquals( 317 | array('v5'), 318 | $test->getDiffAdditions(), 319 | 'Tracking is working again' 320 | ); 321 | } 322 | 323 | /** 324 | * Tests checking if attribute has to be replaced on persistence 325 | * 326 | * @return void 327 | */ 328 | public function testIsReplaced() 329 | { 330 | $test = new NodeAttribute('test'); 331 | $test->add(array('v1', 'v2', 'v3', 'v4')); 332 | $test->snapshot(); 333 | 334 | $test->add('v5'); 335 | $test->remove('v3'); 336 | $test->remove('v2'); 337 | $test->add('v3'); 338 | $this->assertEquals( 339 | array('v5'), 340 | $test->getDiffAdditions(), 341 | 'Tracking works as usual' 342 | ); 343 | $this->assertEquals( 344 | array('v2'), 345 | $test->getDiffDeletions(), 346 | 'Tracking works as usual' 347 | ); 348 | $this->assertEquals( 349 | array('v3'), 350 | $test->getDiffReplacements(), 351 | 'Tracking works as usual' 352 | ); 353 | $this->assertFalse($test->isReplaced(), 'We have just been adding & removing values'); 354 | 355 | $test->set(array('v2', 'v6')); 356 | $this->assertEquals( 357 | array(), 358 | array_merge( 359 | $test->getDiffAdditions(), 360 | $test->getDiffDeletions(), 361 | $test->getDiffReplacements() 362 | ), 363 | 'Diff got cleared' 364 | ); 365 | $this->assertTrue($test->isReplaced(), 'Object is marked for a complete replacement'); 366 | $test->add('v5'); 367 | $test->remove('v2'); 368 | $this->assertEquals( 369 | array(), 370 | array_merge( 371 | $test->getDiffAdditions(), 372 | $test->getDiffDeletions(), 373 | $test->getDiffReplacements() 374 | ), 375 | 'Diff not updated' 376 | ); 377 | $this->assertTrue($test->isReplaced(), 'Object is still marked for a replacement'); 378 | 379 | $test->snapshot(); 380 | $test->add('v2'); 381 | $this->assertEquals( 382 | array('v2'), 383 | $test->getDiffAdditions(), 384 | 'Diff tracked again' 385 | ); 386 | $this->assertFalse($test->isReplaced(), 'We are no more in a replacement case'); 387 | } 388 | 389 | /** 390 | * Tests retrieving attribute name 391 | * 392 | * @return void 393 | */ 394 | public function testGetName() 395 | { 396 | $test = new NodeAttribute('test'); 397 | $other = new NodeAttribute('other'); 398 | $this->assertEquals('test', $test->getName()); 399 | $this->assertEquals('other', $other->getName()); 400 | } 401 | 402 | /** 403 | * Tests attributes diff tracking 404 | * 405 | * @return void 406 | */ 407 | public function testDiffTracking() 408 | { 409 | $test = new NodeAttribute('test'); 410 | $test->add(array('value4', 'value5')); 411 | 412 | $test->snapshot(); 413 | 414 | $test->add(array('value1', 'value2', 'value3')); 415 | $test->remove(array('value4', 'value1', 'value5')); 416 | $test[] = 'value6'; 417 | $test[] = 'value4'; 418 | $test[] = 'value7'; 419 | unset($test[5]); //value6 420 | 421 | $this->assertEquals( 422 | array('value2', 'value3', 'value7'), 423 | $test->getDiffAdditions(), 424 | 'Additions have been tracked' 425 | ); 426 | 427 | $this->assertEquals( 428 | array('value5'), 429 | $test->getDiffDeletions(), 430 | 'Deletions have been tracked' 431 | ); 432 | 433 | $this->assertEquals( 434 | array('value4'), 435 | $test->getDiffReplacements(), 436 | 'Replacements have been tracked' 437 | ); 438 | 439 | $test->snapshot(); 440 | $this->assertEquals( 441 | array(), 442 | $test->getDiffAdditions(), 443 | 'Diff tracking has been reset' 444 | ); 445 | $this->assertEquals( 446 | array(), 447 | $test->getDiffDeletions(), 448 | 'Diff tracking has been reset' 449 | ); 450 | $this->assertEquals( 451 | array(), 452 | $test->getDiffReplacements(), 453 | 'Diff tracking has been reset' 454 | ); 455 | } 456 | } -------------------------------------------------------------------------------- /Tests/Core/SearchResultTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Core; 13 | 14 | use Toyota\Component\Ldap\Tests\TestCase; 15 | use Toyota\Component\Ldap\Platform\Test\Search; 16 | use Toyota\Component\Ldap\Platform\Test\Entry; 17 | use Toyota\Component\Ldap\Core\SearchResult; 18 | 19 | class SearchResultTest extends TestCase 20 | { 21 | 22 | /** 23 | * Tests iterator implementation 24 | * 25 | * @return void 26 | */ 27 | public function testIteratorImplementation() 28 | { 29 | $result = new SearchResult(); 30 | $result->rewind(); 31 | $this->assertFalse($result->valid()); 32 | $this->assertFalse($result->current()); 33 | $this->assertNull($result->key()); 34 | $result->next(); 35 | $this->assertFalse($result->valid()); 36 | 37 | $search = new Search(); 38 | $search->setEntries(array(new Entry('a'), new Entry('b'), new Entry('c'))); 39 | 40 | $result->setSearch($search); 41 | 42 | $result->rewind(); 43 | $this->assertTrue($result->valid()); 44 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\Node', $result->current()); 45 | $this->assertEquals('a', $result->current()->getDn()); 46 | $this->assertEquals('a', $result->key()); 47 | $result->next(); 48 | $this->assertTrue($result->valid()); 49 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\Node', $result->current()); 50 | $this->assertEquals('b', $result->current()->getDn()); 51 | $this->assertEquals('b', $result->key()); 52 | $result->next(); 53 | $this->assertTrue($result->valid()); 54 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\Node', $result->current()); 55 | $this->assertEquals('c', $result->current()->getDn()); 56 | $this->assertEquals('c', $result->key()); 57 | $result->next(); 58 | $this->assertFalse($result->valid()); 59 | $this->assertFalse($result->current()); 60 | $this->assertNull($result->key()); 61 | 62 | $result->rewind(); 63 | $this->assertTrue($result->valid()); 64 | $this->assertInstanceOf('Toyota\Component\Ldap\Core\Node', $result->current()); 65 | $this->assertEquals('a', $result->current()->getDn()); 66 | $this->assertEquals('a', $result->key()); 67 | 68 | $search = new Search(); 69 | $search->setEntries(array(new Entry('d'), new Entry('e'), new Entry('f'))); 70 | 71 | $result->setSearch($search); 72 | $this->assertTrue($result->valid()); 73 | $this->assertEquals('d', $result->key(), 'Iterator is rewinded when new search is set'); 74 | } 75 | 76 | /** 77 | * Tests setting search frees memory 78 | * 79 | * @return void 80 | */ 81 | public function testSetSearch() 82 | { 83 | $result = new SearchResult(); 84 | 85 | $search = new Search(); 86 | $search->setEntries(array(new Entry('a'), new Entry('b'), new Entry('c'))); 87 | 88 | $result->setSearch($search); 89 | 90 | $other = new Search(); 91 | $other->setEntries(array(new Entry('d'), new Entry('e'), new Entry('f'))); 92 | 93 | $result->setSearch($search); 94 | 95 | $this->assertEquals( 96 | 0, 97 | count($search->getEntries()), 98 | 'When old search got released from search result, it was freed from memory' 99 | ); 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/Platform/Test/DriverTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Platform\Test; 13 | 14 | use Toyota\Component\Ldap\Tests\TestCase; 15 | use Toyota\Component\Ldap\Platform\Test\Driver; 16 | use Toyota\Component\Ldap\Exception\ConnectionException; 17 | 18 | class DriverTest extends TestCase 19 | { 20 | /** 21 | * Tests connect 22 | * 23 | * @return void 24 | */ 25 | public function testConnect() 26 | { 27 | $driver = new Driver(); 28 | 29 | $conn = $driver->connect('host', 999, true, true); 30 | $this->assertEquals('host', $driver->getHostname()); 31 | $this->assertEquals(999, $driver->getPort()); 32 | $this->assertTrue($driver->hasSSL()); 33 | $this->assertTrue($driver->hasTLS()); 34 | $this->assertInstanceOf('Toyota\Component\Ldap\Platform\Test\Connection', $conn); 35 | $this->assertEquals($conn, $driver->getConnection()); 36 | 37 | $driver->connect('host', 999, false, true); 38 | $this->assertFalse($driver->hasSSL()); 39 | $this->assertTrue($driver->hasTLS()); 40 | 41 | $driver->setFailureFlag(); 42 | try { 43 | $driver->connect('other'); 44 | $this->fail('ConnectionException is raised when driver flag is set'); 45 | } catch (ConnectionException $e) { 46 | $this->assertRegExp('/Cannot connect/', $e->getMessage()); 47 | } 48 | 49 | try { 50 | $driver->connect('other'); 51 | $this->fail('Flag is still set so exceptions keep throwing'); 52 | } catch (ConnectionException $e) { 53 | $this->assertRegExp('/Cannot connect/', $e->getMessage()); 54 | } 55 | 56 | $driver->setFailureFlag(false); 57 | $driver->connect('other'); 58 | $this->assertEquals('other', $driver->getHostname()); 59 | $this->assertEquals(389, $driver->getPort()); 60 | $this->assertFalse($driver->hasSSL()); 61 | $this->assertFalse($driver->hasTLS()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Platform/Test/EntryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Platform\Test; 13 | 14 | use Toyota\Component\Ldap\Tests\TestCase; 15 | use Toyota\Component\Ldap\Platform\Test\Entry; 16 | 17 | class EntryTest extends TestCase 18 | { 19 | /** 20 | * Test accessors 21 | * 22 | * @return void 23 | */ 24 | public function testAccessors() 25 | { 26 | $entry = new Entry('test', array('val1', 'val2')); 27 | $this->assertEquals('test', $entry->getDn()); 28 | $this->assertEquals(array('val1', 'val2'), $entry->getAttributes()); 29 | 30 | $entry = new Entry('other'); 31 | $this->assertEquals('other', $entry->getDn()); 32 | $this->assertEquals(array(), $entry->getAttributes()); 33 | 34 | $entry->setDn('changed'); 35 | $entry->setAttributes(array('new')); 36 | $this->assertEquals('changed', $entry->getDn()); 37 | $this->assertEquals(array('new'), $entry->getAttributes()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/Platform/Test/SearchTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests\Platform\Test; 13 | 14 | use Toyota\Component\Ldap\Tests\TestCase; 15 | use Toyota\Component\Ldap\Platform\Test\Search; 16 | 17 | class SearchTest extends TestCase 18 | { 19 | /** 20 | * Tests iterating search results 21 | * 22 | * @return void 23 | */ 24 | public function testIterating() 25 | { 26 | $search = new Search(); 27 | $search->setEntries(array(1, 2, 3, false)); 28 | 29 | $this->assertEquals(1, $search->next()); 30 | $this->assertEquals(2, $search->next()); 31 | $this->assertEquals(3, $search->next()); 32 | $this->assertNull($search->next()); 33 | $this->assertNull($search->next()); 34 | $this->assertNull($search->next(), 'Does not reset when end of array is reached'); 35 | 36 | $search->reset(); 37 | $this->assertEquals(1, $search->next()); 38 | $this->assertEquals(2, $search->next()); 39 | 40 | $search->reset(); 41 | 42 | $this->assertEquals(1, $search->next()); 43 | } 44 | 45 | /** 46 | * Tests freeing result set 47 | * 48 | * @return void 49 | */ 50 | public function testFree() 51 | { 52 | $search = new Search(); 53 | $search->setEntries(array(1, 2, 3)); 54 | 55 | $search->free(); 56 | $this->assertEquals(array(), $search->getEntries()); 57 | 58 | $search->free(); 59 | $this->assertEquals( 60 | array(), 61 | $search->getEntries(), 62 | 'Repeating free on a freed search is not an issue' 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Toyota\Component\Ldap\Tests; 13 | 14 | class TestCase extends \PHPUnit_Framework_TestCase 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /autoload.php-dist: -------------------------------------------------------------------------------- 1 | =5.3.3", 15 | "ext-ldap": "*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": ">=3.7.13" 19 | }, 20 | "autoload": { 21 | "psr-0": { "Toyota\\Component\\Ldap\\": "" } 22 | }, 23 | "target-dir": "Toyota/Component/Ldap", 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "1.0-dev" 27 | } 28 | }, 29 | "minimum-stability": "stable" 30 | } 31 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "9adb4405517c108fd37e73a64bc149dd", 3 | "packages": [ 4 | 5 | ], 6 | "packages-dev": [ 7 | { 8 | "name": "phpunit/php-code-coverage", 9 | "version": "1.2.7", 10 | "source": { 11 | "type": "git", 12 | "url": "git://github.com/sebastianbergmann/php-code-coverage.git", 13 | "reference": "1.2.7" 14 | }, 15 | "dist": { 16 | "type": "zip", 17 | "url": "https://github.com/sebastianbergmann/php-code-coverage/archive/1.2.7.zip", 18 | "reference": "1.2.7", 19 | "shasum": "" 20 | }, 21 | "require": { 22 | "php": ">=5.3.3", 23 | "phpunit/php-file-iterator": ">=1.3.0@stable", 24 | "phpunit/php-token-stream": ">=1.1.3@stable", 25 | "phpunit/php-text-template": ">=1.1.1@stable" 26 | }, 27 | "suggest": { 28 | "ext-dom": "*", 29 | "ext-xdebug": ">=2.0.5" 30 | }, 31 | "time": "2012-12-02 14:54:55", 32 | "type": "library", 33 | "autoload": { 34 | "classmap": [ 35 | "PHP/" 36 | ] 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "include-path": [ 40 | "" 41 | ], 42 | "license": [ 43 | "BSD-3-Clause" 44 | ], 45 | "authors": [ 46 | { 47 | "name": "Sebastian Bergmann", 48 | "email": "sb@sebastian-bergmann.de", 49 | "role": "lead" 50 | } 51 | ], 52 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 53 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 54 | "keywords": [ 55 | "testing", 56 | "coverage", 57 | "xunit" 58 | ] 59 | }, 60 | { 61 | "name": "phpunit/php-file-iterator", 62 | "version": "1.3.3", 63 | "source": { 64 | "type": "git", 65 | "url": "git://github.com/sebastianbergmann/php-file-iterator.git", 66 | "reference": "1.3.3" 67 | }, 68 | "dist": { 69 | "type": "zip", 70 | "url": "https://github.com/sebastianbergmann/php-file-iterator/zipball/1.3.3", 71 | "reference": "1.3.3", 72 | "shasum": "" 73 | }, 74 | "require": { 75 | "php": ">=5.3.3" 76 | }, 77 | "time": "2012-10-11 04:44:38", 78 | "type": "library", 79 | "autoload": { 80 | "classmap": [ 81 | "File/" 82 | ] 83 | }, 84 | "notification-url": "https://packagist.org/downloads/", 85 | "include-path": [ 86 | "" 87 | ], 88 | "license": [ 89 | "BSD-3-Clause" 90 | ], 91 | "authors": [ 92 | { 93 | "name": "Sebastian Bergmann", 94 | "email": "sb@sebastian-bergmann.de", 95 | "role": "lead" 96 | } 97 | ], 98 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 99 | "homepage": "http://www.phpunit.de/", 100 | "keywords": [ 101 | "filesystem", 102 | "iterator" 103 | ] 104 | }, 105 | { 106 | "name": "phpunit/php-text-template", 107 | "version": "1.1.4", 108 | "source": { 109 | "type": "git", 110 | "url": "git://github.com/sebastianbergmann/php-text-template.git", 111 | "reference": "1.1.4" 112 | }, 113 | "dist": { 114 | "type": "zip", 115 | "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.4", 116 | "reference": "1.1.4", 117 | "shasum": "" 118 | }, 119 | "require": { 120 | "php": ">=5.3.3" 121 | }, 122 | "time": "2012-10-31 11:15:28", 123 | "type": "library", 124 | "autoload": { 125 | "classmap": [ 126 | "Text/" 127 | ] 128 | }, 129 | "notification-url": "https://packagist.org/downloads/", 130 | "include-path": [ 131 | "" 132 | ], 133 | "license": [ 134 | "BSD-3-Clause" 135 | ], 136 | "authors": [ 137 | { 138 | "name": "Sebastian Bergmann", 139 | "email": "sb@sebastian-bergmann.de", 140 | "role": "lead" 141 | } 142 | ], 143 | "description": "Simple template engine.", 144 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 145 | "keywords": [ 146 | "template" 147 | ] 148 | }, 149 | { 150 | "name": "phpunit/php-timer", 151 | "version": "1.0.4", 152 | "source": { 153 | "type": "git", 154 | "url": "git://github.com/sebastianbergmann/php-timer.git", 155 | "reference": "1.0.4" 156 | }, 157 | "dist": { 158 | "type": "zip", 159 | "url": "https://github.com/sebastianbergmann/php-timer/zipball/1.0.4", 160 | "reference": "1.0.4", 161 | "shasum": "" 162 | }, 163 | "require": { 164 | "php": ">=5.3.3" 165 | }, 166 | "time": "2012-10-11 04:45:58", 167 | "type": "library", 168 | "autoload": { 169 | "classmap": [ 170 | "PHP/" 171 | ] 172 | }, 173 | "notification-url": "https://packagist.org/downloads/", 174 | "include-path": [ 175 | "" 176 | ], 177 | "license": [ 178 | "BSD-3-Clause" 179 | ], 180 | "authors": [ 181 | { 182 | "name": "Sebastian Bergmann", 183 | "email": "sb@sebastian-bergmann.de", 184 | "role": "lead" 185 | } 186 | ], 187 | "description": "Utility class for timing", 188 | "homepage": "http://www.phpunit.de/", 189 | "keywords": [ 190 | "timer" 191 | ] 192 | }, 193 | { 194 | "name": "phpunit/php-token-stream", 195 | "version": "1.1.5", 196 | "source": { 197 | "type": "git", 198 | "url": "git://github.com/sebastianbergmann/php-token-stream.git", 199 | "reference": "1.1.5" 200 | }, 201 | "dist": { 202 | "type": "zip", 203 | "url": "https://github.com/sebastianbergmann/php-token-stream/zipball/1.1.5", 204 | "reference": "1.1.5", 205 | "shasum": "" 206 | }, 207 | "require": { 208 | "ext-tokenizer": "*", 209 | "php": ">=5.3.3" 210 | }, 211 | "time": "2012-10-11 04:47:14", 212 | "type": "library", 213 | "autoload": { 214 | "classmap": [ 215 | "PHP/" 216 | ] 217 | }, 218 | "notification-url": "https://packagist.org/downloads/", 219 | "include-path": [ 220 | "" 221 | ], 222 | "license": [ 223 | "BSD-3-Clause" 224 | ], 225 | "authors": [ 226 | { 227 | "name": "Sebastian Bergmann", 228 | "email": "sb@sebastian-bergmann.de", 229 | "role": "lead" 230 | } 231 | ], 232 | "description": "Wrapper around PHP's tokenizer extension.", 233 | "homepage": "http://www.phpunit.de/", 234 | "keywords": [ 235 | "tokenizer" 236 | ] 237 | }, 238 | { 239 | "name": "phpunit/phpunit", 240 | "version": "3.7.13", 241 | "source": { 242 | "type": "git", 243 | "url": "git://github.com/sebastianbergmann/phpunit.git", 244 | "reference": "3.7.13" 245 | }, 246 | "dist": { 247 | "type": "zip", 248 | "url": "https://github.com/sebastianbergmann/phpunit/archive/3.7.13.zip", 249 | "reference": "3.7.13", 250 | "shasum": "" 251 | }, 252 | "require": { 253 | "php": ">=5.3.3", 254 | "phpunit/php-file-iterator": ">=1.3.1", 255 | "phpunit/php-text-template": ">=1.1.1", 256 | "phpunit/php-code-coverage": ">=1.2.1", 257 | "phpunit/php-timer": ">=1.0.2", 258 | "phpunit/phpunit-mock-objects": ">=1.2.0,<1.3.0", 259 | "symfony/yaml": ">=2.1.0,<2.2.0", 260 | "ext-dom": "*", 261 | "ext-pcre": "*", 262 | "ext-reflection": "*", 263 | "ext-spl": "*" 264 | }, 265 | "suggest": { 266 | "phpunit/php-invoker": ">=1.1.0", 267 | "ext-json": "*", 268 | "ext-simplexml": "*", 269 | "ext-tokenizer": "*" 270 | }, 271 | "time": "2013-01-13 10:21:19", 272 | "bin": [ 273 | "composer/bin/phpunit" 274 | ], 275 | "type": "library", 276 | "extra": { 277 | "branch-alias": { 278 | "dev-master": "3.7.x-dev" 279 | } 280 | }, 281 | "autoload": { 282 | "classmap": [ 283 | "PHPUnit/" 284 | ] 285 | }, 286 | "notification-url": "https://packagist.org/downloads/", 287 | "include-path": [ 288 | "", 289 | "../../symfony/yaml/" 290 | ], 291 | "license": [ 292 | "BSD-3-Clause" 293 | ], 294 | "authors": [ 295 | { 296 | "name": "Sebastian Bergmann", 297 | "email": "sebastian@phpunit.de", 298 | "role": "lead" 299 | } 300 | ], 301 | "description": "The PHP Unit Testing framework.", 302 | "homepage": "http://www.phpunit.de/", 303 | "keywords": [ 304 | "testing", 305 | "phpunit", 306 | "xunit" 307 | ] 308 | }, 309 | { 310 | "name": "phpunit/phpunit-mock-objects", 311 | "version": "1.2.3", 312 | "source": { 313 | "type": "git", 314 | "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", 315 | "reference": "1.2.3" 316 | }, 317 | "dist": { 318 | "type": "zip", 319 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", 320 | "reference": "1.2.3", 321 | "shasum": "" 322 | }, 323 | "require": { 324 | "php": ">=5.3.3", 325 | "phpunit/php-text-template": ">=1.1.1@stable" 326 | }, 327 | "suggest": { 328 | "ext-soap": "*" 329 | }, 330 | "time": "2013-01-13 10:24:48", 331 | "type": "library", 332 | "autoload": { 333 | "classmap": [ 334 | "PHPUnit/" 335 | ] 336 | }, 337 | "notification-url": "https://packagist.org/downloads/", 338 | "include-path": [ 339 | "" 340 | ], 341 | "license": [ 342 | "BSD-3-Clause" 343 | ], 344 | "authors": [ 345 | { 346 | "name": "Sebastian Bergmann", 347 | "email": "sb@sebastian-bergmann.de", 348 | "role": "lead" 349 | } 350 | ], 351 | "description": "Mock Object library for PHPUnit", 352 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 353 | "keywords": [ 354 | "mock", 355 | "xunit" 356 | ] 357 | }, 358 | { 359 | "name": "symfony/yaml", 360 | "version": "v2.1.7", 361 | "target-dir": "Symfony/Component/Yaml", 362 | "source": { 363 | "type": "git", 364 | "url": "https://github.com/symfony/Yaml", 365 | "reference": "v2.1.7" 366 | }, 367 | "dist": { 368 | "type": "zip", 369 | "url": "https://github.com/symfony/Yaml/archive/v2.1.7.zip", 370 | "reference": "v2.1.7", 371 | "shasum": "" 372 | }, 373 | "require": { 374 | "php": ">=5.3.3" 375 | }, 376 | "time": "2013-01-17 21:21:51", 377 | "type": "library", 378 | "autoload": { 379 | "psr-0": { 380 | "Symfony\\Component\\Yaml": "" 381 | } 382 | }, 383 | "notification-url": "https://packagist.org/downloads/", 384 | "license": [ 385 | "MIT" 386 | ], 387 | "authors": [ 388 | { 389 | "name": "Fabien Potencier", 390 | "email": "fabien@symfony.com" 391 | }, 392 | { 393 | "name": "Symfony Community", 394 | "homepage": "http://symfony.com/contributors" 395 | } 396 | ], 397 | "description": "Symfony Yaml Component", 398 | "homepage": "http://symfony.com" 399 | } 400 | ], 401 | "aliases": [ 402 | 403 | ], 404 | "minimum-stability": "stable", 405 | "stability-flags": [ 406 | 407 | ] 408 | } 409 | -------------------------------------------------------------------------------- /phpunit.xml-dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ./Tests 22 | 23 | 24 | 25 | 26 | 27 | ./ 28 | 29 | ./Tests 30 | ./Platform/Native 31 | ./vendor 32 | 33 | 34 | 35 | 36 | --------------------------------------------------------------------------------