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

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