1 <?php
2 /**
3  * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
4  * Version 4.0.4
5  *
6  * PHP Version 5 with SSL and LDAP support
7  *
8  * Written by Scott Barnett, Richard Hyland
9  *   email: scott@wiggumworld.com, adldap@richardhyland.com
10  *   http://adldap.sourceforge.net/
11  *
12  * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
13  *
14  * We'd appreciate any improvements or additions to be submitted back
15  * to benefit the entire community :)
16  *
17  * This library is free software; you can redistribute it and/or
18  * modify it under the terms of the GNU Lesser General Public
19  * License as published by the Free Software Foundation; either
20  * version 2.1 of the License.
21  *
22  * This library is distributed in the hope that it will be useful,
23  * but WITHOUT ANY WARRANTY; without even the implied warranty of
24  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
25  * Lesser General Public License for more details.
26  *
27  * @category ToolsAndUtilities
28  * @package adLDAP
29  * @author Scott Barnett, Richard Hyland
30  * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
31  * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
32  * @revision $Revision: 169 $
33  * @version 4.0.4
34  * @link http://adldap.sourceforge.net/
35  */
36 
37 /**
38 * Main adLDAP class
39 *
40 * Can be initialised using $adldap = new adLDAP();
41 *
42 * Something to keep in mind is that Active Directory is a permissions
43 * based directory. If you bind as a domain user, you can't fetch as
44 * much information on other users as you could as a domain admin.
45 *
46 * Before asking questions, please read the Documentation at
47 * http://adldap.sourceforge.net/wiki/doku.php?id=api
48 */
49 require_once(dirname(__FILE__) . '/collections/adLDAPCollection.php');
50 require_once(dirname(__FILE__) . '/classes/adLDAPGroups.php');
51 require_once(dirname(__FILE__) . '/classes/adLDAPUsers.php');
52 require_once(dirname(__FILE__) . '/classes/adLDAPFolders.php');
53 require_once(dirname(__FILE__) . '/classes/adLDAPUtils.php');
54 require_once(dirname(__FILE__) . '/classes/adLDAPContacts.php');
55 require_once(dirname(__FILE__) . '/classes/adLDAPExchange.php');
56 require_once(dirname(__FILE__) . '/classes/adLDAPComputers.php');
57 
58 class adLDAP {
59 
60     /**
61      * Define the different types of account in AD
62      */
63     const ADLDAP_NORMAL_ACCOUNT = 805306368;
64     const ADLDAP_WORKSTATION_TRUST = 805306369;
65     const ADLDAP_INTERDOMAIN_TRUST = 805306370;
66     const ADLDAP_SECURITY_GLOBAL_GROUP = 268435456;
67     const ADLDAP_DISTRIBUTION_GROUP = 268435457;
68     const ADLDAP_SECURITY_LOCAL_GROUP = 536870912;
69     const ADLDAP_DISTRIBUTION_LOCAL_GROUP = 536870913;
70     const ADLDAP_FOLDER = 'OU';
71     const ADLDAP_CONTAINER = 'CN';
72 
73     /**
74     * The default port for LDAP non-SSL connections
75     */
76     const ADLDAP_LDAP_PORT = '389';
77     /**
78     * The default port for LDAPS SSL connections
79     */
80     const ADLDAP_LDAPS_PORT = '636';
81 
82     /**
83     * The account suffix for your domain, can be set when the class is invoked
84     *
85     * @var string
86     */
87         protected $accountSuffix = "@mydomain.local";
88 
89     /**
90     * The base dn for your domain
91     *
92     * If this is set to null then adLDAP will attempt to obtain this automatically from the rootDSE
93     *
94     * @var string
95     */
96         protected $baseDn = "DC=mydomain,DC=local";
97 
98     /**
99     * Port used to talk to the domain controllers.
100     *
101     * @var int
102     */
103     protected $adPort = self::ADLDAP_LDAP_PORT;
104 
105     /**
106     * Array of domain controllers. Specifiy multiple controllers if you
107     * would like the class to balance the LDAP queries amongst multiple servers
108     *
109     * @var array
110     */
111     protected $domainControllers = array("dc01.mydomain.local");
112 
113     /**
114     * Optional account with higher privileges for searching
115     * This should be set to a domain admin account
116     *
117     * @var string
118     * @var string
119     */
120         protected $adminUsername = NULL;
121     protected $adminPassword = NULL;
122 
123     /**
124     * AD does not return the primary group. http://support.microsoft.com/?kbid=321360
125     * This tweak will resolve the real primary group.
126     * Setting to false will fudge "Domain Users" and is much faster. Keep in mind though that if
127     * someone's primary group is NOT domain users, this is obviously going to mess up the results
128     *
129     * @var bool
130     */
131         protected $realPrimaryGroup = true;
132 
133     /**
134     * Use SSL (LDAPS), your server needs to be setup, please see
135     * http://adldap.sourceforge.net/wiki/doku.php?id=ldap_over_ssl
136     *
137     * @var bool
138     */
139         protected $useSSL = false;
140 
141     /**
142     * Use TLS
143     * If you wish to use TLS you should ensure that $useSSL is set to false and vice-versa
144     *
145     * @var bool
146     */
147     protected $useTLS = false;
148 
149     /**
150     * Use SSO
151     * To indicate to adLDAP to reuse password set by the brower through NTLM or Kerberos
152     *
153     * @var bool
154     */
155     protected $useSSO = false;
156 
157     /**
158     * When querying group memberships, do it recursively
159     * eg. User Fred is a member of Group A, which is a member of Group B, which is a member of Group C
160     * user_ingroup("Fred","C") will returns true with this option turned on, false if turned off
161     *
162     * @var bool
163     */
164         protected $recursiveGroups = true;
165 
166         // You should not need to edit anything below this line
167         //******************************************************************************************
168 
169         /**
170     * Connection and bind default variables
171     *
172     * @var mixed
173     * @var mixed
174     */
175         protected $ldapConnection;
176         protected $ldapBind;
177 
178     /**
179     * Get the active LDAP Connection
180     *
181     * @return resource
182     */
183     public function getLdapConnection() {
184         if ($this->ldapConnection) {
185             return $this->ldapConnection;
186         }
187         return false;
188     }
189 
190     /**
191     * Get the bind status
192     *
193     * @return bool
194     */
195     public function getLdapBind() {
196         return $this->ldapBind;
197     }
198 
199     /**
200     * Get the current base DN
201     *
202     * @return string
203     */
204     public function getBaseDn() {
205         return $this->baseDn;
206     }
207 
208     /**
209     * The group class
210     *
211     * @var adLDAPGroups
212     */
213     protected $groupClass;
214 
215     /**
216     * Get the group class interface
217     *
218     * @return adLDAPGroups
219     */
220     public function group() {
221         if (!$this->groupClass) {
222             $this->groupClass = new adLDAPGroups($this);
223         }
224         return $this->groupClass;
225     }
226 
227     /**
228     * The user class
229     *
230     * @var adLDAPUsers
231     */
232     protected $userClass;
233 
234     /**
235     * Get the userclass interface
236     *
237     * @return adLDAPUsers
238     */
239     public function user() {
240         if (!$this->userClass) {
241             $this->userClass = new adLDAPUsers($this);
242         }
243         return $this->userClass;
244     }
245 
246     /**
247     * The folders class
248     *
249     * @var adLDAPFolders
250     */
251     protected $folderClass;
252 
253     /**
254     * Get the folder class interface
255     *
256     * @return adLDAPFolders
257     */
258     public function folder() {
259         if (!$this->folderClass) {
260             $this->folderClass = new adLDAPFolders($this);
261         }
262         return $this->folderClass;
263     }
264 
265     /**
266     * The utils class
267     *
268     * @var adLDAPUtils
269     */
270     protected $utilClass;
271 
272     /**
273     * Get the utils class interface
274     *
275     * @return adLDAPUtils
276     */
277     public function utilities() {
278         if (!$this->utilClass) {
279             $this->utilClass = new adLDAPUtils($this);
280         }
281         return $this->utilClass;
282     }
283 
284     /**
285     * The contacts class
286     *
287     * @var adLDAPContacts
288     */
289     protected $contactClass;
290 
291     /**
292     * Get the contacts class interface
293     *
294     * @return adLDAPContacts
295     */
296     public function contact() {
297         if (!$this->contactClass) {
298             $this->contactClass = new adLDAPContacts($this);
299         }
300         return $this->contactClass;
301     }
302 
303     /**
304     * The exchange class
305     *
306     * @var adLDAPExchange
307     */
308     protected $exchangeClass;
309 
310     /**
311     * Get the exchange class interface
312     *
313     * @return adLDAPExchange
314     */
315     public function exchange() {
316         if (!$this->exchangeClass) {
317             $this->exchangeClass = new adLDAPExchange($this);
318         }
319         return $this->exchangeClass;
320     }
321 
322     /**
323     * The computers class
324     *
325     * @var adLDAPComputers
326     */
327     protected $computersClass;
328 
329     /**
330     * Get the computers class interface
331     *
332     * @return adLDAPComputers
333     */
334     public function computer() {
335         if (!$this->computerClass) {
336             $this->computerClass = new adLDAPComputers($this);
337         }
338         return $this->computerClass;
339     }
340 
341     /**
342     * Getters and Setters
343     */
344 
345     /**
346     * Set the account suffix
347     *
348     * @param string $accountSuffix
349     * @return void
350     */
351     public function setAccountSuffix($accountSuffix)
352     {
353           $this->accountSuffix = $accountSuffix;
354     }
355 
356     /**
357     * Get the account suffix
358     *
359     * @return string
360     */
361     public function getAccountSuffix()
362     {
363           return $this->accountSuffix;
364     }
365 
366     /**
367     * Set the domain controllers array
368     *
369     * @param array $domainControllers
370     * @return void
371     */
372     public function setDomainControllers(array $domainControllers)
373     {
374           $this->domainControllers = $domainControllers;
375     }
376 
377     /**
378     * Get the list of domain controllers
379     *
380     * @return void
381     */
382     public function getDomainControllers()
383     {
384           return $this->domainControllers;
385     }
386 
387     /**
388     * Sets the port number your domain controller communicates over
389     *
390     * @param int $adPort
391     */
392     public function setPort($adPort)
393     {
394         $this->adPort = $adPort;
395     }
396 
397     /**
398     * Gets the port number your domain controller communicates over
399     *
400     * @return int
401     */
402     public function getPort()
403     {
404         return $this->adPort;
405     }
406 
407     /**
408     * Set the username of an account with higher priviledges
409     *
410     * @param string $adminUsername
411     * @return void
412     */
413     public function setAdminUsername($adminUsername)
414     {
415           $this->adminUsername = $adminUsername;
416     }
417 
418     /**
419     * Get the username of the account with higher priviledges
420     *
421     * This will throw an exception for security reasons
422     */
423     public function getAdminUsername()
424     {
425           throw new adLDAPException('For security reasons you cannot access the domain administrator account details');
426     }
427 
428     /**
429     * Set the password of an account with higher priviledges
430     *
431     * @param string $adminPassword
432     * @return void
433     */
434     public function setAdminPassword($adminPassword)
435     {
436           $this->adminPassword = $adminPassword;
437     }
438 
439     /**
440     * Get the password of the account with higher priviledges
441     *
442     * This will throw an exception for security reasons
443     */
444     public function getAdminPassword()
445     {
446           throw new adLDAPException('For security reasons you cannot access the domain administrator account details');
447     }
448 
449     /**
450     * Set whether to detect the true primary group
451     *
452     * @param bool $realPrimaryGroup
453     * @return void
454     */
455     public function setRealPrimaryGroup($realPrimaryGroup)
456     {
457           $this->realPrimaryGroup = $realPrimaryGroup;
458     }
459 
460     /**
461     * Get the real primary group setting
462     *
463     * @return bool
464     */
465     public function getRealPrimaryGroup()
466     {
467           return $this->realPrimaryGroup;
468     }
469 
470     /**
471     * Set whether to use SSL
472     *
473     * @param bool $useSSL
474     * @return void
475     */
476     public function setUseSSL($useSSL)
477     {
478           $this->useSSL = $useSSL;
479           // Set the default port correctly
480           if($this->useSSL) {
481             $this->setPort(self::ADLDAP_LDAPS_PORT);
482           }
483           else {
484             $this->setPort(self::ADLDAP_LDAP_PORT);
485           }
486     }
487 
488     /**
489     * Get the SSL setting
490     *
491     * @return bool
492     */
493     public function getUseSSL()
494     {
495           return $this->useSSL;
496     }
497 
498     /**
499     * Set whether to use TLS
500     *
501     * @param bool $useTLS
502     * @return void
503     */
504     public function setUseTLS($useTLS)
505     {
506           $this->useTLS = $useTLS;
507     }
508 
509     /**
510     * Get the TLS setting
511     *
512     * @return bool
513     */
514     public function getUseTLS()
515     {
516           return $this->useTLS;
517     }
518 
519     /**
520     * Set whether to use SSO
521     * Requires ldap_sasl_bind support. Be sure --with-ldap-sasl is used when configuring PHP otherwise this function will be undefined.
522     *
523     * @param bool $useSSO
524     * @return void
525     */
526     public function setUseSSO($useSSO)
527     {
528           if ($useSSO === true && !$this->ldapSaslSupported()) {
529               throw new adLDAPException('No LDAP SASL support for PHP.  See: http://php.net/ldap_sasl_bind');
530           }
531           $this->useSSO = $useSSO;
532     }
533 
534     /**
535     * Get the SSO setting
536     *
537     * @return bool
538     */
539     public function getUseSSO()
540     {
541           return $this->useSSO;
542     }
543 
544     /**
545     * Set whether to lookup recursive groups
546     *
547     * @param bool $recursiveGroups
548     * @return void
549     */
550     public function setRecursiveGroups($recursiveGroups)
551     {
552           $this->recursiveGroups = $recursiveGroups;
553     }
554 
555     /**
556     * Get the recursive groups setting
557     *
558     * @return bool
559     */
560     public function getRecursiveGroups()
561     {
562           return $this->recursiveGroups;
563     }
564 
565     /**
566     * Default Constructor
567     *
568     * Tries to bind to the AD domain over LDAP or LDAPs
569     *
570     * @param array $options Array of options to pass to the constructor
571     * @throws Exception - if unable to bind to Domain Controller
572     * @return bool
573     */
574     function __construct($options = array()) {
575         // You can specifically overide any of the default configuration options setup above
576         if (count($options) > 0) {
577             if (array_key_exists("account_suffix",$options)){ $this->accountSuffix = $options["account_suffix"]; }
578             if (array_key_exists("base_dn",$options)){ $this->baseDn = $options["base_dn"]; }
579             if (array_key_exists("domain_controllers",$options)){
580                 if (!is_array($options["domain_controllers"])) {
581                     throw new adLDAPException('[domain_controllers] option must be an array');
582                 }
583                 $this->domainControllers = $options["domain_controllers"];
584             }
585             if (array_key_exists("admin_username",$options)){ $this->adminUsername = $options["admin_username"]; }
586             if (array_key_exists("admin_password",$options)){ $this->adminPassword = $options["admin_password"]; }
587             if (array_key_exists("real_primarygroup",$options)){ $this->realPrimaryGroup = $options["real_primarygroup"]; }
588             if (array_key_exists("use_ssl",$options)){ $this->setUseSSL($options["use_ssl"]); }
589             if (array_key_exists("use_tls",$options)){ $this->useTLS = $options["use_tls"]; }
590             if (array_key_exists("recursive_groups",$options)){ $this->recursiveGroups = $options["recursive_groups"]; }
591             if (array_key_exists("ad_port",$options)){ $this->setPort($options["ad_port"]); }
592             if (array_key_exists("sso",$options)) {
593                 $this->setUseSSO($options["sso"]);
594                 if (!$this->ldapSaslSupported()) {
595                     $this->setUseSSO(false);
596                 }
597             }
598         }
599 
600         if ($this->ldapSupported() === false) {
601             throw new adLDAPException('No LDAP support for PHP.  See: http://php.net/ldap');
602         }
603 
604         return $this->connect();
605     }
606 
607     /**
608     * Default Destructor
609     *
610     * Closes the LDAP connection
611     *
612     * @return void
613     */
614     function __destruct() {
615         $this->close();
616     }
617 
618     /**
619     * Connects and Binds to the Domain Controller
620     *
621     * @return bool
622     */
623     public function connect()
624     {
625         // Connect to the AD/LDAP server as the username/password
626         $domainController = $this->randomController();
627         if ($this->useSSL) {
628             $this->ldapConnection = ldap_connect("ldaps://" . $domainController, $this->adPort);
629         } else {
630             $this->ldapConnection = ldap_connect($domainController, $this->adPort);
631         }
632 
633         // Set some ldap options for talking to AD
634         ldap_set_option($this->ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3);
635         ldap_set_option($this->ldapConnection, LDAP_OPT_REFERRALS, 0);
636 
637         if ($this->useTLS) {
638             ldap_start_tls($this->ldapConnection);
639         }
640 
641         // Bind as a domain admin if they've set it up
642         if ($this->adminUsername !== NULL && $this->adminPassword !== NULL) {
643             $this->ldapBind = @ldap_bind($this->ldapConnection, $this->adminUsername . $this->accountSuffix, $this->adminPassword);
644             if (!$this->ldapBind) {
645                 if ($this->useSSL && !$this->useTLS) {
646                     // If you have problems troubleshooting, remove the @ character from the ldapldapBind command above to get the actual error message
647                     throw new adLDAPException('Bind to Active Directory failed. Either the LDAPs connection failed or the login credentials are incorrect. AD said: ' . $this->getLastError());
648                 }
649                 else {
650                     throw new adLDAPException('Bind to Active Directory failed. Check the login credentials and/or server details. AD said: ' . $this->getLastError());
651                 }
652             }
653         }
654         if ($this->useSSO && $_SERVER['REMOTE_USER'] && $this->adminUsername === null && $_SERVER['KRB5CCNAME']) {
655             putenv("KRB5CCNAME=" . $_SERVER['KRB5CCNAME']);
656             $this->ldapBind = @ldap_sasl_bind($this->ldapConnection, NULL, NULL, "GSSAPI");
657             if (!$this->ldapBind){
658                 throw new adLDAPException('Rebind to Active Directory failed. AD said: ' . $this->getLastError());
659             }
660             else {
661                 return true;
662             }
663         }
664 
665 
666         if ($this->baseDn == NULL) {
667             $this->baseDn = $this->findBaseDn();
668         }
669 
670         return true;
671     }
672 
673     /**
674     * Closes the LDAP connection
675     *
676     * @return void
677     */
678     public function close() {
679         if ($this->ldapConnection) {
680             @ldap_close($this->ldapConnection);
681         }
682     }
683 
684     /**
685     * Validate a user's login credentials
686     *
687     * @param string $username A user's AD username
688     * @param string $password A user's AD password
689     * @param bool optional $preventRebind
690     * @return bool
691     */
692     public function authenticate($username, $password, $preventRebind = false) {
693         // Prevent null binding
694         if ($username === NULL || $password === NULL) { return false; }
695         if (empty($username) || empty($password)) { return false; }
696 
697         // Allow binding over SSO for Kerberos
698         if ($this->useSSO && $_SERVER['REMOTE_USER'] && $_SERVER['REMOTE_USER'] == $username && $this->adminUsername === NULL && $_SERVER['KRB5CCNAME']) {
699             putenv("KRB5CCNAME=" . $_SERVER['KRB5CCNAME']);
700             $this->ldapBind = @ldap_sasl_bind($this->ldapConnection, NULL, NULL, "GSSAPI");
701             if (!$this->ldapBind) {
702                 throw new adLDAPException('Rebind to Active Directory failed. AD said: ' . $this->getLastError());
703             }
704             else {
705                 return true;
706             }
707         }
708 
709         // Bind as the user
710         $ret = true;
711         $this->ldapBind = @ldap_bind($this->ldapConnection, $username . $this->accountSuffix, $password);
712         if (!$this->ldapBind){
713             $ret = false;
714         }
715 
716         // Cnce we've checked their details, kick back into admin mode if we have it
717         if ($this->adminUsername !== NULL && !$preventRebind) {
718             $this->ldapBind = @ldap_bind($this->ldapConnection, $this->adminUsername . $this->accountSuffix , $this->adminPassword);
719             if (!$this->ldapBind){
720                 // This should never happen in theory
721                 throw new adLDAPException('Rebind to Active Directory failed. AD said: ' . $this->getLastError());
722             }
723         }
724 
725         return $ret;
726     }
727 
728     /**
729     * Find the Base DN of your domain controller
730     *
731     * @return string
732     */
733     public function findBaseDn()
734     {
735         $namingContext = $this->getRootDse(array('defaultnamingcontext'));
736         return $namingContext[0]['defaultnamingcontext'][0];
737     }
738 
739     /**
740     * Get the RootDSE properties from a domain controller
741     *
742     * @param array $attributes The attributes you wish to query e.g. defaultnamingcontext
743     * @return array
744     */
745     public function getRootDse($attributes = array("*", "+")) {
746         if (!$this->ldapBind){ return (false); }
747 
748         $sr = @ldap_read($this->ldapConnection, NULL, 'objectClass=*', $attributes);
749         $entries = @ldap_get_entries($this->ldapConnection, $sr);
750         return $entries;
751     }
752 
753     /**
754     * Get last error from Active Directory
755     *
756     * This function gets the last message from Active Directory
757     * This may indeed be a 'Success' message but if you get an unknown error
758     * it might be worth calling this function to see what errors were raised
759     *
760     * return string
761     */
762     public function getLastError() {
763         return @ldap_error($this->ldapConnection);
764     }
765 
766     /**
767     * Detect LDAP support in php
768     *
769     * @return bool
770     */
771     protected function ldapSupported()
772     {
773         if (!function_exists('ldap_connect')) {
774             return false;
775         }
776         return true;
777     }
778 
779     /**
780     * Detect ldap_sasl_bind support in PHP
781     *
782     * @return bool
783     */
784     protected function ldapSaslSupported()
785     {
786         if (!function_exists('ldap_sasl_bind')) {
787             return false;
788         }
789         return true;
790     }
791 
792     /**
793     * Schema
794     *
795     * @param array $attributes Attributes to be queried
796     * @return array
797     */
798     public function adldap_schema($attributes){
799 
800         // LDAP doesn't like NULL attributes, only set them if they have values
801         // If you wish to remove an attribute you should set it to a space
802         // TO DO: Adapt user_modify to use ldap_mod_delete to remove a NULL attribute
803         $mod=array();
804 
805         // Check every attribute to see if it contains 8bit characters and then UTF8 encode them
806         array_walk($attributes, array($this, 'encode8bit'));
807 
808         if ($attributes["address_city"]){ $mod["l"][0]=$attributes["address_city"]; }
809         if ($attributes["address_code"]){ $mod["postalCode"][0]=$attributes["address_code"]; }
810         //if ($attributes["address_country"]){ $mod["countryCode"][0]=$attributes["address_country"]; } // use country codes?
811         if ($attributes["address_country"]){ $mod["c"][0]=$attributes["address_country"]; }
812         if ($attributes["address_pobox"]){ $mod["postOfficeBox"][0]=$attributes["address_pobox"]; }
813         if ($attributes["address_state"]){ $mod["st"][0]=$attributes["address_state"]; }
814         if ($attributes["address_street"]){ $mod["streetAddress"][0]=$attributes["address_street"]; }
815         if ($attributes["company"]){ $mod["company"][0]=$attributes["company"]; }
816         if ($attributes["change_password"]){ $mod["pwdLastSet"][0]=0; }
817         if ($attributes["department"]){ $mod["department"][0]=$attributes["department"]; }
818         if ($attributes["description"]){ $mod["description"][0]=$attributes["description"]; }
819         if ($attributes["display_name"]){ $mod["displayName"][0]=$attributes["display_name"]; }
820         if ($attributes["email"]){ $mod["mail"][0]=$attributes["email"]; }
821         if ($attributes["expires"]){ $mod["accountExpires"][0]=$attributes["expires"]; } //unix epoch format?
822         if ($attributes["firstname"]){ $mod["givenName"][0]=$attributes["firstname"]; }
823         if ($attributes["home_directory"]){ $mod["homeDirectory"][0]=$attributes["home_directory"]; }
824         if ($attributes["home_drive"]){ $mod["homeDrive"][0]=$attributes["home_drive"]; }
825         if ($attributes["initials"]){ $mod["initials"][0]=$attributes["initials"]; }
826         if ($attributes["logon_name"]){ $mod["userPrincipalName"][0]=$attributes["logon_name"]; }
827         if ($attributes["manager"]){ $mod["manager"][0]=$attributes["manager"]; }  //UNTESTED ***Use DistinguishedName***
828         if ($attributes["office"]){ $mod["physicalDeliveryOfficeName"][0]=$attributes["office"]; }
829         if ($attributes["password"]){ $mod["unicodePwd"][0]=$this->user()->encodePassword($attributes["password"]); }
830         if ($attributes["profile_path"]){ $mod["profilepath"][0]=$attributes["profile_path"]; }
831         if ($attributes["script_path"]){ $mod["scriptPath"][0]=$attributes["script_path"]; }
832         if ($attributes["surname"]){ $mod["sn"][0]=$attributes["surname"]; }
833         if ($attributes["title"]){ $mod["title"][0]=$attributes["title"]; }
834         if ($attributes["telephone"]){ $mod["telephoneNumber"][0]=$attributes["telephone"]; }
835         if ($attributes["mobile"]){ $mod["mobile"][0]=$attributes["mobile"]; }
836         if ($attributes["pager"]){ $mod["pager"][0]=$attributes["pager"]; }
837         if ($attributes["ipphone"]){ $mod["ipphone"][0]=$attributes["ipphone"]; }
838         if ($attributes["web_page"]){ $mod["wWWHomePage"][0]=$attributes["web_page"]; }
839         if ($attributes["fax"]){ $mod["facsimileTelephoneNumber"][0]=$attributes["fax"]; }
840         if ($attributes["enabled"]){ $mod["userAccountControl"][0]=$attributes["enabled"]; }
841         if ($attributes["homephone"]){ $mod["homephone"][0]=$attributes["homephone"]; }
842 
843         // Distribution List specific schema
844         if ($attributes["group_sendpermission"]){ $mod["dlMemSubmitPerms"][0]=$attributes["group_sendpermission"]; }
845         if ($attributes["group_rejectpermission"]){ $mod["dlMemRejectPerms"][0]=$attributes["group_rejectpermission"]; }
846 
847         // Exchange Schema
848         if ($attributes["exchange_homemdb"]){ $mod["homeMDB"][0]=$attributes["exchange_homemdb"]; }
849         if ($attributes["exchange_mailnickname"]){ $mod["mailNickname"][0]=$attributes["exchange_mailnickname"]; }
850         if ($attributes["exchange_proxyaddress"]){ $mod["proxyAddresses"][0]=$attributes["exchange_proxyaddress"]; }
851         if ($attributes["exchange_usedefaults"]){ $mod["mDBUseDefaults"][0]=$attributes["exchange_usedefaults"]; }
852         if ($attributes["exchange_policyexclude"]){ $mod["msExchPoliciesExcluded"][0]=$attributes["exchange_policyexclude"]; }
853         if ($attributes["exchange_policyinclude"]){ $mod["msExchPoliciesIncluded"][0]=$attributes["exchange_policyinclude"]; }
854         if ($attributes["exchange_addressbook"]){ $mod["showInAddressBook"][0]=$attributes["exchange_addressbook"]; }
855         if ($attributes["exchange_altrecipient"]){ $mod["altRecipient"][0]=$attributes["exchange_altrecipient"]; }
856         if ($attributes["exchange_deliverandredirect"]){ $mod["deliverAndRedirect"][0]=$attributes["exchange_deliverandredirect"]; }
857 
858         // This schema is designed for contacts
859         if ($attributes["exchange_hidefromlists"]){ $mod["msExchHideFromAddressLists"][0]=$attributes["exchange_hidefromlists"]; }
860         if ($attributes["contact_email"]){ $mod["targetAddress"][0]=$attributes["contact_email"]; }
861 
862         //echo ("<pre>"); print_r($mod);
863         /*
864         // modifying a name is a bit fiddly
865         if ($attributes["firstname"] && $attributes["surname"]){
866             $mod["cn"][0]=$attributes["firstname"]." ".$attributes["surname"];
867             $mod["displayname"][0]=$attributes["firstname"]." ".$attributes["surname"];
868             $mod["name"][0]=$attributes["firstname"]." ".$attributes["surname"];
869         }
870         */
871 
872         if (count($mod)==0){ return (false); }
873         return ($mod);
874     }
875 
876     /**
877     * Convert 8bit characters e.g. accented characters to UTF8 encoded characters
878     */
879     protected function encode8Bit(&$item, $key) {
880         $encode = false;
881         if (is_string($item)) {
882             for ($i=0; $i<strlen($item); $i++) {
883                 if (ord($item[$i]) >> 7) {
884                     $encode = true;
885                 }
886             }
887         }
888         if ($encode === true && $key != 'password') {
889             $item = utf8_encode($item);
890         }
891     }
892 
893     /**
894     * Select a random domain controller from your domain controller array
895     *
896     * @return string
897     */
898     protected function randomController()
899     {
900         mt_srand(doubleval(microtime()) * 100000000); // For older PHP versions
901         /*if (sizeof($this->domainControllers) > 1) {
902             $adController = $this->domainControllers[array_rand($this->domainControllers)];
903             // Test if the controller is responding to pings
904             $ping = $this->pingController($adController);
905             if ($ping === false) {
906                 // Find the current key in the domain controllers array
907                 $key = array_search($adController, $this->domainControllers);
908                 // Remove it so that we don't end up in a recursive loop
909                 unset($this->domainControllers[$key]);
910                 // Select a new controller
911                 return $this->randomController();
912             }
913             else {
914                 return ($adController);
915             }
916         } */
917         return $this->domainControllers[array_rand($this->domainControllers)];
918     }
919 
920     /**
921     * Test basic connectivity to controller
922     *
923     * @return bool
924     */
925     protected function pingController($host) {
926         $port = $this->adPort;
927         fsockopen($host, $port, $errno, $errstr, 10);
928         if ($errno > 0) {
929             return false;
930         }
931         return true;
932     }
933 
934 }
935 
936 /**
937 * adLDAP Exception Handler
938 *
939 * Exceptions of this type are thrown on bind failure or when SSL is required but not configured
940 * Example:
941 * try {
942 *   $adldap = new adLDAP();
943 * }
944 * catch (adLDAPException $e) {
945 *   echo $e;
946 *   exit();
947 * }
948 */
949 class adLDAPException extends Exception {}
950