You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LDAP3.php 108KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139
  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | Net/LDAP3.php |
  5. | |
  6. | Based on code created by the Roundcube Webmail team. |
  7. | |
  8. | Copyright (C) 2006-2014, The Roundcube Dev Team |
  9. | Copyright (C) 2012-2014, Kolab Systems AG |
  10. | |
  11. | This program is free software: you can redistribute it and/or modify |
  12. | it under the terms of the GNU General Public License as published by |
  13. | the Free Software Foundation, either version 3 of the License, or |
  14. | (at your option) any later version. |
  15. | |
  16. | This program is distributed in the hope that it will be useful, |
  17. | but WITHOUT ANY WARRANTY; without even the implied warranty of |
  18. | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
  19. | GNU General Public License for more details. |
  20. | |
  21. | You should have received a copy of the GNU General Public License |
  22. | along with this program. If not, see <http://www.gnu.org/licenses/>. |
  23. | |
  24. | PURPOSE: |
  25. | Provide advanced functionality for accessing LDAP directories |
  26. | |
  27. +-----------------------------------------------------------------------+
  28. | Authors: Thomas Bruederli <roundcube@gmail.com> |
  29. | Aleksander Machniak <machniak@kolabsys.com> |
  30. | Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> |
  31. +-----------------------------------------------------------------------+
  32. */
  33. require_once __DIR__ . '/LDAP3/Result.php';
  34. /**
  35. * Model class to access a LDAP directories
  36. *
  37. * @package Net_LDAP3
  38. */
  39. class Net_LDAP3
  40. {
  41. public $conn;
  42. public $vlv_active = false;
  43. private $attribute_level_rights_map = array(
  44. "r" => "read",
  45. "s" => "search",
  46. "w" => "write",
  47. "o" => "delete",
  48. "c" => "compare",
  49. "W" => "write",
  50. "O" => "delete"
  51. );
  52. private $entry_level_rights_map = array(
  53. "a" => "add",
  54. "d" => "delete",
  55. "n" => "modrdn",
  56. "v" => "read"
  57. );
  58. /*
  59. * Manipulate configuration through the config_set and config_get methods.
  60. * Available options:
  61. * 'debug' => false,
  62. * 'hosts' => array(),
  63. * 'port' => 389,
  64. * 'use_tls' => false,
  65. * 'ldap_version' => 3, // using LDAPv3
  66. * 'auth_method' => '', // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
  67. * 'gssapi_cn' => null // Kerberos cache name (KRB5CCNAME) for SASL GSSAPI authentication
  68. * 'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
  69. * 'referrals' => false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
  70. * 'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
  71. * 'sizelimit' => 0, // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
  72. * 'timelimit' => 0, // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
  73. * 'vlv' => false, // force VLV off
  74. * 'config_root_dn' => 'cn=config', // Root DN to read config (e.g. vlv indexes) from
  75. * 'service_bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
  76. * 'service_bind_pw' => 'Welcome2KolabSystems',
  77. * 'root_dn' => 'dc=example,dc=org',
  78. */
  79. protected $config = array(
  80. 'sizelimit' => 0,
  81. 'timelimit' => 0,
  82. );
  83. protected $debug_level = false;
  84. protected $list_page = 1;
  85. protected $page_size = 10;
  86. protected $icache = array();
  87. protected $cache;
  88. // Use public method config_set('log_hook', $callback) to have $callback be
  89. // call_user_func'ed instead of the local log functions.
  90. protected $_log_hook;
  91. // Use public method config_set('config_get_hook', $callback) to have
  92. // $callback be call_user_func'ed instead of the local config_get function.
  93. protected $_config_get_hook;
  94. // Use public method config_set('config_set_hook', $callback) to have
  95. // $callback be call_user_func'ed instead of the local config_set function.
  96. protected $_config_set_hook;
  97. // Not Yet Implemented
  98. // Intended to allow hooking in for the purpose of caching.
  99. protected $_result_hook;
  100. // Runtime. These are not the variables you're looking for.
  101. protected $_current_bind_dn;
  102. protected $_current_bind_pw;
  103. protected $_current_host;
  104. protected $_supported_control = array();
  105. protected $_vlv_indexes_and_searches;
  106. /**
  107. * Constructor
  108. *
  109. * @param array $config Configuration parameters that have not already
  110. * been initialized. For configuration parameters
  111. * that have in fact been set, use the config_set()
  112. * method after initialization.
  113. */
  114. public function __construct($config = array())
  115. {
  116. if (!empty($config) && is_array($config)) {
  117. foreach ($config as $key => $value) {
  118. if (empty($this->config[$key])) {
  119. $setter = 'config_set_' . $key;
  120. if (method_exists($this, $setter)) {
  121. $this->$setter($value);
  122. }
  123. else if (isset($this->$key)) {
  124. $this->$key = $value;
  125. }
  126. else {
  127. $this->config[$key] = $value;
  128. }
  129. }
  130. }
  131. }
  132. }
  133. /**
  134. * Add multiple entries to the directory information tree in one go.
  135. */
  136. public function add_entries($entries, $attributes = array())
  137. {
  138. // If $entries is an associative array, it's keys are DNs and its
  139. // values are the attributes for that DN.
  140. //
  141. // If $entries is a non-associative array, the attributes are expected
  142. // to be positional in $attributes.
  143. $result_set = array();
  144. if (array_keys($entries) == range(0, count($entries) - 1)) {
  145. // $entries is sequential
  146. if (count($entries) !== count($attributes)) {
  147. $this->_error("LDAP: Wrong entry/attribute count in " . __FUNCTION__);
  148. return false;
  149. }
  150. for ($i = 0; $i < count($entries); $i++) {
  151. $result_set[$i] = $this->add_entry($entries[$i], $attributes[$i]);
  152. }
  153. }
  154. else {
  155. // $entries is associative
  156. foreach ($entries as $entry_dn => $entry_attributes) {
  157. if (array_keys($attributes) !== range(0, count($attributes)-1)) {
  158. // $attributes is associative as well, let's merge these
  159. //
  160. // $entry_attributes takes precedence, so is in the second
  161. // position in array_merge()
  162. $entry_attributes = array_merge($attributes, $entry_attributes);
  163. }
  164. $result_set[$entry_dn] = $this->add_entry($entry_dn, $entry_attributes);
  165. }
  166. }
  167. return $result_set;
  168. }
  169. /**
  170. * Add an entry to the directory information tree.
  171. */
  172. public function add_entry($entry_dn, $attributes)
  173. {
  174. // TODO:
  175. // - Get entry rdn attribute value from entry_dn and see if it exists in
  176. // attributes -> issue warning if so (but not block the operation).
  177. $this->_debug("Entry DN", $entry_dn);
  178. $this->_debug("Attributes", $attributes);
  179. foreach ($attributes as $attr_name => $attr_value) {
  180. if (empty($attr_value)) {
  181. unset($attributes[$attr_name]);
  182. } else if (is_array($attr_value)) {
  183. $attributes[$attr_name] = array_values($attr_value);
  184. }
  185. }
  186. $this->_debug("C: Add $entry_dn: " . json_encode($attributes));
  187. if (!ldap_add($this->conn, $entry_dn, $attributes)) {
  188. $this->_debug("S: " . ldap_error($this->conn));
  189. $this->_warning("LDAP: Adding entry $entry_dn failed. " . ldap_error($this->conn));
  190. return false;
  191. }
  192. $this->_debug("S: OK");
  193. return true;
  194. }
  195. /**
  196. * Add replication agreements and initialize the consumer(s) for
  197. * $domain_root_dn.
  198. *
  199. * Searches the configured replicas for any of the current domain/config
  200. * databases, and uses this information to configure the additional
  201. * replication for the (new) domain database (at $domain_root_dn).
  202. *
  203. * Very specific to Netscape-based directory servers, and currently also
  204. * very specific to multi-master replication.
  205. */
  206. public function add_replication_agreements($domain_root_dn)
  207. {
  208. $replica_hosts = $this->list_replicas();
  209. if (empty($replica_hosts)) {
  210. return;
  211. }
  212. $result = $this->search($this->config_get('config_root_dn'), "(&(objectclass=nsDS5Replica)(nsDS5ReplicaType=3))", "sub");
  213. if (!$result) {
  214. $this->_debug("No replication configuration found.");
  215. return;
  216. }
  217. // Preserve the number of replicated databases we have, because the replication ID
  218. // can be calculated from the number of databases replicated, and the number of
  219. // servers.
  220. $num_replica_dbs = $result->count();
  221. $replicas = $result->entries(true);
  222. $max_replica_agreements = 0;
  223. foreach ($replicas as $replica_dn => $replica_attrs) {
  224. $result = $this->search($replica_dn, "(objectclass=nsDS5ReplicationAgreement)", "sub");
  225. if ($result) {
  226. if ($max_replica_agreements < $result->count()) {
  227. $max_replica_agreements = $result->count();
  228. $max_replica_agreements_dn = $replica_dn;
  229. }
  230. }
  231. }
  232. $max_repl_id = $num_replica_dbs * count($replica_hosts);
  233. $this->_debug("The current maximum replication ID is $max_repl_id");
  234. $this->_debug("The current maximum number of replication agreements for any database is $max_replica_agreements (for $max_replica_agreements_dn)");
  235. $this->_debug("With " . count($replica_hosts) . " replicas, the next is " . ($max_repl_id + 1) . " and the last one is " . ($max_repl_id + count($replica_hosts)));
  236. // Then add the replication agreements
  237. foreach ($replica_hosts as $num => $replica_host) {
  238. $ldap = new Net_LDAP3($this->config);
  239. $ldap->config_set('hosts', array($replica_host));
  240. $ldap->connect();
  241. $ldap->bind($this->_current_bind_dn, $this->_current_bind_pw);
  242. $replica_attrs = array(
  243. 'cn' => 'replica',
  244. 'objectclass' => array(
  245. 'top',
  246. 'nsds5replica',
  247. 'extensibleobject',
  248. ),
  249. 'nsDS5ReplicaBindDN' => $ldap->get_entry_attribute($replica_dn, "nsDS5ReplicaBindDN"),
  250. 'nsDS5ReplicaId' => ($max_repl_id + $num + 1),
  251. 'nsDS5ReplicaRoot' => $domain_root_dn,
  252. 'nsDS5ReplicaType' => $ldap->get_entry_attribute($replica_dn, "nsDS5ReplicaType"),
  253. 'nsds5ReplicaPurgeDelay' => $ldap->get_entry_attribute($replica_dn, "nsds5ReplicaPurgeDelay"),
  254. 'nsDS5Flags' => $ldap->get_entry_attribute($replica_dn, "nsDS5Flags")
  255. );
  256. $new_replica_dn = 'cn=replica,cn="' . $domain_root_dn . '",cn=mapping tree,cn=config';
  257. $this->_debug("Adding $new_replica_dn to $replica_host with attributes: " . var_export($replica_attrs, true));
  258. $result = $ldap->add_entry($new_replica_dn, $replica_attrs);
  259. if (!$result) {
  260. $this->_error("LDAP: Could not add replication configuration to database for $domain_root_dn on $replica_host");
  261. continue;
  262. }
  263. $result = $ldap->search($replica_dn, "(objectclass=nsDS5ReplicationAgreement)", "sub");
  264. if (!$result) {
  265. $this->_error("LDAP: Host $replica_host does not have any replication agreements");
  266. continue;
  267. }
  268. $entries = $result->entries(true);
  269. $replica_agreement_tpl_dn = key($entries);
  270. $this->_debug("Using " . var_export($replica_agreement_tpl_dn, true) . " as the template for new replication agreements");
  271. foreach ($replica_hosts as $replicate_to_host) {
  272. // Skip the current server
  273. if ($replicate_to_host == $replica_host) {
  274. continue;
  275. }
  276. $this->_debug("Adding a replication agreement for $domain_root_dn to $replicate_to_host on " . $replica_host);
  277. $attrs = array(
  278. 'objectclass',
  279. 'nsDS5ReplicaBindDN',
  280. 'nsDS5ReplicaCredentials',
  281. 'nsDS5ReplicaTransportInfo',
  282. 'nsDS5ReplicaBindMethod',
  283. 'nsDS5ReplicaHost',
  284. 'nsDS5ReplicaPort'
  285. );
  286. $replica_agreement_attrs = $ldap->get_entry_attributes($replica_agreement_tpl_dn, $attrs);
  287. $replica_agreement_attrs['cn'] = array_shift(explode('.', $replicate_to_host)) . str_replace(array('dc=',','), array('_',''), $domain_root_dn);
  288. $replica_agreement_attrs['nsDS5ReplicaRoot'] = $domain_root_dn;
  289. $replica_agreement_dn = "cn=" . $replica_agreement_attrs['cn'] . "," . $new_replica_dn;
  290. $this->_debug("Adding $replica_agreement_dn to $replica_host with attributes: " . var_export($replica_agreement_attrs, true));
  291. $result = $ldap->add_entry($replica_agreement_dn, $replica_agreement_attrs);
  292. if (!$result) {
  293. $this->_error("LDAP: Failed adding $replica_agreement_dn");
  294. }
  295. }
  296. }
  297. $server_id = implode('', array_diff($replica_hosts, $this->_server_id_not));
  298. $this->_debug("About to trigger consumer initialization for replicas on current 'parent': $server_id");
  299. $result = $this->search($this->config_get('config_root_dn'), "(&(objectclass=nsDS5ReplicationAgreement)(nsds5replicaroot=$domain_root_dn))", "sub");
  300. if ($result) {
  301. foreach ($result->entries(true) as $agreement_dn => $agreement_attrs) {
  302. $this->modify_entry_attributes(
  303. $agreement_dn,
  304. array(
  305. 'replace' => array(
  306. 'nsds5BeginReplicaRefresh' => 'start',
  307. ),
  308. )
  309. );
  310. }
  311. }
  312. }
  313. public function attribute_details($attributes = array())
  314. {
  315. $schema = $this->init_schema();
  316. if (!$schema) {
  317. return array();
  318. }
  319. $attribs = $schema->getAll('attributes');
  320. $attributes_details = array();
  321. foreach ($attributes as $attribute) {
  322. if (array_key_exists($attribute, $attribs)) {
  323. $attrib_details = $attribs[$attribute];
  324. if (!empty($attrib_details['sup'])) {
  325. foreach ($attrib_details['sup'] as $super_attrib) {
  326. $_attrib_details = $attribs[$super_attrib];
  327. if (is_array($_attrib_details)) {
  328. $attrib_details = array_merge($_attrib_details, $attrib_details);
  329. }
  330. }
  331. }
  332. }
  333. else if (array_key_exists(strtolower($attribute), $attribs)) {
  334. $attrib_details = $attribs[strtolower($attribute)];
  335. if (!empty($attrib_details['sup'])) {
  336. foreach ($attrib_details['sup'] as $super_attrib) {
  337. $_attrib_details = $attribs[$super_attrib];
  338. if (is_array($_attrib_details)) {
  339. $attrib_details = array_merge($_attrib_details, $attrib_details);
  340. }
  341. }
  342. }
  343. }
  344. else {
  345. $this->_warning("LDAP: No schema details exist for attribute $attribute (which is strange)");
  346. }
  347. // The relevant parts only, please
  348. $attributes_details[$attribute] = array(
  349. 'type' => !empty($attrib_details['single-value']) ? 'text' : 'list',
  350. 'description' => $attrib_details['desc'],
  351. 'syntax' => $attrib_details['syntax'],
  352. 'max-length' => $attrib_details['max-length'] ?: false,
  353. );
  354. }
  355. return $attributes_details;
  356. }
  357. public function attributes_allowed($objectclasses = array())
  358. {
  359. $this->_debug("Listing allowed_attributes for objectclasses", $objectclasses);
  360. if (!is_array($objectclasses) || empty($objectclasses)) {
  361. return false;
  362. }
  363. $schema = $this->init_schema();
  364. if (!$schema) {
  365. return false;
  366. }
  367. $may = array();
  368. $must = array();
  369. $superclasses = array();
  370. foreach ($objectclasses as $objectclass) {
  371. $superclass = $schema->superclass($objectclass);
  372. if (!empty($superclass)) {
  373. $superclasses = array_merge($superclass, $superclasses);
  374. }
  375. $_may = $schema->may($objectclass);
  376. $_must = $schema->must($objectclass);
  377. if (is_array($_may)) {
  378. $may = array_merge($may, $_may);
  379. }
  380. if (is_array($_must)) {
  381. $must = array_merge($must, $_must);
  382. }
  383. }
  384. $may = array_unique($may);
  385. $must = array_unique($must);
  386. $superclasses = array_unique($superclasses);
  387. return array('may' => $may, 'must' => $must, 'super' => $superclasses);
  388. }
  389. public function classes_allowed()
  390. {
  391. $schema = $this->init_schema();
  392. if (!$schema) {
  393. return false;
  394. }
  395. $list = $schema->getAll('objectclasses');
  396. $classes = array();
  397. foreach ($list as $class) {
  398. $classes[] = $class['name'];
  399. }
  400. return $classes;
  401. }
  402. /**
  403. * Bind connection with DN and password
  404. *
  405. * @param string $dn Bind DN
  406. * @param string $pass Bind password
  407. *
  408. * @return boolean True on success, False on error
  409. */
  410. public function bind($bind_dn, $bind_pw)
  411. {
  412. if (!$this->conn) {
  413. return false;
  414. }
  415. if ($bind_dn == $this->_current_bind_dn) {
  416. return true;
  417. }
  418. $this->_debug("C: Bind [dn: $bind_dn]");
  419. if (@ldap_bind($this->conn, $bind_dn, $bind_pw)) {
  420. $this->_debug("S: OK");
  421. $this->_current_bind_dn = $bind_dn;
  422. $this->_current_bind_pw = $bind_pw;
  423. return true;
  424. }
  425. $this->_debug("S: ".ldap_error($this->conn));
  426. $this->_error("LDAP: Bind failed for dn=$bind_dn. ".ldap_error($this->conn));
  427. return false;
  428. }
  429. /**
  430. * Close connection to LDAP server
  431. */
  432. public function close()
  433. {
  434. if ($this->conn) {
  435. $this->_debug("C: Close");
  436. ldap_unbind($this->conn);
  437. $this->_current_bind_dn = null;
  438. $this->_current_bind_pw = null;
  439. $this->conn = null;
  440. }
  441. }
  442. /**
  443. * Get the value of a configuration item.
  444. *
  445. * @param string $key Configuration key
  446. * @param mixed $default Default value to return
  447. */
  448. public function config_get($key, $default = null)
  449. {
  450. if (!empty($this->_config_get_hook)) {
  451. return call_user_func_array($this->_config_get_hook, array($key, $value));
  452. }
  453. else if (method_exists($this, "config_get_{$key}")) {
  454. return call_user_func(array($this, "config_get_$key"), $value);
  455. }
  456. else if (!isset($this->config[$key])) {
  457. return $default;
  458. }
  459. else {
  460. return $this->config[$key];
  461. }
  462. }
  463. /**
  464. * Set a configuration item to value.
  465. *
  466. * @param string $key Configuration key
  467. * @param mixed $value Configuration value
  468. */
  469. public function config_set($key, $value = null)
  470. {
  471. if (is_array($key)) {
  472. foreach ($key as $k => $v) {
  473. $this->config_set($k, $v);
  474. }
  475. return;
  476. }
  477. if (!empty($this->_config_set_hook)) {
  478. call_user_func($this->_config_set_hook, array($key, $value));
  479. }
  480. else if (method_exists($this, "config_set_{$key}")) {
  481. call_user_func_array(array($this, "config_set_$key"), array($value));
  482. }
  483. else if (property_exists($this, $key)) {
  484. $this->$key = $value;
  485. }
  486. else {
  487. // 'host' option is deprecated
  488. if ($key == 'host') {
  489. $this->config['hosts'] = (array) $value;
  490. }
  491. else {
  492. $this->config[$key] = $value;
  493. }
  494. }
  495. }
  496. /**
  497. * Establish a connection to the LDAP server
  498. */
  499. public function connect($host = null)
  500. {
  501. if (!function_exists('ldap_connect')) {
  502. $this->_error("No ldap support in this PHP installation");
  503. return false;
  504. }
  505. if (is_resource($this->conn)) {
  506. $this->_debug("Connection already exists");
  507. return true;
  508. }
  509. $hosts = !empty($host) ? $host : $this->config_get('hosts', array());
  510. $port = $this->config_get('port', 389);
  511. foreach ((array) $hosts as $host) {
  512. $this->_debug("C: Connect [$host:$port]");
  513. if ($lc = @ldap_connect($host, $port)) {
  514. if ($this->config_get('use_tls', false) === true) {
  515. if (!ldap_start_tls($lc)) {
  516. $this->_debug("S: Could not start TLS. " . ldap_error($lc));
  517. continue;
  518. }
  519. }
  520. $this->_debug("S: OK");
  521. $ldap_version = $this->config_get('ldap_version', 3);
  522. $timeout = $this->config_get('network_timeout');
  523. $referrals = $this->config_get('referrals');
  524. ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $ldap_version);
  525. if ($timeout) {
  526. ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $timeout);
  527. }
  528. if ($referrals !== null) {
  529. ldap_set_option($lc, LDAP_OPT_REFERRALS, (bool) $referrals);
  530. }
  531. $this->_current_host = $host;
  532. $this->conn = $lc;
  533. break;
  534. }
  535. $this->_debug("S: NOT OK");
  536. }
  537. if (!is_resource($this->conn)) {
  538. $this->_error("Could not connect to LDAP");
  539. return false;
  540. }
  541. return true;
  542. }
  543. /**
  544. * Shortcut to ldap_delete()
  545. */
  546. public function delete_entry($entry_dn)
  547. {
  548. $this->_debug("C: Delete $entry_dn");
  549. if (ldap_delete($this->conn, $entry_dn) === false) {
  550. $this->_debug("S: " . ldap_error($this->conn));
  551. $this->_warning("LDAP: Removing entry $entry_dn failed. " . ldap_error($this->conn));
  552. return false;
  553. }
  554. $this->_debug("S: OK");
  555. return true;
  556. }
  557. /**
  558. * Deletes specified entry and all entries in the tree
  559. */
  560. public function delete_entry_recursive($entry_dn)
  561. {
  562. // searching for sub entries, but not scope sub, just one level
  563. $result = $this->search($entry_dn, '(|(objectclass=*)(objectclass=ldapsubentry))', 'one');
  564. if ($result) {
  565. $entries = $result->entries(true);
  566. foreach (array_keys($entries) as $sub_dn) {
  567. if (!$this->delete_entry_recursive($sub_dn)) {
  568. return false;
  569. }
  570. }
  571. }
  572. if (!$this->delete_entry($entry_dn)) {
  573. return false;
  574. }
  575. return true;
  576. }
  577. public function effective_rights($subject)
  578. {
  579. $effective_rights_control_oid = "1.3.6.1.4.1.42.2.27.9.5.2";
  580. $supported_controls = $this->supported_controls();
  581. if (!in_array($effective_rights_control_oid, $supported_controls)) {
  582. $this->_debug("LDAP: No getEffectiveRights control in supportedControls");
  583. return false;
  584. }
  585. $attributes = array(
  586. 'attributeLevelRights' => array(),
  587. 'entryLevelRights' => array(),
  588. );
  589. $entry_dn = $this->entry_dn($subject);
  590. if (!$entry_dn) {
  591. $entry_dn = $this->config_get($subject . "_base_dn");
  592. }
  593. if (!$entry_dn) {
  594. $entry_dn = $this->config_get("base_dn");
  595. }
  596. if (!$entry_dn) {
  597. $entry_dn = $this->config_get("root_dn");
  598. }
  599. $this->_debug("effective_rights for subject $subject resolves to entry dn $entry_dn");
  600. $moz_ldapsearch = "/usr/lib64/mozldap/ldapsearch";
  601. if (!is_file($moz_ldapsearch)) {
  602. $moz_ldapsearch = "/usr/lib/mozldap/ldapsearch";
  603. }
  604. if (!is_file($moz_ldapsearch)) {
  605. $moz_ldapsearch = null;
  606. }
  607. if (empty($moz_ldapsearch)) {
  608. $this->_error("Mozilla LDAP C SDK binary ldapsearch not found, cannot get effective rights on subject $subject");
  609. return null;
  610. }
  611. $output = array();
  612. $command = Array(
  613. $moz_ldapsearch,
  614. '-x',
  615. '-h',
  616. $this->_current_host,
  617. '-p',
  618. $this->config_get('port', 389),
  619. '-b',
  620. escapeshellarg($entry_dn),
  621. '-s',
  622. 'base',
  623. '-D',
  624. escapeshellarg($this->_current_bind_dn),
  625. '-w',
  626. escapeshellarg($this->_current_bind_pw)
  627. );
  628. if ($this->vendor_name() == "Oracle Corporation") {
  629. // For Oracle DSEE
  630. $command[] = "-J";
  631. $command[] = escapeshellarg(
  632. implode(
  633. ':',
  634. Array(
  635. $effective_rights_control_oid, // OID
  636. 'true' // Criticality
  637. )
  638. )
  639. );
  640. $command[] = "-c";
  641. $command[] = escapeshellarg(
  642. 'dn:' . $this->_current_bind_dn
  643. );
  644. } else {
  645. // For 389 DS:
  646. $command[] = "-J";
  647. $command[] = escapeshellarg(
  648. implode(
  649. ':',
  650. Array(
  651. $effective_rights_control_oid, // OID
  652. 'true', // Criticality
  653. 'dn:' . $this->_current_bind_dn // User DN
  654. )
  655. )
  656. );
  657. }
  658. // For both
  659. $command[] = '"(objectclass=*)"';
  660. $command[] = '"*"';
  661. if ($this->vendor_name() == "Oracle Corporation") {
  662. // Oracle DSEE
  663. $command[] = 'aclRights';
  664. }
  665. // remove password from debug log
  666. $command_debug = $command;
  667. $command_debug[13] = '*';
  668. $command = implode(' ', $command);
  669. $command_debug = implode(' ', $command_debug);
  670. $this->_debug("LDAP: Executing command: $command_debug");
  671. exec($command, $output, $return_code);
  672. $this->_debug("LDAP: Command output:" . var_export($output, true));
  673. $this->_debug("Return code: " . $return_code);
  674. if ($return_code) {
  675. $this->_error("Command $moz_ldapsearch returned error code: $return_code");
  676. return null;
  677. }
  678. $lines = array();
  679. foreach ($output as $line_num => $line) {
  680. if (substr($line, 0, 1) == " ") {
  681. $lines[count($lines)-1] .= trim($line);
  682. }
  683. else {
  684. $lines[] = trim($line);
  685. }
  686. }
  687. if ($this->vendor_name() == "Oracle Corporation") {
  688. // Example for attribute level rights:
  689. // aclRights;attributeLevel;$attr:$right:$bool,$right:$bool
  690. // Example for entry level rights:
  691. // aclRights;entryLevel: add:1,delete:1,read:1,write:1,proxy:1
  692. foreach ($lines as $line) {
  693. $line_components = explode(':', $line);
  694. $attribute_name = explode(';', array_shift($line_components));
  695. switch ($attribute_name[0]) {
  696. case "aclRights":
  697. $this->parse_aclrights($attributes, $line);
  698. break;
  699. case "dn":
  700. $attributes[$attribute_name[0]] = trim(implode(';', $line_components));
  701. break;
  702. default:
  703. break;
  704. }
  705. }
  706. } else {
  707. foreach ($lines as $line) {
  708. $line_components = explode(':', $line);
  709. $attribute_name = array_shift($line_components);
  710. $attribute_value = trim(implode(':', $line_components));
  711. switch ($attribute_name) {
  712. case "attributeLevelRights":
  713. $attributes[$attribute_name] = $this->parse_attribute_level_rights($attribute_value);
  714. break;
  715. case "dn":
  716. $attributes[$attribute_name] = $attribute_value;
  717. break;
  718. case "entryLevelRights":
  719. $attributes[$attribute_name] = $this->parse_entry_level_rights($attribute_value);
  720. break;
  721. default:
  722. break;
  723. }
  724. }
  725. }
  726. return $attributes;
  727. }
  728. /**
  729. * Resolve entry data to entry DN
  730. *
  731. * @param string $subject Entry string (e.g. entry DN or unique attribute value)
  732. * @param array $attributes Additional attributes
  733. * @param string $base_dn Optional base DN
  734. *
  735. * @return string Entry DN string
  736. */
  737. public function entry_dn($subject, $attributes = array(), $base_dn = null)
  738. {
  739. $this->_debug("Net_LDAP3::entry_dn($subject)");
  740. $is_dn = ldap_explode_dn($subject, 1);
  741. if (is_array($is_dn) && array_key_exists("count", $is_dn) && $is_dn["count"] > 0) {
  742. $this->_debug("$subject is a dn");
  743. return $subject;
  744. }
  745. $this->_debug("$subject is not a dn");
  746. if (strlen($subject) < 32 || preg_match('/[^a-fA-F0-9-]/', $subject)) {
  747. $this->_debug("$subject is not a unique identifier");
  748. return;
  749. }
  750. $unique_attr = $this->config_get('unique_attribute', 'nsuniqueid');
  751. $this->_debug("Using unique_attribute " . var_export($unique_attr, true) . " at " . __FILE__ . ":" . __LINE__);
  752. $attributes = array_merge(array($unique_attr => $subject), (array)$attributes);
  753. $subject = $this->entry_find_by_attribute($attributes, $base_dn);
  754. if (!empty($subject)) {
  755. return key($subject);
  756. }
  757. }
  758. public function entry_find_by_attribute($attributes, $base_dn = null)
  759. {
  760. $this->_debug("Net_LDAP3::entry_find_by_attribute(\$attributes, \$base_dn) called with base_dn", $base_dn, "and attributes", $attributes);
  761. if (empty($attributes) || !is_array($attributes)) {
  762. return false;
  763. }
  764. if (empty($attributes[key($attributes)])) {
  765. return false;
  766. }
  767. $filter = count($attributes) ? "(&" : "";
  768. foreach ($attributes as $key => $value) {
  769. $filter .= "(" . $key . "=" . $value . ")";
  770. }
  771. $filter .= count($attributes) ? ")" : "";
  772. if (empty($base_dn)) {
  773. $base_dn = $this->config_get('root_dn');
  774. $this->_debug("Using base_dn from domain " . $this->domain . ": " . $base_dn);
  775. }
  776. $result = $this->search($base_dn, $filter, 'sub', array_keys($attributes));
  777. if ($result && $result->count() > 0) {
  778. $this->_debug("Results found: " . implode(', ', array_keys($result->entries(true))));
  779. return $result->entries(true);
  780. }
  781. else {
  782. $this->_debug("No result");
  783. return false;
  784. }
  785. }
  786. public function find_user_groups($member_dn)
  787. {
  788. $groups = array();
  789. $root_dn = $this->config_get('root_dn');
  790. // TODO: Do not query for both, it's either one or the other
  791. $entries = $this->search($root_dn, "(|" .
  792. "(&(objectclass=groupofnames)(member=$member_dn))" .
  793. "(&(objectclass=groupofuniquenames)(uniquemember=$member_dn))" .
  794. ")"
  795. );
  796. if ($entries) {
  797. $groups = array_keys($entries->entries(true));
  798. }
  799. return $groups;
  800. }
  801. public function get_entry_attribute($subject_dn, $attribute)
  802. {
  803. $entry = $this->get_entry_attributes($subject_dn, (array)$attribute);
  804. return $entry[strtolower($attribute)];
  805. }
  806. public function get_entry_attributes($subject_dn, $attributes)
  807. {
  808. // @TODO: use get_entry?
  809. $result = $this->search($subject_dn, '(objectclass=*)', 'base', $attributes);
  810. if (!$result) {
  811. return array();
  812. }
  813. $entries = $result->entries(true);
  814. $entry_dn = key($entries);
  815. $entry = $entries[$entry_dn];
  816. return $entry;
  817. }
  818. /**
  819. * Get a specific LDAP entry, identified by its DN
  820. *
  821. * @param string $dn Record identifier
  822. * @param array $attributes Attributes to return
  823. *
  824. * @return array Hash array
  825. */
  826. public function get_entry($dn, $attributes = array())
  827. {
  828. $rec = null;
  829. if ($this->conn && $dn) {
  830. $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
  831. if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $attributes)) {
  832. $this->_debug("S: OK");
  833. if ($entry = ldap_first_entry($this->conn, $ldap_result)) {
  834. $rec = ldap_get_attributes($this->conn, $entry);
  835. }
  836. }
  837. else {
  838. $this->_debug("S: ".ldap_error($this->conn));
  839. $this->_warning("LDAP: Failed to read $dn. " . ldap_error($this->conn));
  840. }
  841. if (!empty($rec)) {
  842. $rec['dn'] = $dn; // Add in the dn for the entry.
  843. }
  844. }
  845. return $rec;
  846. }
  847. public function list_replicas()
  848. {
  849. $this->_debug("Finding replicas for this server.");
  850. // Search any host that is a replica for the current host
  851. $replica_hosts = $this->config_get('replica_hosts', array());
  852. $root_dn = $this->config_get('config_root_dn');
  853. if (!empty($replica_hosts)) {
  854. return $replica_hosts;
  855. }
  856. $ldap = new Net_LDAP3($this->config);
  857. $ldap->connect();
  858. $ldap->bind($this->_current_bind_dn, $this->_current_bind_pw);
  859. $result = $ldap->search($root_dn, '(objectclass=nsds5replicationagreement)', 'sub', array('nsds5replicahost'));
  860. if (!$result) {
  861. $this->_debug("No replicas configured");
  862. return $replica_hosts;
  863. }
  864. $this->_debug("Replication agreements found: " . var_export($result->entries(true), true));
  865. foreach ($result->entries(true) as $dn => $attrs) {
  866. if (!in_array($attrs['nsds5replicahost'], $replica_hosts)) {
  867. $replica_hosts[] = $attrs['nsds5replicahost'];
  868. }
  869. }
  870. // $replica_hosts now holds the IDs of servers we are currently NOT
  871. // connected to. We might need this later in order to set
  872. $this->_server_id_not = $replica_hosts;
  873. $this->_debug("So far, we have the following replicas: " . var_export($replica_hosts, true));
  874. $ldap->close();
  875. foreach ($replica_hosts as $replica_host) {
  876. $ldap->config_set('hosts', array($replica_host));
  877. $ldap->connect();
  878. $ldap->bind($this->_current_bind_dn, $this->_current_bind_pw);
  879. $result = $ldap->search($root_dn, '(objectclass=nsds5replicationagreement)', 'sub', array('nsds5replicahost'));
  880. if (!$result) {
  881. $this->_debug("No replicas configured on $replica_host");
  882. $ldap->close();
  883. continue;
  884. }
  885. foreach ($result->entries(true) as $dn => $attrs) {
  886. if (!in_array($attrs['nsds5replicahost'], $replica_hosts)) {
  887. $replica_hosts[] = $attrs['nsds5replicahost'];
  888. }
  889. }
  890. $ldap->close();
  891. }
  892. $this->config_set('replica_hosts', $replica_hosts);
  893. return $replica_hosts;
  894. }
  895. public function login($username, $password, $domain = null, &$attributes = null)
  896. {
  897. $this->_debug("Net_LDAP3::login($username,***,$domain)");
  898. $_bind_dn = $this->config_get('service_bind_dn');
  899. $_bind_pw = $this->config_get('service_bind_pw');
  900. if (empty($_bind_dn)) {
  901. $this->_debug("No valid service bind dn found.");
  902. return null;
  903. }
  904. if (empty($_bind_pw)) {
  905. $this->_debug("No valid service bind password found.");
  906. return null;
  907. }
  908. $bound = $this->bind($_bind_dn, $_bind_pw);
  909. if (!$bound) {
  910. $this->_debug("Could not bind with service bind credentials.");
  911. return null;
  912. }
  913. $entry_dn = $this->entry_dn($username);
  914. if (!empty($entry_dn)) {
  915. $bound = $this->bind($entry_dn, $password);
  916. if (!$bound) {
  917. $this->_error("LDAP: Could not bind with " . $entry_dn);
  918. return null;
  919. }
  920. // fetch user attributes if requested
  921. if (!empty($attributes)) {
  922. $attributes = $this->get_entry($entry_dn, $attributes);
  923. $attributes = self::normalize_entry($attributes, true);
  924. }
  925. return $entry_dn;
  926. }
  927. $base_dn = $this->config_get('root_dn');
  928. if (empty($base_dn)) {
  929. $this->_debug("Could not get a valid base dn to search.");
  930. return null;
  931. }
  932. $localpart = $username;
  933. if (empty($domain) ) {
  934. if (count(explode('@', $username)) > 1) {
  935. $_parts = explode('@', $username);
  936. $localpart = $_parts[0];
  937. $domain = $_parts[1];
  938. }
  939. else {
  940. $localpart = $username;
  941. $domain = '';
  942. }
  943. }
  944. $realm = $domain;
  945. $filter = $this->config_get("login_filter", null);
  946. if (empty($filter)) {
  947. $filter = $this->config_get("filter", null);
  948. }
  949. if (empty($filter)) {
  950. $filter = "(&(|(mail=%s)(mail=%U@%d)(alias=%s)(alias=%U@%d)(uid=%s))(objectclass=inetorgperson))";
  951. }
  952. $this->_debug("Net::LDAP3::login() original filter: " . $filter);
  953. $replace_patterns = array(
  954. '/%s/' => $username,
  955. '/%d/' => $domain,
  956. '/%U/' => $localpart,
  957. '/%r/' => $realm
  958. );
  959. $filter = preg_replace(array_keys($replace_patterns), array_values($replace_patterns), $filter);
  960. $this->_debug("Net::LDAP3::login() actual filter: " . $filter);
  961. $result = $this->search($base_dn, $filter, 'sub', $attributes);
  962. if (!$result) {
  963. $this->_debug("Could not search $base_dn with $filter");
  964. return null;
  965. }
  966. if ($result->count() > 1) {
  967. $this->_debug("Multiple entries found.");
  968. return null;
  969. }
  970. else if ($result->count() < 1) {
  971. $this->_debug("No entries found.");
  972. return null;
  973. }
  974. $entries = $result->entries(true);
  975. $entry_dn = key($entries);
  976. $bound = $this->bind($entry_dn, $password);
  977. if (!$bound) {
  978. $this->_debug("Could not bind with " . $entry_dn);
  979. return null;
  980. }
  981. // replace attributes list with key-value data
  982. if (!empty($attributes)) {
  983. $attributes = $entries[$entry_dn];
  984. }
  985. return $entry_dn;
  986. }
  987. public function list_group_members($dn, $entry = null, $recurse = true)
  988. {
  989. $this->_debug("Net_LDAP3::list_group_members($dn)");
  990. if (is_array($entry) && in_array('objectclass', $entry)) {
  991. if (!in_array(array('groupofnames', 'groupofuniquenames', 'groupofurls'), $entry['objectclass'])) {
  992. $this->_debug("Called list_group_members on a non-group!");
  993. return array();
  994. }
  995. }
  996. else {
  997. $entry = $this->get_entry($dn, array('member', 'uniquemember', 'memberurl', 'objectclass'));
  998. if (!$entry) {
  999. return array();
  1000. }
  1001. }
  1002. $group_members = array();
  1003. foreach ((array)$entry['objectclass'] as $objectclass) {
  1004. switch (strtolower($objectclass)) {
  1005. case "groupofnames":
  1006. case "kolabgroupofnames":
  1007. $group_members = array_merge($group_members, $this->list_group_member($dn, $entry['member'], $recurse));
  1008. break;
  1009. case "groupofuniquenames":
  1010. case "kolabgroupofuniquenames":
  1011. $group_members = array_merge($group_members, $this->list_group_uniquemember($dn, $entry['uniquemember'], $recurse));
  1012. break;
  1013. case "groupofurls":
  1014. $group_members = array_merge($group_members, $this->list_group_memberurl($dn, $entry['memberurl'], $recurse));
  1015. break;
  1016. }
  1017. }
  1018. return array_values(array_filter($group_members));
  1019. }
  1020. public function modify_entry($subject_dn, $old_attrs, $new_attrs)
  1021. {
  1022. $this->_debug("OLD ATTRIBUTES", $old_attrs);
  1023. $this->_debug("NEW ATTRIBUTES", $new_attrs);
  1024. // TODO: Get $rdn_attr - we have type_id in $new_attrs
  1025. $dn_components = ldap_explode_dn($subject_dn, 0);
  1026. $rdn_components = explode('=', $dn_components[0]);
  1027. $rdn_attr = $rdn_components[0];
  1028. $this->_debug("Net_LDAP3::modify_entry() using rdn attribute: " . $rdn_attr);
  1029. $mod_array = array(
  1030. 'add' => array(), // For use with ldap_mod_add()
  1031. 'del' => array(), // For use with ldap_mod_del()
  1032. 'replace' => array(), // For use with ldap_mod_replace()
  1033. 'rename' => array(), // For use with ldap_rename()
  1034. );
  1035. // This is me cheating. Remove this special attribute.
  1036. if (array_key_exists('ou', $old_attrs) || array_key_exists('ou', $new_attrs)) {
  1037. $old_ou = is_array($old_attrs['ou']) ? array_shift($old_attrs['ou']) : $old_attrs['ou'];
  1038. $new_ou = is_array($new_attrs['ou']) ? array_shift($new_attrs['ou']) : $new_attrs['ou'];
  1039. unset($old_attrs['ou']);
  1040. unset($new_attrs['ou']);
  1041. }
  1042. else {
  1043. $old_ou = null;
  1044. $new_ou = null;
  1045. }
  1046. // Compare each attribute value of the old attrs with the corresponding value
  1047. // in the new attrs, if any.
  1048. foreach ($old_attrs as $attr => $old_attr_value) {
  1049. if (is_array($old_attr_value)) {
  1050. if (count($old_attr_value) == 1) {
  1051. $old_attrs[$attr] = $old_attr_value[0];
  1052. $old_attr_value = $old_attrs[$attr];
  1053. }
  1054. }
  1055. if (array_key_exists($attr, $new_attrs)) {
  1056. if (is_array($new_attrs[$attr])) {
  1057. if (count($new_attrs[$attr]) == 1) {
  1058. $new_attrs[$attr] = $new_attrs[$attr][0];
  1059. }
  1060. }
  1061. if (is_array($old_attrs[$attr]) && is_array($new_attrs[$attr])) {
  1062. $_sort1 = $new_attrs[$attr];
  1063. sort($_sort1);
  1064. $_sort2 = $old_attr_value;
  1065. sort($_sort2);
  1066. }
  1067. else {
  1068. $_sort1 = true;
  1069. $_sort2 = false;
  1070. }
  1071. if ($new_attrs[$attr] !== $old_attr_value && $_sort1 !== $_sort2) {
  1072. $this->_debug("Attribute $attr changed from " . var_export($old_attr_value, true) . " to " . var_export($new_attrs[$attr], true));
  1073. if ($attr === $rdn_attr) {
  1074. $this->_debug("This attribute is the RDN attribute. Let's see if it is multi-valued, and if the original still exists in the new value.");
  1075. if (is_array($old_attrs[$attr])) {
  1076. if (!is_array($new_attrs[$attr])) {
  1077. if (in_array($new_attrs[$attr], $old_attrs[$attr])) {
  1078. // TODO: Need to remove all $old_attrs[$attr] values not equal to $new_attrs[$attr], and not equal to the current $rdn_attr value [0]
  1079. $this->_debug("old attrs. is array, new attrs. is not array. new attr. exists in old attrs.");
  1080. $rdn_attr_value = array_shift($old_attrs[$attr]);
  1081. $_attr_to_remove = array();
  1082. foreach ($old_attrs[$attr] as $value) {
  1083. if (strtolower($value) != strtolower($new_attrs[$attr])) {
  1084. $_attr_to_remove[] = $value;
  1085. }
  1086. }
  1087. $this->_debug("Adding to delete attribute $attr values:" . implode(', ', $_attr_to_remove));
  1088. $mod_array['del'][$attr] = $_attr_to_remove;
  1089. if (strtolower($new_attrs[$attr]) !== strtolower($rdn_attr_value)) {
  1090. $this->_debug("new attrs is not the same as the old rdn value, issuing a rename");
  1091. $mod_array['rename']['dn'] = $subject_dn;
  1092. $mod_array['rename']['new_rdn'] = $rdn_attr . '=' . self::quote_string($new_attrs[$attr], true);
  1093. }
  1094. }
  1095. else {
  1096. $this->_debug("new attrs is not the same as any of the old rdn value, issuing a full rename");
  1097. $mod_array['rename']['dn'] = $subject_dn;
  1098. $mod_array['rename']['new_rdn'] = $rdn_attr . '=' . self::quote_string($new_attrs[$attr], true);
  1099. }
  1100. }
  1101. else {
  1102. // TODO: See if the rdn attr. value is still in $new_attrs[$attr]
  1103. if (in_array($old_attrs[$attr][0], $new_attrs[$attr])) {
  1104. $this->_debug("Simply replacing attr $attr as rdn attr value is preserved.");
  1105. $mod_array['replace'][$attr] = $new_attrs[$attr];
  1106. }
  1107. else {
  1108. // TODO: This fails.
  1109. $mod_array['rename']['dn'] = $subject_dn;
  1110. $mod_array['rename']['new_rdn'] = $rdn_attr . '=' . self::quote_string($new_attrs[$attr][0], true);
  1111. $mod_array['del'][$attr] = $old_attrs[$attr][0];
  1112. }
  1113. }
  1114. }
  1115. else {
  1116. if (!is_array($new_attrs[$attr])) {
  1117. $this->_debug("Renaming " . $old_attrs[$attr] . " to " . $new_attrs[$attr]);
  1118. $mod_array['rename']['dn'] = $subject_dn;
  1119. $mod_array['rename']['new_rdn'] = $rdn_attr . '=' . self::quote_string($new_attrs[$attr], true);
  1120. }
  1121. else {
  1122. $this->_debug("Adding to replace");
  1123. // An additional attribute value is being supplied. Just replace and continue.
  1124. $mod_array['replace'][$attr] = $new_attrs[$attr];
  1125. continue;
  1126. }
  1127. }
  1128. }
  1129. else {
  1130. if (!isset($new_attrs[$attr]) || $new_attrs[$attr] === '' || (is_array($new_attrs[$attr]) && empty($new_attrs[$attr]))) {
  1131. switch ($attr) {
  1132. case "userpassword":
  1133. break;
  1134. default:
  1135. $this->_debug("Adding to del: $attr");
  1136. $mod_array['del'][$attr] = (array)($old_attr_value);
  1137. break;
  1138. }
  1139. }
  1140. else {
  1141. $this->_debug("Adding to replace: $attr");
  1142. $mod_array['replace'][$attr] = (array)($new_attrs[$attr]);
  1143. }
  1144. }
  1145. }
  1146. else {
  1147. $this->_debug("Attribute $attr unchanged");
  1148. }
  1149. }
  1150. else {
  1151. // TODO: Since we're not shipping the entire object back and forth, and only post
  1152. // part of the data... we don't know what is actually removed (think modifiedtimestamp, etc.)
  1153. $this->_debug("Group attribute $attr not mentioned in \$new_attrs..., but not explicitly removed... by assumption");
  1154. }
  1155. }
  1156. foreach ($new_attrs as $attr => $value) {
  1157. // OU's parent base dn
  1158. if ($attr == 'base_dn') {
  1159. continue;
  1160. }
  1161. if (is_array($value)) {
  1162. if (count($value) == 1) {
  1163. $new_attrs[$attr] = $value[0];
  1164. $value = $new_attrs[$attr];
  1165. }
  1166. }
  1167. if (array_key_exists($attr, $old_attrs)) {
  1168. if (is_array($old_attrs[$attr])) {
  1169. if (count($old_attrs[$attr]) == 1) {
  1170. $old_attrs[$attr] = $old_attrs[$attr][0];
  1171. }
  1172. }
  1173. if (is_array($new_attrs[$attr]) && is_array($old_attrs[$attr])) {
  1174. $_sort1 = $old_attrs[$attr];
  1175. sort($_sort1);
  1176. $_sort2 = $value;
  1177. sort($_sort2);
  1178. }
  1179. else {
  1180. $_sort1 = true;
  1181. $_sort2 = false;
  1182. }
  1183. if ($value === null || $value === '' || (is_array($value) && empty($value))) {
  1184. if (!array_key_exists($attr, $mod_array['del'])) {
  1185. switch ($attr) {
  1186. case 'userpassword':
  1187. break;
  1188. default:
  1189. $this->_debug("Adding to del(2): $attr");
  1190. $mod_array['del'][$attr] = (array)($old_attrs[$attr]);
  1191. break;
  1192. }
  1193. }
  1194. }
  1195. else {
  1196. if (!($old_attrs[$attr] === $value) && !($attr === $rdn_attr) && !($_sort1 === $_sort2)) {
  1197. if (!array_key_exists($attr, $mod_array['replace'])) {
  1198. $this->_debug("Adding to replace(2): $attr");
  1199. $mod_array['replace'][$attr] = $value;
  1200. }
  1201. }
  1202. }
  1203. }
  1204. else {
  1205. if (!empty($value)) {
  1206. $mod_array['add'][$attr] = $value;
  1207. }
  1208. }
  1209. }
  1210. if (empty($old_ou)) {
  1211. $subject_dn_components = ldap_explode_dn($subject_dn, 0);
  1212. unset($subject_dn_components["count"]);
  1213. $subject_rdn = array_shift($subject_dn_components);
  1214. $old_ou = implode(',', $subject_dn_components);
  1215. }
  1216. $subject_dn = self::unified_dn($subject_dn);
  1217. $prefix = self::unified_dn('ou=' . $old_ou) . ',';
  1218. // object is an organizational unit
  1219. if (strpos($subject_dn, $prefix) === 0) {
  1220. $root = substr($subject_dn, strlen($prefix)); // remove ou=*,
  1221. if ((!empty($new_attrs['base_dn']) && strtolower($new_attrs['base_dn']) !== strtolower($root))
  1222. || (strtolower($old_ou) !== strtolower($new_ou))
  1223. ) {
  1224. if (!empty($new_attrs['base_dn'])) {
  1225. $root = $new_attrs['base_dn'];
  1226. }
  1227. $mod_array['rename']['new_parent'] = $root;
  1228. $mod_array['rename']['dn'] = $subject_dn;
  1229. $mod_array['rename']['new_rdn'] = 'ou=' . self::quote_string($new_ou, true);
  1230. }
  1231. }
  1232. // not OU object, but changed ou attribute
  1233. else if (!empty($old_ou) && !empty($new_ou)) {
  1234. // unify DN strings for comparison
  1235. $old_ou = self::unified_dn($old_ou);
  1236. $new_ou = self::unified_dn($new_ou);
  1237. if (strtolower($old_ou) !== strtolower($new_ou)) {
  1238. $mod_array['rename']['new_parent'] = $new_ou;
  1239. if (empty($mod_array['rename']['dn']) || empty($mod_array['rename']['new_rdn'])) {
  1240. $rdn_attr_value = self::quote_string($new_attrs[$rdn_attr], true);
  1241. $mod_array['rename']['dn'] = $subject_dn;
  1242. $mod_array['rename']['new_rdn'] = $rdn_attr . '=' . $rdn_attr_value;
  1243. }
  1244. }
  1245. }
  1246. $this->_debug($mod_array);
  1247. $result = $this->modify_entry_attributes($subject_dn, $mod_array);
  1248. if ($result) {
  1249. return $mod_array;
  1250. }
  1251. }
  1252. /**
  1253. * Bind connection with (SASL-) user and password
  1254. *
  1255. * @param string $authc Authentication user
  1256. * @param string $pass Bind password
  1257. * @param string $authz Autorization user
  1258. *
  1259. * @return boolean True on success, False on error
  1260. */
  1261. public function sasl_bind($authc = '', $pass = '', $authz = null)
  1262. {
  1263. if (!$this->conn) {
  1264. return false;
  1265. }
  1266. if (!function_exists('ldap_sasl_bind')) {
  1267. $this->_error("LDAP: Unable to bind. ldap_sasl_bind() not exists");
  1268. return false;
  1269. }
  1270. if (!empty($authz)) {
  1271. $authz = 'u:' . $authz;
  1272. }
  1273. $gssapi = $this->config_get('gssapi_cn');
  1274. $method = $this->config_get('auth_method');
  1275. if (empty($method)) {
  1276. $method = 'DIGEST-MD5';
  1277. }
  1278. if ($gssapi && strcasecmp($method, 'GSSAPI') == 0) {
  1279. putenv("KRB5CCNAME=$gssapi");
  1280. }
  1281. $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz, gssapi: $gssapi]");
  1282. if (ldap_sasl_bind($this->conn, null, $pass, $method, null, $authc, $authz)) {
  1283. $this->_debug("S: OK");
  1284. return true;
  1285. }
  1286. $this->_debug("S: ".ldap_error($this->conn));
  1287. $this->_error("LDAP: Bind failed for authcid=$authc. ".ldap_error($this->conn));
  1288. return false;
  1289. }
  1290. /**
  1291. * Execute LDAP search
  1292. *
  1293. * @param string $base_dn Base DN to use for searching
  1294. * @param string $filter Filter string to query
  1295. * @param string $scope The LDAP scope (list|sub|base)
  1296. * @param array $attrs List of entry attributes to read
  1297. * @param array $prop Hash array with query configuration properties:
  1298. * - sort: array of sort attributes (has to be in sync with the VLV index)
  1299. * - search: search string used for VLV controls
  1300. * @param bool $count_only Set to true if only entry count is requested
  1301. *
  1302. * @return mixed Net_LDAP3_Result object or number of entries (if $count_only=true) or False on failure
  1303. */
  1304. public function search($base_dn, $filter = '(objectclass=*)', $scope = 'sub', $attrs = array('dn'), $props = array(), $count_only = false)
  1305. {
  1306. if (!$this->conn) {
  1307. $this->_debug("No active connection for " . __CLASS__ . "::" . __FUNCTION__);
  1308. return false;
  1309. }
  1310. // make sure attributes list is not empty
  1311. if (empty($attrs)) {
  1312. $attrs = array('dn');
  1313. }
  1314. // make sure filter is not empty
  1315. if (empty($filter)) {
  1316. $filter = '(objectclass=*)';
  1317. }
  1318. $this->_debug("C: Search base dn: [$base_dn] scope [$scope] with filter [$filter]");
  1319. $function = self::scope_to_function($scope, $ns_function);
  1320. if (!$count_only && ($sort = $this->find_vlv($base_dn, $filter, $scope, $props['sort']))) {
  1321. // when using VLV, we get the total count by...
  1322. // ...either reading numSubOrdinates attribute
  1323. if (($sub_filter = $this->config_get('numsub_filter')) &&
  1324. ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0))
  1325. ) {
  1326. $counts = ldap_get_entries($this->conn, $result_count);
  1327. for ($vlv_count = $j = 0; $j < $counts['count']; $j++) {
  1328. $vlv_count += $counts[$j]['numsubordinates'][0];
  1329. }
  1330. $this->_debug("D: total numsubordinates = " . $vlv_count);
  1331. }
  1332. // ...or by fetching all records dn and count them
  1333. else if (!function_exists('ldap_parse_virtuallist_control')) {
  1334. // @FIXME: this search will ignore $props['search']
  1335. $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $props, true);
  1336. }
  1337. $this->vlv_active = $this->_vlv_set_controls($sort, $this->list_page, $this->page_size,
  1338. $this->_vlv_search($sort, $props['search']));
  1339. }
  1340. else {
  1341. $this->vlv_active = false;
  1342. }
  1343. $sizelimit = (int) $this->config['sizelimit'];
  1344. $timelimit = (int) $this->config['timelimit'];
  1345. $phplimit = (int) @ini_get('max_execution_time');
  1346. // set LDAP time limit to be (one second) less than PHP time limit
  1347. // otherwise we have no chance to log the error below
  1348. if ($phplimit && $timelimit >= $phplimit) {
  1349. $timelimit = $phplimit - 1;
  1350. }
  1351. $this->_debug("Using function $function on scope $scope (\$ns_function is $ns_function)");
  1352. if ($this->vlv_active) {
  1353. if (!empty($this->additional_filter)) {
  1354. $filter = "(&" . $filter . $this->additional_filter . ")";
  1355. $this->_debug("C: (With VLV) Setting a filter (with additional filter) of " . $filter);
  1356. }
  1357. else {
  1358. $this->_debug("C: (With VLV) Setting a filter (without additional filter) of " . $filter);
  1359. }
  1360. }
  1361. else {
  1362. if (!empty($this->additional_filter)) {
  1363. $filter = "(&" . $filter . $this->additional_filter . ")";
  1364. }
  1365. $this->_debug("C: (Without VLV) Setting a filter of " . $filter);
  1366. }
  1367. $this->_debug("Executing search with return attributes: " . var_export($attrs, true));
  1368. $ldap_result = @$function($this->conn, $base_dn, $filter, $attrs, 0, $sizelimit, $timelimit);
  1369. if (!$ldap_result) {
  1370. $this->_warning("LDAP: $function failed for dn=$base_dn. " . ldap_error($this->conn));
  1371. return false;
  1372. }
  1373. // when running on a patched PHP we can use the extended functions
  1374. // to retrieve the total count from the LDAP search result
  1375. if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
  1376. if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
  1377. ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult);
  1378. $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count");
  1379. }
  1380. else {
  1381. $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
  1382. }
  1383. }
  1384. else {
  1385. $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found");
  1386. }
  1387. $result = new Net_LDAP3_Result($this->conn, $base_dn, $filter, $scope, $ldap_result);
  1388. if (isset($last_offset)) {
  1389. $result->set('offset', $last_offset);
  1390. }
  1391. if (isset($vlv_count)) {
  1392. $result->set('count', $vlv_count);
  1393. }
  1394. $result->set('vlv', $this->vlv_active);
  1395. return $count_only ? $result->count() : $result;
  1396. }
  1397. /**
  1398. * Similar to Net_LDAP3::search() but using a search array with multiple
  1399. * keys and values that to continue to use the VLV but with an original
  1400. * filter adding the search stuff to an additional filter.
  1401. *
  1402. * @see Net_LDAP3::search()
  1403. */
  1404. public function search_entries($base_dn, $filter = '(objectclass=*)', $scope = 'sub', $attrs = array('dn'), $props = array())
  1405. {
  1406. $this->_debug("Net_LDAP3::search_entries with search " . var_export($props, true));
  1407. if (is_array($props['search']) && array_key_exists('params', $props['search'])) {
  1408. $_search = $this->search_filter($props['search']);
  1409. $this->_debug("C: Search filter: $_search");
  1410. if (!empty($_search)) {
  1411. $this->additional_filter = $_search;
  1412. }
  1413. else {
  1414. $this->additional_filter = "(|";
  1415. foreach ($props['search'] as $attr => $value) {
  1416. $this->additional_filter .= "(" . $attr . "=" . $this->_fuzzy_search_prefix() . $value . $this->_fuzzy_search_suffix() . ")";
  1417. }
  1418. $this->additional_filter .= ")";
  1419. }
  1420. $this->_debug("C: Setting an additional filter " . $this->additional_filter);
  1421. }
  1422. $search = $this->search($base_dn, $filter, $scope, $attrs, $props);
  1423. $this->additional_filter = null;
  1424. if (!$search) {
  1425. $this->_debug("Net_LDAP3: Search did not succeed!");
  1426. return false;
  1427. }
  1428. return $search;
  1429. }
  1430. /**
  1431. * Create LDAP search filter string according to defined parameters.
  1432. */
  1433. public function search_filter($search)
  1434. {
  1435. if (empty($search) || !is_array($search) || empty($search['params'])) {
  1436. return null;
  1437. }
  1438. $operators = array('=', '~=', '>=', '<=');
  1439. $filter = '';
  1440. foreach ((array) $search['params'] as $field => $param) {
  1441. $value = (array) $param['value'];
  1442. switch ((string)$param['type']) {
  1443. case 'prefix':
  1444. $prefix = '';
  1445. $suffix = '*';
  1446. break;
  1447. case 'suffix':
  1448. $prefix = '*';
  1449. $suffix = '';
  1450. break;
  1451. case 'exact':
  1452. case '=':
  1453. case '~=':
  1454. case '>=':
  1455. case '<=':
  1456. $prefix = '';
  1457. $suffix = '';
  1458. // this is a common query to find entry by DN, make sure
  1459. // it is a unified DN so special characters are handled correctly
  1460. if ($field == 'entrydn') {
  1461. $value = array_map(array('Net_LDAP3', 'unified_dn'), $value);
  1462. }
  1463. break;
  1464. case 'exists':
  1465. $prefix = '*';
  1466. $suffix = '';
  1467. $param['value'] = '';
  1468. break;
  1469. case 'both':
  1470. default:
  1471. $prefix = '*';
  1472. $suffix = '*';
  1473. break;
  1474. }
  1475. $operator = $param['type'] && in_array($param['type'], $operators) ? $param['type'] : '=';
  1476. if (count($value) < 2) {
  1477. $value = array_pop($value);
  1478. }
  1479. if (is_array($value)) {
  1480. $val_filter = array();
  1481. foreach ($value as $val) {
  1482. $val = self::quote_string($val);
  1483. $val_filter[] = "(" . $field . $operator . $prefix . $val . $suffix . ")";
  1484. }
  1485. $filter .= "(|" . implode($val_filter, '') . ")";
  1486. }
  1487. else {
  1488. $value = self::quote_string($value);
  1489. $filter .= "(" . $field . $operator . $prefix . $value . $suffix . ")";
  1490. }
  1491. }
  1492. // join search parameters with specified operator ('OR' or 'AND')
  1493. if (count($search['params']) > 1) {
  1494. $filter = '(' . ($search['operator'] == 'AND' ? '&' : '|') . $filter . ')';
  1495. }
  1496. return $filter;
  1497. }
  1498. /**
  1499. * Set properties for VLV-based paging
  1500. *
  1501. * @param number $page Page number to list (starting at 1)
  1502. * @param number $size Number of entries to display on one page
  1503. */
  1504. public function set_vlv_page($page, $size = 10)
  1505. {
  1506. $this->list_page = $page;
  1507. $this->page_size = $size;
  1508. }
  1509. /**
  1510. * Turn an LDAP entry into a regular PHP array with attributes as keys.
  1511. *
  1512. * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
  1513. * @param bool $flat Convert one-element-array values into strings
  1514. *
  1515. * @return array Hash array with attributes as keys
  1516. */
  1517. public static function normalize_entry($entry, $flat = false)
  1518. {
  1519. $rec = array();
  1520. for ($i=0; $i < $entry['count']; $i++) {
  1521. $attr = $entry[$i];
  1522. for ($j=0; $j < $entry[$attr]['count']; $j++) {
  1523. $rec[$attr][$j] = $entry[$attr][$j];
  1524. }
  1525. if ($flat && count($rec[$attr]) == 1) {
  1526. $rec[$attr] = $rec[$attr][0];
  1527. }
  1528. }
  1529. return $rec;
  1530. }
  1531. /**
  1532. * Normalize a ldap result by converting entry attribute arrays into single values
  1533. */
  1534. public static function normalize_result($_result)
  1535. {
  1536. if (!is_array($_result)) {
  1537. return array();
  1538. }
  1539. $result = array();
  1540. for ($x = 0; $x < $_result['count']; $x++) {
  1541. $dn = $_result[$x]['dn'];
  1542. $entry = self::normalize_entry($_result[$x], true);
  1543. if (!empty($entry['objectclass'])) {
  1544. if (is_array($entry['objectclass'])) {
  1545. $entry['objectclass'] = array_map('strtolower', $entry['objectclass']);
  1546. }
  1547. else {
  1548. $entry['objectclass'] = strtolower($entry['objectclass']);
  1549. }
  1550. }
  1551. $result[$dn] = $entry;
  1552. }
  1553. return $result;
  1554. }
  1555. public static function scopeint2str($scope)
  1556. {
  1557. switch ($scope) {
  1558. case 2:
  1559. return 'sub';
  1560. case 1:
  1561. return 'one';
  1562. case 0:
  1563. return 'base';
  1564. default:
  1565. $this->_debug("Scope $scope is not a valid scope integer");
  1566. }
  1567. }
  1568. /**
  1569. * Choose the right PHP function according to scope property
  1570. *
  1571. * @param string $scope The LDAP scope (sub|base|list)
  1572. * @param string $ns_function Function to be used for numSubOrdinates queries
  1573. * @return string PHP function to be used to query directory
  1574. */
  1575. public static function scope_to_function($scope, &$ns_function = null)
  1576. {
  1577. switch ($scope) {
  1578. case 'sub':
  1579. $function = $ns_function = 'ldap_search';
  1580. break;
  1581. case 'base':
  1582. $function = $ns_function = 'ldap_read';
  1583. break;
  1584. case 'one':
  1585. case 'list':
  1586. default:
  1587. $function = 'ldap_list';
  1588. $ns_function = 'ldap_read';
  1589. break;
  1590. }
  1591. return $function;
  1592. }
  1593. private function config_set_config_get_hook($callback)
  1594. {
  1595. $this->_config_get_hook = $callback;
  1596. }
  1597. private function config_set_config_set_hook($callback)
  1598. {
  1599. $this->_config_set_hook = $callback;
  1600. }
  1601. /**
  1602. * Sets the debug level both for this class and the ldap connection.
  1603. */
  1604. private function config_set_debug($value)
  1605. {
  1606. $this->config['debug'] = (bool) $value;
  1607. ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, (int) $value);
  1608. }
  1609. /**
  1610. * Sets a log hook that is called with every log message in this module.
  1611. */
  1612. private function config_set_log_hook($callback)
  1613. {
  1614. $this->_log_hook = $callback;
  1615. }
  1616. /**
  1617. * Find a matching VLV
  1618. */
  1619. protected function find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
  1620. {
  1621. if ($scope == 'base') {
  1622. return false;
  1623. }
  1624. $vlv_indexes = $this->find_vlv_indexes_and_searches();
  1625. if (empty($vlv_indexes)) {
  1626. return false;
  1627. }
  1628. $this->_debug("Existing vlv index and search information", $vlv_indexes);
  1629. $filter = strtolower($filter);
  1630. $base_dn = self::unified_dn($base_dn);
  1631. foreach ($vlv_indexes as $vlv_index) {
  1632. if (!empty($vlv_index[$base_dn])) {
  1633. $this->_debug("Found a VLV for base_dn: " . $base_dn);
  1634. if ($vlv_index[$base_dn]['filter'] == $filter) {
  1635. if ($vlv_index[$base_dn]['scope'] == $scope) {
  1636. $this->_debug("Scope and filter matches");
  1637. // Not passing any sort attributes means you don't care
  1638. if (!empty($sort_attrs)) {
  1639. $sort_attrs = array_map('strtolower', (array) $sort_attrs);
  1640. foreach ($vlv_index[$base_dn]['sort'] as $sss_config) {
  1641. $sss_config = array_map('strtolower', $sss_config);
  1642. if (count(array_intersect($sort_attrs, $sss_config)) == count($sort_attrs)) {
  1643. $this->_debug("Sorting matches");
  1644. return $sort_attrs;
  1645. }
  1646. }
  1647. $this->_debug("Sorting does not match");
  1648. }
  1649. else {
  1650. $sort = array_filter((array) $vlv_index[$base_dn]['sort'][0]);
  1651. $this->_debug("Sorting unimportant");
  1652. return $sort;
  1653. }
  1654. }
  1655. else {
  1656. $this->_debug("Scope does not match");
  1657. }
  1658. }
  1659. else {
  1660. $this->_debug("Filter does not match");
  1661. }
  1662. }
  1663. }
  1664. return false;
  1665. }
  1666. /**
  1667. * Return VLV indexes and searches including necessary configuration
  1668. * details.
  1669. */
  1670. protected function find_vlv_indexes_and_searches()
  1671. {
  1672. // Use of Virtual List View control has been specifically disabled.
  1673. if ($this->config['vlv'] === false) {
  1674. return false;
  1675. }
  1676. // Virtual List View control has been configured in kolab.conf, for example;
  1677. //
  1678. // [ldap]
  1679. // vlv = [
  1680. // {
  1681. // 'ou=People,dc=example,dc=org': {
  1682. // 'scope': 'sub',
  1683. // 'filter': '(objectclass=inetorgperson)',
  1684. // 'sort' : [
  1685. // [
  1686. // 'displayname',
  1687. // 'sn',
  1688. // 'givenname',
  1689. // 'cn'
  1690. // ]
  1691. // ]
  1692. // }
  1693. // },
  1694. // {
  1695. // 'ou=Groups,dc=example,dc=org': {
  1696. // 'scope': 'sub',
  1697. // 'filter': '(objectclass=groupofuniquenames)',
  1698. // 'sort' : [
  1699. // [
  1700. // 'cn'
  1701. // ]
  1702. // ]
  1703. // }
  1704. // },
  1705. // ]
  1706. //
  1707. if (is_array($this->config['vlv'])) {
  1708. return $this->config['vlv'];
  1709. }
  1710. // We have done this dance before.
  1711. if ($this->_vlv_indexes_and_searches !== null) {
  1712. return $this->_vlv_indexes_and_searches;
  1713. }
  1714. $this->_vlv_indexes_and_searches = array();
  1715. $config_root_dn = $this->config_get('config_root_dn');
  1716. if (empty($config_root_dn)) {
  1717. return array();
  1718. }
  1719. if ($cached_config = $this->get_cache_data('vlvconfig')) {
  1720. $this->_vlv_indexes_and_searches = $cached_config;
  1721. return $this->_vlv_indexes_and_searches;
  1722. }
  1723. $this->_debug("No VLV information available yet, refreshing");
  1724. $search_filter = '(objectclass=vlvsearch)';
  1725. $search_result = ldap_search($this->conn, $config_root_dn, $search_filter, array('*'), 0, 0, 0);
  1726. if ($search_result === false) {
  1727. $this->_debug("Search for '$search_filter' on '$config_root_dn' failed:".ldap_error($this->conn));
  1728. return;
  1729. }
  1730. $vlv_searches = new Net_LDAP3_Result($this->conn, $config_root_dn, $search_filter, 'sub', $search_result);
  1731. if ($vlv_searches->count() < 1) {
  1732. $this->_debug("Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
  1733. return;
  1734. }
  1735. $index_filter = '(objectclass=vlvindex)';
  1736. foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
  1737. // The attributes we are interested in are as follows:
  1738. $_vlv_base_dn = self::unified_dn($vlv_search_attrs['vlvbase']);
  1739. $_vlv_scope = $vlv_search_attrs['vlvscope'];
  1740. $_vlv_filter = $vlv_search_attrs['vlvfilter'];
  1741. // Multiple indexes may exist
  1742. $index_result = ldap_search($this->conn, $vlv_search_dn, $index_filter, array('*'), 0, 0, 0);
  1743. if ($index_result === false) {
  1744. $this->_debug("Search for '$index_filter' on '$vlv_search_dn' failed:".ldap_error($this->conn));
  1745. continue;
  1746. }
  1747. $vlv_indexes = new Net_LDAP3_Result($this->conn, $vlv_search_dn, $index_filter, 'sub', $index_result);
  1748. $vlv_indexes = $vlv_indexes->entries(true);
  1749. // Reset this one for each VLV search.
  1750. $_vlv_sort = array();
  1751. foreach ($vlv_indexes as $vlv_index_dn => $vlv_index_attrs) {
  1752. $_vlv_sort[] = explode(' ', trim($vlv_index_attrs['vlvsort']));
  1753. }
  1754. $this->_vlv_indexes_and_searches[] = array(
  1755. $_vlv_base_dn => array(
  1756. 'scope' => self::scopeint2str($_vlv_scope),
  1757. 'filter' => strtolower($_vlv_filter),
  1758. 'sort' => $_vlv_sort,
  1759. ),
  1760. );
  1761. }
  1762. // cache this
  1763. $this->set_cache_data('vlvconfig', $this->_vlv_indexes_and_searches);
  1764. return $this->_vlv_indexes_and_searches;
  1765. }
  1766. private function init_schema()
  1767. {
  1768. // use PEAR include if autoloading failed
  1769. if (!class_exists('Net_LDAP2')) {
  1770. require_once('Net/LDAP2.php');
  1771. }
  1772. $port = $this->config_get('port', 389);
  1773. $tls = $this->config_get('use_tls', false);
  1774. foreach ((array) $this->config_get('hosts') as $host) {
  1775. $this->_debug("C: Connect [$host:$port]");
  1776. $_ldap_cfg = array(
  1777. 'host' => $host,
  1778. 'port' => $port,
  1779. 'tls' => $tls,
  1780. 'version' => 3,
  1781. 'binddn' => $this->config_get('service_bind_dn'),
  1782. 'bindpw' => $this->config_get('service_bind_pw')
  1783. );
  1784. $_ldap_schema_cache_cfg = array(
  1785. 'path' => "/tmp/" . $host . ":" . ($port ? $port : '389') . "-Net_LDAP2_Schema.cache",
  1786. 'max_age' => 86400,
  1787. );
  1788. $_ldap = Net_LDAP2::connect($_ldap_cfg);
  1789. if (!is_a($_ldap, 'Net_LDAP2_Error')) {
  1790. $this->_debug("S: OK");
  1791. break;
  1792. }
  1793. $this->_debug("S: NOT OK");
  1794. $this->_debug($_ldap->getMessage());
  1795. }
  1796. if (is_a($_ldap, 'Net_LDAP2_Error')) {
  1797. return null;
  1798. }
  1799. $_ldap_schema_cache = new Net_LDAP2_SimpleFileSchemaCache($_ldap_schema_cache_cfg);
  1800. $_ldap->registerSchemaCache($_ldap_schema_cache);
  1801. // TODO: We should learn what LDAP tech. we're running against.
  1802. // Perhaps with a scope base objectclass recognize rootdse entry
  1803. $schema_root_dn = $this->config_get('schema_root_dn');
  1804. if (!$schema_root_dn) {
  1805. $_schema = $_ldap->schema();
  1806. }
  1807. return $_schema;
  1808. }
  1809. private function list_group_member($dn, $members, $recurse = true)
  1810. {
  1811. $this->_debug("Net_LDAP3::list_group_member($dn)");
  1812. $members = (array) $members;
  1813. $group_members = array();
  1814. // remove possible 'count' item
  1815. unset($members['count']);
  1816. // Use the member attributes to return an array of member ldap objects
  1817. // NOTE that the member attribute is supposed to contain a DN
  1818. foreach ($members as $member) {
  1819. $member_entry = $this->get_entry($member, array('member', 'uniquemember', 'memberurl', 'objectclass'));
  1820. if (empty($member_entry)) {
  1821. continue;
  1822. }
  1823. $group_members[$member] = $member;
  1824. if ($recurse) {
  1825. // Nested groups
  1826. $group_group_members = $this->list_group_members($member, $member_entry);
  1827. if ($group_group_members) {
  1828. $group_members = array_merge($group_group_members, $group_members);
  1829. }
  1830. }
  1831. }
  1832. return array_filter($group_members);
  1833. }
  1834. private function list_group_uniquemember($dn, $uniquemembers, $recurse = true)
  1835. {
  1836. $this->_debug("Net_LDAP3::list_group_uniquemember($dn)", $entry);
  1837. $uniquemembers = (array)($uniquemembers);
  1838. $group_members = array();
  1839. // remove possible 'count' item
  1840. unset($uniquemembers['count']);
  1841. foreach ($uniquemembers as $member) {
  1842. $member_entry = $this->get_entry($member, array('member', 'uniquemember', 'memberurl', 'objectclass'));
  1843. if (empty($member_entry)) {
  1844. continue;
  1845. }
  1846. $group_members[$member] = $member;
  1847. if ($recurse) {
  1848. // Nested groups
  1849. $group_group_members = $this->list_group_members($member, $member_entry);
  1850. if ($group_group_members) {
  1851. $group_members = array_merge($group_group_members, $group_members);
  1852. }
  1853. }
  1854. }
  1855. return array_filter($group_members);
  1856. }
  1857. private function list_group_memberurl($dn, $memberurls, $recurse = true)
  1858. {
  1859. $this->_debug("Net_LDAP3::list_group_memberurl($dn)");
  1860. $group_members = array();
  1861. $memberurls = (array) $memberurls;
  1862. $attributes = array('member', 'uniquemember', 'memberurl', 'objectclass');
  1863. // remove possible 'count' item
  1864. unset($memberurls['count']);
  1865. foreach ($memberurls as $url) {
  1866. $ldap_uri = $this->parse_memberurl($url);
  1867. $result = $this->search($ldap_uri[3], $ldap_uri[6], 'sub', $attributes);
  1868. if (!$result) {
  1869. continue;
  1870. }
  1871. foreach ($result->entries(true) as $entry_dn => $_entry) {
  1872. $group_members[$entry_dn] = $entry_dn;
  1873. $this->_debug("Found " . $entry_dn);
  1874. if ($recurse) {
  1875. // Nested group
  1876. $group_group_members = $this->list_group_members($entry_dn, $_entry);
  1877. if ($group_group_members) {
  1878. $group_members = array_merge($group_members, $group_group_members);
  1879. }
  1880. }
  1881. }
  1882. }
  1883. return array_filter($group_members);
  1884. }
  1885. /**
  1886. * memberUrl attribute parser
  1887. *
  1888. * @param string $url URL string
  1889. *
  1890. * @return array URL elements
  1891. */
  1892. private function parse_memberurl($url)
  1893. {
  1894. preg_match('/(.*):\/\/(.*)\/(.*)\?(.*)\?(.*)\?(.*)/', $url, $matches);
  1895. return $matches;
  1896. }
  1897. private function modify_entry_attributes($subject_dn, $attributes)
  1898. {
  1899. if (is_array($attributes['rename']) && !empty($attributes['rename'])) {
  1900. $olddn = $attributes['rename']['dn'];
  1901. $newrdn = $attributes['rename']['new_rdn'];
  1902. $new_parent = $attributes['rename']['new_parent'];
  1903. $this->_debug("C: Rename $olddn to $newrdn,$new_parent");
  1904. // Note: for some reason the operation fails if RDN contains special characters
  1905. // and last argument of ldap_rename() is set to TRUE. That's why we use FALSE.
  1906. // However, we need to modify RDN attribute value later, otherwise it
  1907. // will contain an array of previous and current values
  1908. for ($i = 1; $i >= 0; $i--) {
  1909. $result = ldap_rename($this->conn, $olddn, $newrdn, $new_parent, $i == 1);
  1910. if ($result) {
  1911. break;
  1912. }
  1913. }
  1914. if ($result) {
  1915. $this->_debug("S: OK");
  1916. if ($new_parent) {
  1917. $subject_dn = $newrdn . ',' . $new_parent;
  1918. }
  1919. else {
  1920. $old_parent_dn_components = ldap_explode_dn($olddn, 0);
  1921. unset($old_parent_dn_components["count"]);
  1922. $old_rdn = array_shift($old_parent_dn_components);
  1923. $old_parent_dn = implode(",", $old_parent_dn_components);
  1924. $subject_dn = $newrdn . ',' . $old_parent_dn;
  1925. }
  1926. // modify RDN attribute value, see note above
  1927. if (!$i && empty($attributes['replace'][$attr])) {
  1928. list($attr, $val) = explode('=', $newrdn, 2);
  1929. $attributes['replace'][$attr] = self::quote_string($val, true, true);
  1930. }
  1931. }
  1932. else {
  1933. $this->_debug("S: " . ldap_error($this->conn));
  1934. $this->_warning("LDAP: Failed to rename $olddn to $newrdn,$new_parent. " . ldap_error($this->conn));
  1935. return false;
  1936. }
  1937. }
  1938. if (is_array($attributes['replace']) && !empty($attributes['replace'])) {
  1939. $this->_debug("C: Mod-Replace $subject_dn: " . json_encode($attributes['replace']));
  1940. $result = ldap_mod_replace($this->conn, $subject_dn, $attributes['replace']);
  1941. if ($result) {
  1942. $this->_debug("S: OK");
  1943. }
  1944. else {
  1945. $this->_debug("S: " . ldap_error($this->conn));
  1946. $this->_warning("LDAP: Failed to replace attributes on $subject_dn: " . json_encode($attributes['replace']));
  1947. return false;
  1948. }
  1949. }
  1950. if (is_array($attributes['del']) && !empty($attributes['del'])) {
  1951. $this->_debug("C: Mod-Delete $subject_dn: " . json_encode($attributes['del']));
  1952. $result = ldap_mod_del($this->conn, $subject_dn, $attributes['del']);
  1953. if ($result) {
  1954. $this->_debug("S: OK");
  1955. }
  1956. else {
  1957. $this->_debug("S: " . ldap_error($this->conn));
  1958. $this->_warning("LDAP: Failed to delete attributes on $subject_dn: " . json_encode($attributes['del']));
  1959. return false;
  1960. }
  1961. }
  1962. if (is_array($attributes['add']) && !empty($attributes['add'])) {
  1963. $this->_debug("C: Mod-Add $subject_dn: " . json_encode($attributes['add']));
  1964. $result = ldap_mod_add($this->conn, $subject_dn, $attributes['add']);
  1965. if ($result) {
  1966. $this->_debug("S: OK");
  1967. }
  1968. else {
  1969. $this->_debug("S: " . ldap_error($this->conn));
  1970. $this->_warning("LDAP: Failed to add attributes on $subject_dn: " . json_encode($attributes['add']));
  1971. return false;
  1972. }
  1973. }
  1974. return true;
  1975. }
  1976. private function parse_aclrights(&$attributes, $attribute_value)
  1977. {
  1978. $components = explode(':', $attribute_value);
  1979. $_acl_target = array_shift($components);
  1980. $_acl_value = trim(implode(':', $components));
  1981. $_acl_components = explode(';', $_acl_target);
  1982. switch ($_acl_components[1]) {
  1983. case "entryLevel":
  1984. $attributes['entryLevelRights'] = Array();
  1985. $_acl_value = explode(',', $_acl_value);
  1986. foreach ($_acl_value as $right) {
  1987. list($method, $bool) = explode(':', $right);
  1988. if ($bool == "1" && !in_array($method, $attributes['entryLevelRights'])) {
  1989. $attributes['entryLevelRights'][] = $method;
  1990. }
  1991. }
  1992. break;
  1993. case "attributeLevel":
  1994. $attributes['attributeLevelRights'][$_acl_components[2]] = Array();
  1995. $_acl_value = explode(',', $_acl_value);
  1996. foreach ($_acl_value as $right) {
  1997. list($method, $bool) = explode(':', $right);
  1998. if ($bool == "1" && !in_array($method, $attributes['attributeLevelRights'][$_acl_components[2]])) {
  1999. $attributes['attributeLevelRights'][$_acl_components[2]][] = $method;
  2000. }
  2001. }
  2002. break;
  2003. default:
  2004. break;
  2005. }
  2006. }
  2007. private function parse_attribute_level_rights($attribute_value)
  2008. {
  2009. $attribute_value = str_replace(", ", ",", $attribute_value);
  2010. $attribute_values = explode(",", $attribute_value);
  2011. $attribute_value = array();
  2012. foreach ($attribute_values as $access_right) {
  2013. $access_right_components = explode(":", $access_right);
  2014. $access_attribute = strtolower(array_shift($access_right_components));
  2015. $access_value = array_shift($access_right_components);
  2016. $attribute_value[$access_attribute] = array();
  2017. for ($i = 0; $i < strlen($access_value); $i++) {
  2018. $method = $this->attribute_level_rights_map[substr($access_value, $i, 1)];
  2019. if (!in_array($method, $attribute_value[$access_attribute])) {
  2020. $attribute_value[$access_attribute][] = $method;
  2021. }
  2022. }
  2023. }
  2024. return $attribute_value;
  2025. }
  2026. private function parse_entry_level_rights($attribute_value)
  2027. {
  2028. $_attribute_value = array();
  2029. for ($i = 0; $i < strlen($attribute_value); $i++) {
  2030. $method = $this->entry_level_rights_map[substr($attribute_value, $i, 1)];
  2031. if (!in_array($method, $_attribute_value)) {
  2032. $_attribute_value[] = $method;
  2033. }
  2034. }
  2035. return $_attribute_value;
  2036. }
  2037. private function supported_controls()
  2038. {
  2039. if (!empty($this->supported_controls)) {
  2040. return $this->supported_controls;
  2041. }
  2042. $this->_info("Obtaining supported controls");
  2043. if ($result = $this->search('', '(objectclass=*)', 'base', array('supportedcontrol'))) {
  2044. $result = $result->entries(true);
  2045. $control = $result['']['supportedcontrol'];
  2046. }
  2047. else {
  2048. $control = array();
  2049. }
  2050. $this->_info("Obtained " . count($control) . " supported controls");
  2051. $this->supported_controls = $control;
  2052. return $control;
  2053. }
  2054. private function vendor_name()
  2055. {
  2056. if (!empty($this->vendor_name)) {
  2057. return $this->vendor_name;
  2058. }
  2059. $this->_info("Obtaining LDAP server vendor name");
  2060. if ($result = $this->search('', '(objectclass=*)', 'base', array('vendorname'))) {
  2061. $result = $result->entries(true);
  2062. $name = $result['']['vendorname'];
  2063. }
  2064. else {
  2065. $name = false;
  2066. }
  2067. if ($name !== false) {
  2068. $this->_info("Vendor name is $name");
  2069. } else {
  2070. $this->_info("No vendor name!");
  2071. }
  2072. $this->vendor = $name;
  2073. return $name;
  2074. }
  2075. protected function _alert()
  2076. {
  2077. $this->__log(LOG_ALERT, func_get_args());
  2078. }
  2079. protected function _critical()
  2080. {
  2081. $this->__log(LOG_CRIT, func_get_args());
  2082. }
  2083. protected function _debug()
  2084. {
  2085. $this->__log(LOG_DEBUG, func_get_args());
  2086. }
  2087. protected function _emergency()
  2088. {
  2089. $this->__log(LOG_EMERG, func_get_args());
  2090. }
  2091. protected function _error()
  2092. {
  2093. $this->__log(LOG_ERR, func_get_args());
  2094. }
  2095. protected function _info()
  2096. {
  2097. $this->__log(LOG_INFO, func_get_args());
  2098. }
  2099. protected function _notice()
  2100. {
  2101. $this->__log(LOG_NOTICE, func_get_args());
  2102. }
  2103. protected function _warning()
  2104. {
  2105. $this->__log(LOG_WARNING, func_get_args());
  2106. }
  2107. /**
  2108. * Log a message.
  2109. */
  2110. private function __log($level, $args)
  2111. {
  2112. $msg = array();
  2113. foreach ($args as $arg) {
  2114. $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
  2115. }
  2116. if (!empty($this->_log_hook)) {
  2117. call_user_func_array($this->_log_hook, array($level, $msg));
  2118. return;
  2119. }
  2120. if ($this->debug_level > 0) {
  2121. syslog($level, implode("\n", $msg));
  2122. }
  2123. }
  2124. /**
  2125. * Add BER sequence with correct length and the given identifier
  2126. */
  2127. private static function _ber_addseq($str, $identifier)
  2128. {
  2129. $len = dechex(strlen($str)/2);
  2130. if (strlen($len) % 2 != 0) {
  2131. $len = '0'.$len;
  2132. }
  2133. return $identifier . $len . $str;
  2134. }
  2135. /**
  2136. * Returns BER encoded integer value in hex format
  2137. */
  2138. private static function _ber_encode_int($offset)
  2139. {
  2140. $val = dechex($offset);
  2141. $prefix = '';
  2142. // check if bit 8 of high byte is 1
  2143. if (preg_match('/^[89abcdef]/', $val)) {
  2144. $prefix = '00';
  2145. }
  2146. if (strlen($val)%2 != 0) {
  2147. $prefix .= '0';
  2148. }
  2149. return $prefix . $val;
  2150. }
  2151. /**
  2152. * Quotes attribute value string
  2153. *
  2154. * @param string $str Attribute value
  2155. * @param bool $dn True if the attribute is a DN
  2156. * @param bool $reverse Do reverse replacement
  2157. *
  2158. * @return string Quoted string
  2159. */
  2160. public static function quote_string($str, $is_dn = false, $reverse = false)
  2161. {
  2162. // take first entry if array given
  2163. if (is_array($str)) {
  2164. $str = reset($str);
  2165. }
  2166. if ($is_dn) {
  2167. $replace = array(
  2168. ',' => '\2c',
  2169. '=' => '\3d',
  2170. '+' => '\2b',
  2171. '<' => '\3c',
  2172. '>' => '\3e',
  2173. ';' => '\3b',
  2174. "\\"=> '\5c',
  2175. '"' => '\22',
  2176. '#' => '\23'
  2177. );
  2178. }
  2179. else {
  2180. $replace = array(
  2181. '*' => '\2a',
  2182. '(' => '\28',
  2183. ')' => '\29',
  2184. "\\" => '\5c',
  2185. '/' => '\2f'
  2186. );
  2187. }
  2188. if ($reverse) {
  2189. return str_replace(array_values($replace), array_keys($replace), $str);
  2190. }
  2191. return strtr($str, $replace);
  2192. }
  2193. /**
  2194. * Unify DN string for comparison
  2195. *
  2196. * @para string $str DN string
  2197. *
  2198. * @return string Unified DN string
  2199. */
  2200. public static function unified_dn($str)
  2201. {
  2202. $result = array();
  2203. foreach (explode(',', $str) as $token) {
  2204. list($attr, $value) = explode('=', $token, 2);
  2205. $pos = 0;
  2206. while (preg_match('/\\\\[0-9a-fA-F]{2}/', $value, $matches, PREG_OFFSET_CAPTURE, $pos)) {
  2207. $char = chr(hexdec(substr($matches[0][0], 1)));
  2208. $pos = $matches[0][1];
  2209. $value = substr_replace($value, $char, $pos, 3);
  2210. $pos += 1;
  2211. }
  2212. $result[] = $attr . '=' . self::quote_string($value, true);
  2213. }
  2214. return implode(',', $result);
  2215. }
  2216. /**
  2217. * create ber encoding for sort control
  2218. *
  2219. * @param array List of cols to sort by
  2220. * @return string BER encoded option value
  2221. */
  2222. private static function _sort_ber_encode($sortcols)
  2223. {
  2224. $str = '';
  2225. foreach (array_reverse((array)$sortcols) as $col) {
  2226. $ber_val = self::_string2hex($col);
  2227. // 30 = ber sequence with a length of octet value
  2228. // 04 = octet string with a length of the ascii value
  2229. $oct = self::_ber_addseq($ber_val, '04');
  2230. $str = self::_ber_addseq($oct, '30') . $str;
  2231. }
  2232. // now tack on sequence identifier and length
  2233. $str = self::_ber_addseq($str, '30');
  2234. return pack('H'.strlen($str), $str);
  2235. }
  2236. /**
  2237. * Returns ascii string encoded in hex
  2238. */
  2239. private static function _string2hex($str)
  2240. {
  2241. $hex = '';
  2242. for ($i=0; $i < strlen($str); $i++)
  2243. $hex .= dechex(ord($str[$i]));
  2244. return $hex;
  2245. }
  2246. /**
  2247. * Generate BER encoded string for Virtual List View option
  2248. *
  2249. * @param integer List offset (first record)
  2250. * @param integer Records per page
  2251. * @return string BER encoded option value
  2252. */
  2253. private static function _vlv_ber_encode($offset, $rpp, $search = '')
  2254. {
  2255. // This string is ber-encoded, php will prefix this value with:
  2256. // 04 (octet string) and 10 (length of 16 bytes)
  2257. // the code behind this string is broken down as follows:
  2258. // 30 = ber sequence with a length of 0e (14) bytes following
  2259. // 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
  2260. // 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24)
  2261. // a0 = type context-specific/constructed with a length of 06 (6) bytes following
  2262. // 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
  2263. // 02 = type integer with 2 bytes following (contentCount): 01 00
  2264. // whith a search string present:
  2265. // 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
  2266. // 81 indicates a user string is present where as a a0 indicates just a offset search
  2267. // 81 = type context-specific/constructed with a length of 06 (6) bytes following
  2268. // the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
  2269. // encoding of integer values (note: these values are in
  2270. // two-complement form so since offset will never be negative bit 8 of the
  2271. // leftmost octet should never by set to 1):
  2272. // 8.3.2: If the contents octets of an integer value encoding consist
  2273. // of more than one octet, then the bits of the first octet (rightmost) and bit 8
  2274. // of the second (to the left of first octet) octet:
  2275. // a) shall not all be ones; and
  2276. // b) shall not all be zero
  2277. if ($search) {
  2278. $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
  2279. $ber_val = self::_string2hex($search);
  2280. $str = self::_ber_addseq($ber_val, '81');
  2281. }
  2282. else {
  2283. // construct the string from right to left
  2284. $str = "020100"; # contentCount
  2285. // returns encoded integer value in hex format
  2286. $ber_val = self::_ber_encode_int($offset);
  2287. // calculate octet length of $ber_val
  2288. $str = self::_ber_addseq($ber_val, '02') . $str;
  2289. // now compute length over $str
  2290. $str = self::_ber_addseq($str, 'a0');
  2291. }
  2292. // now tack on records per page
  2293. $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
  2294. // now tack on sequence identifier and length
  2295. $str = self::_ber_addseq($str, '30');
  2296. return pack('H'.strlen($str), $str);
  2297. }
  2298. private function _fuzzy_search_prefix()
  2299. {
  2300. switch ($this->config_get("fuzzy_search", 2)) {
  2301. case 2:
  2302. return "*";
  2303. break;
  2304. case 1:
  2305. case 0:
  2306. default:
  2307. return "";
  2308. break;
  2309. }
  2310. }
  2311. private function _fuzzy_search_suffix()
  2312. {
  2313. switch ($this->config_get("fuzzy_search", 2)) {
  2314. case 2:
  2315. return "*";
  2316. break;
  2317. case 1:
  2318. return "*";
  2319. case 0:
  2320. default:
  2321. return "";
  2322. break;
  2323. }
  2324. }
  2325. /**
  2326. * Return the search string value to be used in VLV controls
  2327. *
  2328. * @param array $sort List of attributes in vlv index
  2329. * @param array|string $search Search string or attribute => value hash
  2330. *
  2331. * @return string Search string
  2332. */
  2333. private function _vlv_search($sort, $search)
  2334. {
  2335. if (!empty($this->additional_filter)) {
  2336. $this->_debug("Not setting a VLV search filter because we already have a filter");
  2337. return;
  2338. }
  2339. if (empty($search)) {
  2340. return;
  2341. }
  2342. foreach ((array) $search as $attr => $value) {
  2343. if ($attr && !in_array(strtolower($attr), $sort)) {
  2344. $this->_debug("Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
  2345. return;
  2346. }
  2347. else {
  2348. return $value . $this->_fuzzy_search_suffix();
  2349. }
  2350. }
  2351. }
  2352. /**
  2353. * Set server controls for Virtual List View (paginated listing)
  2354. */
  2355. private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
  2356. {
  2357. $sort_ctrl = array(
  2358. 'oid' => "1.2.840.113556.1.4.473",
  2359. 'value' => self::_sort_ber_encode($sort)
  2360. );
  2361. if (!empty($search)) {
  2362. $this->_debug("_vlv_set_controls to include search: " . var_export($search, true));
  2363. }
  2364. $vlv_ctrl = array(
  2365. 'oid' => "2.16.840.1.113730.3.4.9",
  2366. 'value' => self::_vlv_ber_encode(
  2367. $offset = ($list_page-1) * $page_size + 1,
  2368. $page_size,
  2369. $search
  2370. ),
  2371. 'iscritical' => true
  2372. );
  2373. $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value']))
  2374. . " (" . implode(',', (array) $sort) . ");"
  2375. . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
  2376. if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
  2377. $this->_debug("S: ".ldap_error($this->conn));
  2378. $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
  2379. return false;
  2380. }
  2381. return true;
  2382. }
  2383. /**
  2384. * Get global handle for cache access
  2385. *
  2386. * @return object Cache object
  2387. */
  2388. public function get_cache()
  2389. {
  2390. if ($this->cache === true) {
  2391. // no memcache support in PHP
  2392. if (!class_exists('Memcache')) {
  2393. $this->cache = false;
  2394. return false;
  2395. }
  2396. // add all configured hosts to pool
  2397. $pconnect = $this->config_get('memcache_pconnect');
  2398. $hosts = $this->config_get('memcache_hosts');
  2399. if ($hosts) {
  2400. $this->cache = new Memcache;
  2401. $this->mc_available = 0;
  2402. $hosts = explode(',', $hosts);
  2403. foreach ($hosts as $host) {
  2404. $host = trim($host);
  2405. if (substr($host, 0, 7) != 'unix://') {
  2406. list($host, $port) = explode(':', $host);
  2407. if (!$port) $port = 11211;
  2408. }
  2409. else {
  2410. $port = 0;
  2411. }
  2412. $this->mc_available += intval($this->cache->addServer(
  2413. $host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure')));
  2414. }
  2415. // test connection and failover (will result in $this->mc_available == 0 on complete failure)
  2416. $this->cache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist
  2417. }
  2418. if (!$this->mc_available) {
  2419. $this->cache = false;
  2420. }
  2421. }
  2422. return $this->cache;
  2423. }
  2424. /**
  2425. * Callback for memcache failure
  2426. */
  2427. public function memcache_failure($host, $port)
  2428. {
  2429. static $seen = array();
  2430. // only report once
  2431. if (!$seen["$host:$port"]++) {
  2432. $this->mc_available--;
  2433. $this->_error("LDAP: Memcache failure on host $host:$port");
  2434. }
  2435. }
  2436. /**
  2437. * Get cached data
  2438. *
  2439. * @param string $key Cache key
  2440. *
  2441. * @return mixed Cached value
  2442. */
  2443. public function get_cache_data($key)
  2444. {
  2445. if ($cache = $this->get_cache()) {
  2446. return $cache->get($key);
  2447. }
  2448. }
  2449. /**
  2450. * Store cached data
  2451. *
  2452. * @param string $key Cache key
  2453. * @param mixed $data Data
  2454. * @param int $ttl Cache TTL in seconds
  2455. *
  2456. * @return bool False on failure or when cache is disabled, True if data was saved succesfully
  2457. */
  2458. public function set_cache_data($key, $data, $ttl = 3600)
  2459. {
  2460. if ($cache = $this->get_cache()) {
  2461. if (!method_exists($cache, 'replace') || !$cache->replace($key, $data, MEMCACHE_COMPRESSED, $ttl)) {
  2462. return $cache->set($key, $data, MEMCACHE_COMPRESSED, $ttl);
  2463. }
  2464. else {
  2465. return true;
  2466. }
  2467. }
  2468. return false;
  2469. }
  2470. /**
  2471. * Translate a domain name into it's corresponding root dn.
  2472. *
  2473. * @param string $domain Domain name
  2474. *
  2475. * @return string|bool Domain root DN or False on error
  2476. */
  2477. public function domain_root_dn($domain)
  2478. {
  2479. if (empty($domain)) {
  2480. return false;
  2481. }
  2482. $ckey = 'domain.root::' . $domain;
  2483. if ($result = $this->icache[$ckey]) {
  2484. return $result;
  2485. }
  2486. $this->_debug("Net_LDAP3::domain_root_dn($domain)");
  2487. if ($entry_attrs = $this->find_domain($domain)) {
  2488. $name_attribute = $this->config_get('domain_name_attribute');
  2489. if (empty($name_attribute)) {
  2490. $name_attribute = 'associateddomain';
  2491. }
  2492. if (is_array($entry_attrs)) {
  2493. if (!empty($entry_attrs['inetdomainbasedn'])) {
  2494. $domain_root_dn = $entry_attrs['inetdomainbasedn'];
  2495. }
  2496. else {
  2497. if (is_array($entry_attrs[$name_attribute])) {
  2498. $domain_root_dn = $this->_standard_root_dn($entry_attrs[$name_attribute][0]);
  2499. }
  2500. else {
  2501. $domain_root_dn = $this->_standard_root_dn($entry_attrs[$name_attribute]);
  2502. }
  2503. }
  2504. }
  2505. }
  2506. if (empty($domain_root_dn)) {
  2507. $domain_root_dn = $this->_standard_root_dn($domain);
  2508. }
  2509. $this->_debug("Net_LDAP3::domain_root_dn() result: $domain_root_dn");
  2510. return $this->icache[$ckey] = $domain_root_dn;
  2511. }
  2512. /**
  2513. * Find domain by name
  2514. *
  2515. * @param string $domain Domain name
  2516. * @param array $attributes Result attributes
  2517. *
  2518. * @return array|bool Domain attributes (plus 'dn' attribute) or False if not found
  2519. */
  2520. public function find_domain($domain, $attributes = array('*'))
  2521. {
  2522. if (empty($domain)) {
  2523. return false;
  2524. }
  2525. $ckey = 'domain::' . $domain;
  2526. $ickey = $ckey . '::' . md5(implode(',', $attributes));
  2527. if (isset($this->icache[$ickey])) {
  2528. return $this->icache[$ickey];
  2529. }
  2530. $this->_debug("Net_LDAP3::find_domain($domain)");
  2531. // use cache
  2532. $domain_dn = $this->get_cache_data($ckey);
  2533. if ($domain_dn) {
  2534. $result = $this->get_entry_attributes($domain_dn, $attributes);
  2535. if (!empty($result)) {
  2536. $result['dn'] = $domain_dn;
  2537. }
  2538. else {
  2539. $result = false;
  2540. }
  2541. }
  2542. else if ($domain_base_dn = $this->config_get('domain_base_dn')) {
  2543. $domain_filter = $this->config_get('domain_filter');
  2544. if (strpos($domain_filter, '%s') !== false) {
  2545. $domain_filter = str_replace('%s', self::quote_string($domain), $domain_filter);
  2546. }
  2547. else {
  2548. $name_attribute = $this->config_get('domain_name_attribute');
  2549. if (empty($name_attribute)) {
  2550. $name_attribute = 'associateddomain';
  2551. }
  2552. $domain_filter = "(&" . $domain_filter . "(" . $name_attribute . "=" . self::quote_string($domain) . "))";
  2553. }
  2554. if ($result = $this->search($domain_base_dn, $domain_filter, 'sub', $attributes)) {
  2555. $result = $result->entries(true);
  2556. $domain_dn = key($result);
  2557. if (empty($domain_dn)) {
  2558. $result = false;
  2559. }
  2560. else {
  2561. $result = $result[$domain_dn];
  2562. $result['dn'] = $domain_dn;
  2563. // cache domain DN
  2564. $this->set_cache_data($ckey, $domain_dn);
  2565. }
  2566. }
  2567. }
  2568. $this->_debug("Net_LDAP3::find_domain() result: " . var_export($result, true));
  2569. return $this->icache[$ickey] = $result;
  2570. }
  2571. /**
  2572. * From a domain name, such as 'kanarip.com', create a standard root
  2573. * dn, such as 'dc=kanarip,dc=com'.
  2574. *
  2575. * As the parameter $associatedDomains, either pass it an array (such
  2576. * as may have been returned by ldap_get_entries() or perhaps
  2577. * ldap_list()), where the function will assume the first value
  2578. * ($array[0]) to be the uber-level domain name, or pass it a string
  2579. * such as 'kanarip.nl'.
  2580. *
  2581. * @return string
  2582. */
  2583. protected function _standard_root_dn($associatedDomains)
  2584. {
  2585. if (is_array($associatedDomains)) {
  2586. // Usually, the associatedDomain in position 0 is the naming attribute associatedDomain
  2587. if ($associatedDomains['count'] > 1) {
  2588. // Issue a debug message here
  2589. $relevant_associatedDomain = $associatedDomains[0];
  2590. }
  2591. else {
  2592. $relevant_associatedDomain = $associatedDomains[0];
  2593. }
  2594. }
  2595. else {
  2596. $relevant_associatedDomain = $associatedDomains;
  2597. }
  2598. return 'dc=' . implode(',dc=', explode('.', $relevant_associatedDomain));
  2599. }
  2600. }