ldap.class.php 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593
  1. <?php
  2. /* Copyright (C) 2004 Rodolphe Quiedeville <rodolphe@quiedeville.org>
  3. * Copyright (C) 2004 Benoit Mortier <benoit.mortier@opensides.be>
  4. * Copyright (C) 2005-2021 Regis Houssin <regis.houssin@inodbox.com>
  5. * Copyright (C) 2006-2021 Laurent Destailleur <eldy@users.sourceforge.net>
  6. *
  7. * This program is free software; you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation; either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. * or see https://www.gnu.org/
  20. */
  21. /**
  22. * \file htdocs/core/class/ldap.class.php
  23. * \brief File of class to manage LDAP features
  24. *
  25. * Note:
  26. * LDAP_ESCAPE_FILTER is to escape char array('\\', '*', '(', ')', "\x00")
  27. * LDAP_ESCAPE_DN is to escape char array('\\', ',', '=', '+', '<', '>', ';', '"', '#')
  28. */
  29. /**
  30. * Class to manage LDAP features
  31. */
  32. class Ldap
  33. {
  34. /**
  35. * @var string Error code (or message)
  36. */
  37. public $error = '';
  38. /**
  39. * @var string[] Array of error strings
  40. */
  41. public $errors = array();
  42. /**
  43. * Tableau des serveurs (IP addresses ou nom d'hotes)
  44. */
  45. public $server = array();
  46. /**
  47. * Current connected server
  48. */
  49. public $connectedServer;
  50. /**
  51. * @var int server port
  52. */
  53. public $serverPort;
  54. /**
  55. * Base DN (e.g. "dc=foo,dc=com")
  56. */
  57. public $dn;
  58. /**
  59. * type de serveur, actuellement OpenLdap et Active Directory
  60. */
  61. public $serverType;
  62. /**
  63. * Version du protocole ldap
  64. */
  65. public $ldapProtocolVersion;
  66. /**
  67. * Server DN
  68. */
  69. public $domain;
  70. public $domainFQDN;
  71. /**
  72. * @var int bind
  73. */
  74. public $bind;
  75. /**
  76. * User administrateur Ldap
  77. * Active Directory ne supporte pas les connexions anonymes
  78. */
  79. public $searchUser;
  80. /**
  81. * Mot de passe de l'administrateur
  82. * Active Directory ne supporte pas les connexions anonymes
  83. */
  84. public $searchPassword;
  85. /**
  86. * DN des utilisateurs
  87. */
  88. public $people;
  89. /**
  90. * DN des groupes
  91. */
  92. public $groups;
  93. /**
  94. * Code erreur retourne par le serveur Ldap
  95. */
  96. public $ldapErrorCode;
  97. /**
  98. * Message texte de l'erreur
  99. */
  100. public $ldapErrorText;
  101. /**
  102. * @var string
  103. */
  104. public $filter;
  105. /**
  106. * @var string
  107. */
  108. public $filtergroup;
  109. /**
  110. * @var string
  111. */
  112. public $filtermember;
  113. /**
  114. * @var string attr_login
  115. */
  116. public $attr_login;
  117. /**
  118. * @var string attr_sambalogin
  119. */
  120. public $attr_sambalogin;
  121. /**
  122. * @var string attr_name
  123. */
  124. public $attr_name;
  125. /**
  126. * @var string attr_firstname
  127. */
  128. public $attr_firstname;
  129. /**
  130. * @var string attr_mail
  131. */
  132. public $attr_mail;
  133. /**
  134. * @var string attr_phone
  135. */
  136. public $attr_phone;
  137. /**
  138. * @var string attr_fax
  139. */
  140. public $attr_fax;
  141. /**
  142. * @var string attr_mobile
  143. */
  144. public $attr_mobile;
  145. /**
  146. * @var int badpwdtime
  147. */
  148. public $badpwdtime;
  149. /**
  150. * @var string ladpUserDN
  151. */
  152. public $ldapUserDN;
  153. //Fetch user
  154. public $name;
  155. public $firstname;
  156. public $login;
  157. public $phone;
  158. public $fax;
  159. public $mail;
  160. public $mobile;
  161. public $uacf;
  162. public $pwdlastset;
  163. public $ldapcharset = 'UTF-8'; // LDAP should be UTF-8 encoded
  164. /**
  165. * The internal LDAP connection handle
  166. */
  167. public $connection;
  168. /**
  169. * Result of any connections etc.
  170. */
  171. public $result;
  172. /**
  173. * No Ldap synchronization
  174. */
  175. const SYNCHRO_NONE = 0;
  176. /**
  177. * Dolibarr to Ldap synchronization
  178. */
  179. const SYNCHRO_DOLIBARR_TO_LDAP = 1;
  180. /**
  181. * Ldap to Dolibarr synchronization
  182. */
  183. const SYNCHRO_LDAP_TO_DOLIBARR = 2;
  184. /**
  185. * Constructor
  186. */
  187. public function __construct()
  188. {
  189. global $conf;
  190. // Server
  191. if (getDolGlobalString('LDAP_SERVER_HOST')) {
  192. $this->server[] = $conf->global->LDAP_SERVER_HOST;
  193. }
  194. if (getDolGlobalString('LDAP_SERVER_HOST_SLAVE')) {
  195. $this->server[] = $conf->global->LDAP_SERVER_HOST_SLAVE;
  196. }
  197. $this->serverPort = getDolGlobalInt('LDAP_SERVER_PORT', 389);
  198. $this->ldapProtocolVersion = getDolGlobalString('LDAP_SERVER_PROTOCOLVERSION');
  199. $this->dn = getDolGlobalString('LDAP_SERVER_DN');
  200. $this->serverType = getDolGlobalString('LDAP_SERVER_TYPE');
  201. $this->domain = getDolGlobalString('LDAP_SERVER_DN');
  202. $this->searchUser = getDolGlobalString('LDAP_ADMIN_DN');
  203. $this->searchPassword = getDolGlobalString('LDAP_ADMIN_PASS');
  204. $this->people = getDolGlobalString('LDAP_USER_DN');
  205. $this->groups = getDolGlobalString('LDAP_GROUP_DN');
  206. $this->filter = getDolGlobalString('LDAP_FILTER_CONNECTION'); // Filter on user
  207. $this->filtergroup = getDolGlobalString('LDAP_GROUP_FILTER'); // Filter on groups
  208. $this->filtermember = getDolGlobalString('LDAP_MEMBER_FILTER'); // Filter on member
  209. // Users
  210. $this->attr_login = getDolGlobalString('LDAP_FIELD_LOGIN'); //unix
  211. $this->attr_sambalogin = getDolGlobalString('LDAP_FIELD_LOGIN_SAMBA'); //samba, activedirectory
  212. $this->attr_name = getDolGlobalString('LDAP_FIELD_NAME');
  213. $this->attr_firstname = getDolGlobalString('LDAP_FIELD_FIRSTNAME');
  214. $this->attr_mail = getDolGlobalString('LDAP_FIELD_MAIL');
  215. $this->attr_phone = getDolGlobalString('LDAP_FIELD_PHONE');
  216. $this->attr_fax = getDolGlobalString('LDAP_FIELD_FAX');
  217. $this->attr_mobile = getDolGlobalString('LDAP_FIELD_MOBILE');
  218. }
  219. // Connection handling methods -------------------------------------------
  220. // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
  221. /**
  222. * Connect and bind
  223. * Use this->server, this->serverPort, this->ldapProtocolVersion, this->serverType, this->searchUser, this->searchPassword
  224. * After return, this->connection and $this->bind are defined
  225. *
  226. * @return int Return integer <0 if KO, 1 if bind anonymous, 2 if bind auth
  227. */
  228. public function connect_bind()
  229. {
  230. // phpcs:enable
  231. global $conf;
  232. global $dolibarr_main_auth_ldap_debug;
  233. $connected = 0;
  234. $this->bind = 0;
  235. $this->error = 0;
  236. $this->connectedServer = '';
  237. $ldapdebug = ((empty($dolibarr_main_auth_ldap_debug) || $dolibarr_main_auth_ldap_debug == "false") ? false : true);
  238. if ($ldapdebug) {
  239. dol_syslog(get_class($this)."::connect_bind");
  240. print "DEBUG: connect_bind<br>\n";
  241. }
  242. // Check parameters
  243. if (count($this->server) == 0 || empty($this->server[0])) {
  244. $this->error = 'LDAP setup (file conf.php) is not complete';
  245. dol_syslog(get_class($this)."::connect_bind ".$this->error, LOG_WARNING);
  246. return -1;
  247. }
  248. if (!function_exists("ldap_connect")) {
  249. $this->error = 'LDAPFunctionsNotAvailableOnPHP';
  250. dol_syslog(get_class($this)."::connect_bind ".$this->error, LOG_WARNING);
  251. $return = -1;
  252. }
  253. if (empty($this->error)) {
  254. // Loop on each ldap server
  255. foreach ($this->server as $host) {
  256. if ($connected) {
  257. break;
  258. }
  259. if (empty($host)) {
  260. continue;
  261. }
  262. if ($this->serverPing($host, $this->serverPort) === true) {
  263. if ($ldapdebug) {
  264. dol_syslog(get_class($this)."::connect_bind serverPing true, we try ldap_connect to ".$host);
  265. }
  266. if (version_compare(PHP_VERSION, '8.3.0', '>=')) {
  267. $uri = $host.':'.$this->serverPort;
  268. $this->connection = ldap_connect($uri);
  269. } else {
  270. $this->connection = ldap_connect($host, $this->serverPort);
  271. }
  272. } else {
  273. if (preg_match('/^ldaps/i', $host)) {
  274. // With host = ldaps://server, the serverPing to ssl://server sometimes fails, even if the ldap_connect succeed, so
  275. // we test this case and continue in such a case even if serverPing fails.
  276. if ($ldapdebug) {
  277. dol_syslog(get_class($this)."::connect_bind serverPing false, we try ldap_connect to ".$host);
  278. }
  279. if (version_compare(PHP_VERSION, '8.3.0', '>=')) {
  280. $uri = $host.':'.$this->serverPort;
  281. $this->connection = ldap_connect($uri);
  282. } else {
  283. $this->connection = ldap_connect($host, $this->serverPort);
  284. }
  285. } else {
  286. continue;
  287. }
  288. }
  289. if (is_resource($this->connection) || is_object($this->connection)) {
  290. if ($ldapdebug) {
  291. dol_syslog(get_class($this)."::connect_bind this->connection is ok", LOG_DEBUG);
  292. }
  293. // Upgrade connexion to TLS, if requested by the configuration
  294. if (getDolGlobalString('LDAP_SERVER_USE_TLS')) {
  295. // For test/debug
  296. //ldap_set_option($this->connection, LDAP_OPT_DEBUG_LEVEL, 7);
  297. //ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, 3);
  298. //ldap_set_option($this->connection, LDAP_OPT_REFERRALS, 0);
  299. $resulttls = ldap_start_tls($this->connection);
  300. if (!$resulttls) {
  301. dol_syslog(get_class($this)."::connect_bind failed to start tls", LOG_WARNING);
  302. $this->error = 'ldap_start_tls Failed to start TLS '.ldap_errno($this->connection).' '.ldap_error($this->connection);
  303. $connected = 0;
  304. $this->unbind();
  305. }
  306. }
  307. // Execute the ldap_set_option here (after connect and before bind)
  308. $this->setVersion();
  309. ldap_set_option($this->connection, LDAP_OPT_SIZELIMIT, 0); // no limit here. should return true.
  310. if ($this->serverType == "activedirectory") {
  311. $result = $this->setReferrals();
  312. dol_syslog(get_class($this)."::connect_bind try bindauth for activedirectory on ".$host." user=".$this->searchUser." password=".preg_replace('/./', '*', $this->searchPassword), LOG_DEBUG);
  313. $this->result = $this->bindauth($this->searchUser, $this->searchPassword);
  314. if ($this->result) {
  315. $this->bind = $this->result;
  316. $connected = 2;
  317. $this->connectedServer = $host;
  318. break;
  319. } else {
  320. $this->error = ldap_errno($this->connection).' '.ldap_error($this->connection);
  321. }
  322. } else {
  323. // Try in auth mode
  324. if ($this->searchUser && $this->searchPassword) {
  325. dol_syslog(get_class($this)."::connect_bind try bindauth on ".$host." user=".$this->searchUser." password=".preg_replace('/./', '*', $this->searchPassword), LOG_DEBUG);
  326. $this->result = $this->bindauth($this->searchUser, $this->searchPassword);
  327. if ($this->result) {
  328. $this->bind = $this->result;
  329. $connected = 2;
  330. $this->connectedServer = $host;
  331. break;
  332. } else {
  333. $this->error = ldap_errno($this->connection).' '.ldap_error($this->connection);
  334. }
  335. }
  336. // Try in anonymous
  337. if (!$this->bind) {
  338. dol_syslog(get_class($this)."::connect_bind try bind anonymously on ".$host, LOG_DEBUG);
  339. $result = $this->bind();
  340. if ($result) {
  341. $this->bind = $this->result;
  342. $connected = 1;
  343. $this->connectedServer = $host;
  344. break;
  345. } else {
  346. $this->error = ldap_errno($this->connection).' '.ldap_error($this->connection);
  347. }
  348. }
  349. }
  350. }
  351. if (!$connected) {
  352. $this->unbind();
  353. }
  354. } // End loop on each server
  355. }
  356. if ($connected) {
  357. $return = $connected;
  358. dol_syslog(get_class($this)."::connect_bind return=".$return, LOG_DEBUG);
  359. } else {
  360. $this->error = 'Failed to connect to LDAP'.($this->error ? ': '.$this->error : '');
  361. $return = -1;
  362. dol_syslog(get_class($this)."::connect_bind return=".$return.' - '.$this->error, LOG_WARNING);
  363. }
  364. return $return;
  365. }
  366. /**
  367. * Simply closes the connection set up earlier. Returns true if OK, false if there was an error.
  368. * This method seems a duplicate/alias of unbind().
  369. *
  370. * @return boolean true or false
  371. * @deprecated ldap_close is an alias of ldap_unbind, so use unbind() instead.
  372. * @see unbind()
  373. */
  374. public function close()
  375. {
  376. return $this->unbind();
  377. }
  378. /**
  379. * Anonymously binds to the connection. After this is done,
  380. * queries and searches can be done - but read-only.
  381. *
  382. * @return boolean true or false
  383. */
  384. public function bind()
  385. {
  386. if (!$this->result = @ldap_bind($this->connection)) {
  387. $this->ldapErrorCode = ldap_errno($this->connection);
  388. $this->ldapErrorText = ldap_error($this->connection);
  389. $this->error = $this->ldapErrorCode." ".$this->ldapErrorText;
  390. return false;
  391. } else {
  392. return true;
  393. }
  394. }
  395. /**
  396. * Binds as an authenticated user, which usually allows for write
  397. * access. The FULL dn must be passed. For a directory manager, this is
  398. * "cn=Directory Manager" under iPlanet. For a user, it will be something
  399. * like "uid=jbloggs,ou=People,dc=foo,dc=com".
  400. *
  401. * @param string $bindDn DN
  402. * @param string $pass Password
  403. * @return boolean true or false
  404. */
  405. public function bindauth($bindDn, $pass)
  406. {
  407. if (!$this->result = @ldap_bind($this->connection, $bindDn, $pass)) {
  408. $this->ldapErrorCode = ldap_errno($this->connection);
  409. $this->ldapErrorText = ldap_error($this->connection);
  410. $this->error = $this->ldapErrorCode." ".$this->ldapErrorText;
  411. return false;
  412. } else {
  413. return true;
  414. }
  415. }
  416. /**
  417. * Unbind of LDAP server (close connection).
  418. *
  419. * @return boolean true or false
  420. * @see close()
  421. */
  422. public function unbind()
  423. {
  424. $this->result = true;
  425. if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
  426. if (is_object($this->connection)) {
  427. try {
  428. $this->result = ldap_unbind($this->connection);
  429. } catch (Throwable $exception) {
  430. $this->error = 'Failed to unbind LDAP connection: '.$exception;
  431. $this->result = false;
  432. dol_syslog(get_class($this).'::unbind - '.$this->error, LOG_WARNING);
  433. }
  434. }
  435. } else {
  436. if (is_resource($this->connection)) {
  437. $this->result = @ldap_unbind($this->connection);
  438. }
  439. }
  440. if ($this->result) {
  441. return true;
  442. } else {
  443. return false;
  444. }
  445. }
  446. /**
  447. * Verification de la version du serveur ldap.
  448. *
  449. * @return string version
  450. */
  451. public function getVersion()
  452. {
  453. $version = 0;
  454. $version = @ldap_get_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $version);
  455. return $version;
  456. }
  457. /**
  458. * Change ldap protocol version to use.
  459. *
  460. * @return boolean version
  461. */
  462. public function setVersion()
  463. {
  464. // LDAP_OPT_PROTOCOL_VERSION est une constante qui vaut 17
  465. $ldapsetversion = ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->ldapProtocolVersion);
  466. return $ldapsetversion;
  467. }
  468. /**
  469. * changement du referrals.
  470. *
  471. * @return boolean referrals
  472. */
  473. public function setReferrals()
  474. {
  475. // LDAP_OPT_REFERRALS est une constante qui vaut ?
  476. $ldapreferrals = ldap_set_option($this->connection, LDAP_OPT_REFERRALS, 0);
  477. return $ldapreferrals;
  478. }
  479. /**
  480. * Add a LDAP entry
  481. * Ldap object connect and bind must have been done
  482. *
  483. * @param string $dn DN entry key
  484. * @param array $info Attributes array
  485. * @param User $user Objet user that create
  486. * @return int Return integer <0 if KO, >0 if OK
  487. */
  488. public function add($dn, $info, $user)
  489. {
  490. dol_syslog(get_class($this)."::add dn=".$dn." info=".print_r($info, true));
  491. // Check parameters
  492. if (!$this->connection) {
  493. $this->error = "NotConnected";
  494. return -2;
  495. }
  496. if (!$this->bind) {
  497. $this->error = "NotConnected";
  498. return -3;
  499. }
  500. // Encode to LDAP page code
  501. $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
  502. foreach ($info as $key => $val) {
  503. if (!is_array($val)) {
  504. $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
  505. }
  506. }
  507. $this->dump($dn, $info);
  508. //print_r($info);
  509. $result = @ldap_add($this->connection, $dn, $info);
  510. if ($result) {
  511. dol_syslog(get_class($this)."::add successfull", LOG_DEBUG);
  512. return 1;
  513. } else {
  514. $this->ldapErrorCode = @ldap_errno($this->connection);
  515. $this->ldapErrorText = @ldap_error($this->connection);
  516. $this->error = $this->ldapErrorCode." ".$this->ldapErrorText;
  517. dol_syslog(get_class($this)."::add failed: ".$this->error, LOG_ERR);
  518. return -1;
  519. }
  520. }
  521. /**
  522. * Modify a LDAP entry
  523. * Ldap object connect and bind must have been done
  524. *
  525. * @param string $dn DN entry key
  526. * @param array $info Attributes array
  527. * @param User $user Objet user that modify
  528. * @return int Return integer <0 if KO, >0 if OK
  529. */
  530. public function modify($dn, $info, $user)
  531. {
  532. dol_syslog(get_class($this)."::modify dn=".$dn." info=".print_r($info, true));
  533. // Check parameters
  534. if (!$this->connection) {
  535. $this->error = "NotConnected";
  536. return -2;
  537. }
  538. if (!$this->bind) {
  539. $this->error = "NotConnected";
  540. return -3;
  541. }
  542. // Encode to LDAP page code
  543. $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
  544. foreach ($info as $key => $val) {
  545. if (!is_array($val)) {
  546. $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
  547. }
  548. }
  549. $this->dump($dn, $info);
  550. //print_r($info);
  551. // For better compatibility with Samba4 AD
  552. if ($this->serverType == "activedirectory") {
  553. unset($info['cn']); // To avoid error : Operation not allowed on RDN (Code 67)
  554. // To avoid error : LDAP Error: 53 (Unwilling to perform)
  555. if (isset($info['unicodePwd'])) {
  556. $info['unicodePwd'] = mb_convert_encoding("\"".$info['unicodePwd']."\"", "UTF-16LE", "UTF-8");
  557. }
  558. }
  559. $result = @ldap_modify($this->connection, $dn, $info);
  560. if ($result) {
  561. dol_syslog(get_class($this)."::modify successfull", LOG_DEBUG);
  562. return 1;
  563. } else {
  564. $this->error = @ldap_error($this->connection);
  565. dol_syslog(get_class($this)."::modify failed: ".$this->error, LOG_ERR);
  566. return -1;
  567. }
  568. }
  569. /**
  570. * Rename a LDAP entry
  571. * Ldap object connect and bind must have been done
  572. *
  573. * @param string $dn Old DN entry key (uid=qqq,ou=xxx,dc=aaa,dc=bbb) (before update)
  574. * @param string $newrdn New RDN entry key (uid=qqq)
  575. * @param string $newparent New parent (ou=xxx,dc=aaa,dc=bbb)
  576. * @param User $user Objet user that modify
  577. * @param bool $deleteoldrdn If true the old RDN value(s) is removed, else the old RDN value(s) is retained as non-distinguished values of the entry.
  578. * @return int Return integer <0 if KO, >0 if OK
  579. */
  580. public function rename($dn, $newrdn, $newparent, $user, $deleteoldrdn = true)
  581. {
  582. dol_syslog(get_class($this)."::modify dn=".$dn." newrdn=".$newrdn." newparent=".$newparent." deleteoldrdn=".($deleteoldrdn ? 1 : 0));
  583. // Check parameters
  584. if (!$this->connection) {
  585. $this->error = "NotConnected";
  586. return -2;
  587. }
  588. if (!$this->bind) {
  589. $this->error = "NotConnected";
  590. return -3;
  591. }
  592. // Encode to LDAP page code
  593. $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
  594. $newrdn = $this->convFromOutputCharset($newrdn, $this->ldapcharset);
  595. $newparent = $this->convFromOutputCharset($newparent, $this->ldapcharset);
  596. //print_r($info);
  597. $result = @ldap_rename($this->connection, $dn, $newrdn, $newparent, $deleteoldrdn);
  598. if ($result) {
  599. dol_syslog(get_class($this)."::rename successfull", LOG_DEBUG);
  600. return 1;
  601. } else {
  602. $this->error = @ldap_error($this->connection);
  603. dol_syslog(get_class($this)."::rename failed: ".$this->error, LOG_ERR);
  604. return -1;
  605. }
  606. }
  607. /**
  608. * Modify a LDAP entry (to use if dn != olddn)
  609. * Ldap object connect and bind must have been done
  610. *
  611. * @param string $dn DN entry key
  612. * @param array $info Attributes array
  613. * @param User $user Objet user that update
  614. * @param string $olddn Old DN entry key (before update)
  615. * @param string $newrdn New RDN entry key (uid=qqq) (for ldap_rename)
  616. * @param string $newparent New parent (ou=xxx,dc=aaa,dc=bbb) (for ldap_rename)
  617. * @return int Return integer <0 if KO, >0 if OK
  618. */
  619. public function update($dn, $info, $user, $olddn, $newrdn = false, $newparent = false)
  620. {
  621. dol_syslog(get_class($this)."::update dn=".$dn." olddn=".$olddn);
  622. // Check parameters
  623. if (!$this->connection) {
  624. $this->error = "NotConnected";
  625. return -2;
  626. }
  627. if (!$this->bind) {
  628. $this->error = "NotConnected";
  629. return -3;
  630. }
  631. if (!$olddn || $olddn != $dn) {
  632. if (!empty($olddn) && !empty($newrdn) && !empty($newparent) && $this->ldapProtocolVersion === '3') {
  633. // This function currently only works with LDAPv3
  634. $result = $this->rename($olddn, $newrdn, $newparent, $user, true);
  635. $result = $this->modify($dn, $info, $user); // We force "modify" for avoid some fields not modify
  636. } else {
  637. // If change we make is rename the key of LDAP record, we create new one and if ok, we delete old one.
  638. $result = $this->add($dn, $info, $user);
  639. if ($result > 0 && $olddn && $olddn != $dn) {
  640. $result = $this->delete($olddn); // If add fails, we do not try to delete old one
  641. }
  642. }
  643. } else {
  644. //$result = $this->delete($olddn);
  645. $result = $this->add($dn, $info, $user); // If record has been deleted from LDAP, we recreate it. We ignore error if it already exists.
  646. $result = $this->modify($dn, $info, $user); // We use add/modify instead of delete/add when olddn is received
  647. }
  648. if ($result <= 0) {
  649. $this->error = ldap_error($this->connection).' (Code '.ldap_errno($this->connection).") ".$this->error;
  650. dol_syslog(get_class($this)."::update ".$this->error, LOG_ERR);
  651. //print_r($info);
  652. return -1;
  653. } else {
  654. dol_syslog(get_class($this)."::update done successfully");
  655. return 1;
  656. }
  657. }
  658. /**
  659. * Delete a LDAP entry
  660. * Ldap object connect and bind must have been done
  661. *
  662. * @param string $dn DN entry key
  663. * @return int Return integer <0 if KO, >0 if OK
  664. */
  665. public function delete($dn)
  666. {
  667. dol_syslog(get_class($this)."::delete Delete LDAP entry dn=".$dn);
  668. // Check parameters
  669. if (!$this->connection) {
  670. $this->error = "NotConnected";
  671. return -2;
  672. }
  673. if (!$this->bind) {
  674. $this->error = "NotConnected";
  675. return -3;
  676. }
  677. // Encode to LDAP page code
  678. $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
  679. $result = @ldap_delete($this->connection, $dn);
  680. if ($result) {
  681. return 1;
  682. }
  683. return -1;
  684. }
  685. // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
  686. /**
  687. * Build a LDAP message
  688. *
  689. * @param string $dn DN entry key
  690. * @param array $info Attributes array
  691. * @return string Content of file
  692. */
  693. public function dump_content($dn, $info)
  694. {
  695. // phpcs:enable
  696. $content = '';
  697. // Create file content
  698. if (preg_match('/^ldap/', $this->server[0])) {
  699. $target = "-H ".join(',', $this->server);
  700. } else {
  701. $target = "-h ".join(',', $this->server)." -p ".$this->serverPort;
  702. }
  703. $content .= "# ldapadd $target -c -v -D ".$this->searchUser." -W -f ldapinput.in\n";
  704. $content .= "# ldapmodify $target -c -v -D ".$this->searchUser." -W -f ldapinput.in\n";
  705. $content .= "# ldapdelete $target -c -v -D ".$this->searchUser." -W -f ldapinput.in\n";
  706. if (in_array('localhost', $this->server)) {
  707. $content .= "# If commands fails to connect, try without -h and -p\n";
  708. }
  709. $content .= "dn: ".$dn."\n";
  710. foreach ($info as $key => $value) {
  711. if (!is_array($value)) {
  712. $content .= "$key: $value\n";
  713. } else {
  714. foreach ($value as $valuevalue) {
  715. $content .= "$key: $valuevalue\n";
  716. }
  717. }
  718. }
  719. return $content;
  720. }
  721. /**
  722. * Dump a LDAP message to ldapinput.in file
  723. *
  724. * @param string $dn DN entry key
  725. * @param array $info Attributes array
  726. * @return int Return integer <0 if KO, >0 if OK
  727. */
  728. public function dump($dn, $info)
  729. {
  730. global $conf;
  731. // Create content
  732. $content = $this->dump_content($dn, $info);
  733. //Create file
  734. $result = dol_mkdir($conf->ldap->dir_temp);
  735. $outputfile = $conf->ldap->dir_temp.'/ldapinput.in';
  736. $fp = fopen($outputfile, "w");
  737. if ($fp) {
  738. fputs($fp, $content);
  739. fclose($fp);
  740. dolChmod($outputfile);
  741. return 1;
  742. } else {
  743. return -1;
  744. }
  745. }
  746. /**
  747. * Ping a server before ldap_connect for avoid waiting
  748. *
  749. * @param string $host Server host or address
  750. * @param int $port Server port (default 389)
  751. * @param int $timeout Timeout in second (default 1s)
  752. * @return boolean true or false
  753. */
  754. public function serverPing($host, $port = 389, $timeout = 1)
  755. {
  756. $regs = array();
  757. if (preg_match('/^ldaps:\/\/([^\/]+)\/?$/', $host, $regs)) {
  758. // Replace ldaps:// by ssl://
  759. $host = 'ssl://'.$regs[1];
  760. } elseif (preg_match('/^ldap:\/\/([^\/]+)\/?$/', $host, $regs)) {
  761. // Remove ldap://
  762. $host = $regs[1];
  763. }
  764. //var_dump($newhostforstream); var_dump($host); var_dump($port);
  765. //$host = 'ssl://ldap.test.local:636';
  766. //$port = 636;
  767. $errno = $errstr = 0;
  768. /*
  769. if ($methodtochecktcpconnect == 'socket') {
  770. Try to use socket_create() method.
  771. Method that use stream_context_create() works only on registered listed in stream stream_get_wrappers(): http, https, ftp, ...
  772. }
  773. */
  774. // Use the method fsockopen to test tcp connect. No way to ignore ssl certificate errors with this method !
  775. $op = @fsockopen($host, $port, $errno, $errstr, $timeout);
  776. //var_dump($op);
  777. if (!$op) {
  778. return false; //DC is N/A
  779. } else {
  780. fclose($op); //explicitly close open socket connection
  781. return true; //DC is up & running, we can safely connect with ldap_connect
  782. }
  783. }
  784. // Attribute methods -----------------------------------------------------
  785. /**
  786. * Add a LDAP attribute in entry
  787. * Ldap object connect and bind must have been done
  788. *
  789. * @param string $dn DN entry key
  790. * @param array $info Attributes array
  791. * @param User $user Objet user that create
  792. * @return int Return integer <0 if KO, >0 if OK
  793. */
  794. public function addAttribute($dn, $info, $user)
  795. {
  796. dol_syslog(get_class($this)."::addAttribute dn=".$dn." info=".join(',', $info));
  797. // Check parameters
  798. if (!$this->connection) {
  799. $this->error = "NotConnected";
  800. return -2;
  801. }
  802. if (!$this->bind) {
  803. $this->error = "NotConnected";
  804. return -3;
  805. }
  806. // Encode to LDAP page code
  807. $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
  808. foreach ($info as $key => $val) {
  809. if (!is_array($val)) {
  810. $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
  811. }
  812. }
  813. $this->dump($dn, $info);
  814. //print_r($info);
  815. $result = @ldap_mod_add($this->connection, $dn, $info);
  816. if ($result) {
  817. dol_syslog(get_class($this)."::add_attribute successfull", LOG_DEBUG);
  818. return 1;
  819. } else {
  820. $this->error = @ldap_error($this->connection);
  821. dol_syslog(get_class($this)."::add_attribute failed: ".$this->error, LOG_ERR);
  822. return -1;
  823. }
  824. }
  825. /**
  826. * Update a LDAP attribute in entry
  827. * Ldap object connect and bind must have been done
  828. *
  829. * @param string $dn DN entry key
  830. * @param array $info Attributes array
  831. * @param User $user Objet user that create
  832. * @return int Return integer <0 if KO, >0 if OK
  833. */
  834. public function updateAttribute($dn, $info, $user)
  835. {
  836. dol_syslog(get_class($this)."::updateAttribute dn=".$dn." info=".join(',', $info));
  837. // Check parameters
  838. if (!$this->connection) {
  839. $this->error = "NotConnected";
  840. return -2;
  841. }
  842. if (!$this->bind) {
  843. $this->error = "NotConnected";
  844. return -3;
  845. }
  846. // Encode to LDAP page code
  847. $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
  848. foreach ($info as $key => $val) {
  849. if (!is_array($val)) {
  850. $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
  851. }
  852. }
  853. $this->dump($dn, $info);
  854. //print_r($info);
  855. $result = @ldap_mod_replace($this->connection, $dn, $info);
  856. if ($result) {
  857. dol_syslog(get_class($this)."::updateAttribute successfull", LOG_DEBUG);
  858. return 1;
  859. } else {
  860. $this->error = @ldap_error($this->connection);
  861. dol_syslog(get_class($this)."::updateAttribute failed: ".$this->error, LOG_ERR);
  862. return -1;
  863. }
  864. }
  865. /**
  866. * Delete a LDAP attribute in entry
  867. * Ldap object connect and bind must have been done
  868. *
  869. * @param string $dn DN entry key
  870. * @param array $info Attributes array
  871. * @param User $user Objet user that create
  872. * @return int Return integer <0 if KO, >0 if OK
  873. */
  874. public function deleteAttribute($dn, $info, $user)
  875. {
  876. dol_syslog(get_class($this)."::deleteAttribute dn=".$dn." info=".join(',', $info));
  877. // Check parameters
  878. if (!$this->connection) {
  879. $this->error = "NotConnected";
  880. return -2;
  881. }
  882. if (!$this->bind) {
  883. $this->error = "NotConnected";
  884. return -3;
  885. }
  886. // Encode to LDAP page code
  887. $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
  888. foreach ($info as $key => $val) {
  889. if (!is_array($val)) {
  890. $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
  891. }
  892. }
  893. $this->dump($dn, $info);
  894. //print_r($info);
  895. $result = @ldap_mod_del($this->connection, $dn, $info);
  896. if ($result) {
  897. dol_syslog(get_class($this)."::deleteAttribute successfull", LOG_DEBUG);
  898. return 1;
  899. } else {
  900. $this->error = @ldap_error($this->connection);
  901. dol_syslog(get_class($this)."::deleteAttribute failed: ".$this->error, LOG_ERR);
  902. return -1;
  903. }
  904. }
  905. /**
  906. * Returns an array containing attributes and values for first record
  907. *
  908. * @param string $dn DN entry key
  909. * @param string $filter Filter
  910. * @return int|array Return integer <0 or false if KO, array if OK
  911. */
  912. public function getAttribute($dn, $filter)
  913. {
  914. // Check parameters
  915. if (!$this->connection) {
  916. $this->error = "NotConnected";
  917. return -2;
  918. }
  919. if (!$this->bind) {
  920. $this->error = "NotConnected";
  921. return -3;
  922. }
  923. $search = @ldap_search($this->connection, $dn, $filter);
  924. // Only one entry should ever be returned
  925. $entry = @ldap_first_entry($this->connection, $search);
  926. if (!$entry) {
  927. $this->ldapErrorCode = -1;
  928. $this->ldapErrorText = "Couldn't find entry";
  929. return 0; // Couldn't find entry...
  930. }
  931. // Get values
  932. if (!($values = ldap_get_attributes($this->connection, $entry))) {
  933. $this->ldapErrorCode = ldap_errno($this->connection);
  934. $this->ldapErrorText = ldap_error($this->connection);
  935. return 0; // No matching attributes
  936. }
  937. // Return an array containing the attributes.
  938. return $values;
  939. }
  940. /**
  941. * Returns an array containing values for an attribute and for first record matching filterrecord
  942. *
  943. * @param string $filterrecord Record
  944. * @param string $attribute Attributes
  945. * @return array|boolean
  946. */
  947. public function getAttributeValues($filterrecord, $attribute)
  948. {
  949. $attributes = array();
  950. $attributes[0] = $attribute;
  951. // We need to search for this user in order to get their entry.
  952. $this->result = @ldap_search($this->connection, $this->people, $filterrecord, $attributes);
  953. // Pourquoi cette ligne ?
  954. //$info = ldap_get_entries($this->connection, $this->result);
  955. // Only one entry should ever be returned (no user will have the same uid)
  956. $entry = ldap_first_entry($this->connection, $this->result);
  957. if (!$entry) {
  958. $this->ldapErrorCode = -1;
  959. $this->ldapErrorText = "Couldn't find user";
  960. return false; // Couldn't find the user...
  961. }
  962. // Get values
  963. if (!$values = @ldap_get_values($this->connection, $entry, $attribute)) {
  964. $this->ldapErrorCode = ldap_errno($this->connection);
  965. $this->ldapErrorText = ldap_error($this->connection);
  966. return false; // No matching attributes
  967. }
  968. // Return an array containing the attributes.
  969. return $values;
  970. }
  971. /**
  972. * Returns an array containing a details or list of LDAP record(s).
  973. * ldapsearch -LLLx -hlocalhost -Dcn=admin,dc=parinux,dc=org -w password -b "ou=adherents,ou=people,dc=parinux,dc=org" userPassword
  974. *
  975. * @param string $search Value of field to search, '*' for all. Not used if $activefilter is set.
  976. * @param string $userDn DN (Ex: ou=adherents,ou=people,dc=parinux,dc=org)
  977. * @param string $useridentifier Name of key field (Ex: uid).
  978. * @param array $attributeArray Array of fields required. Note this array must also contains field $useridentifier (Ex: sn,userPassword)
  979. * @param int $activefilter '1' or 'user'=use field this->filter as filter instead of parameter $search, 'group'=use field this->filtergroup as filter, 'member'=use field this->filtermember as filter
  980. * @param array $attributeAsArray Array of fields wanted as an array not a string
  981. * @return array|int Array of [id_record][ldap_field]=value
  982. */
  983. public function getRecords($search, $userDn, $useridentifier, $attributeArray, $activefilter = 0, $attributeAsArray = array())
  984. {
  985. $fulllist = array();
  986. dol_syslog(get_class($this)."::getRecords search=".$search." userDn=".$userDn." useridentifier=".$useridentifier." attributeArray=array(".join(',', $attributeArray).") activefilter=".$activefilter);
  987. // if the directory is AD, then bind first with the search user first
  988. if ($this->serverType == "activedirectory") {
  989. $this->bindauth($this->searchUser, $this->searchPassword);
  990. dol_syslog(get_class($this)."::bindauth serverType=activedirectory searchUser=".$this->searchUser);
  991. }
  992. // Define filter
  993. if (!empty($activefilter)) { // Use a predefined trusted filter (defined into setup by admin).
  994. if (((string) $activefilter == '1' || (string) $activefilter == 'user') && $this->filter) {
  995. $filter = '('.$this->filter.')';
  996. } elseif (((string) $activefilter == 'group') && $this->filtergroup) {
  997. $filter = '('.$this->filtergroup.')';
  998. } elseif (((string) $activefilter == 'member') && $this->filter) {
  999. $filter = '('.$this->filtermember.')';
  1000. } else {
  1001. // If this->filter/this->filtergroup is empty, make fiter on * (all)
  1002. $filter = '('.ldap_escape($useridentifier, '', LDAP_ESCAPE_FILTER).'=*)';
  1003. }
  1004. } else { // Use a filter forged using the $search value
  1005. $filter = '('.ldap_escape($useridentifier, '', LDAP_ESCAPE_FILTER).'='.ldap_escape($search, '', LDAP_ESCAPE_FILTER).')';
  1006. }
  1007. if (is_array($attributeArray)) {
  1008. // Return list with required fields
  1009. $attributeArray = array_values($attributeArray); // This is to force to have index reordered from 0 (not make ldap_search fails)
  1010. dol_syslog(get_class($this)."::getRecords connection=".$this->connectedServer.":".$this->serverPort." userDn=".$userDn." filter=".$filter." attributeArray=(".join(',', $attributeArray).")");
  1011. //var_dump($attributeArray);
  1012. $this->result = @ldap_search($this->connection, $userDn, $filter, $attributeArray);
  1013. } else {
  1014. // Return list with fields selected by default
  1015. dol_syslog(get_class($this)."::getRecords connection=".$this->connectedServer.":".$this->serverPort." userDn=".$userDn." filter=".$filter);
  1016. $this->result = @ldap_search($this->connection, $userDn, $filter);
  1017. }
  1018. if (!$this->result) {
  1019. $this->error = 'LDAP search failed: '.ldap_errno($this->connection)." ".ldap_error($this->connection);
  1020. return -1;
  1021. }
  1022. $info = @ldap_get_entries($this->connection, $this->result);
  1023. // Warning: Dans info, les noms d'attributs sont en minuscule meme si passe
  1024. // a ldap_search en majuscule !!!
  1025. //print_r($info);
  1026. for ($i = 0; $i < $info["count"]; $i++) {
  1027. $recordid = $this->convToOutputCharset($info[$i][strtolower($useridentifier)][0], $this->ldapcharset);
  1028. if ($recordid) {
  1029. //print "Found record with key $useridentifier=".$recordid."<br>\n";
  1030. $fulllist[$recordid][$useridentifier] = $recordid;
  1031. // Add to the array for each attribute in my list
  1032. $num = count($attributeArray);
  1033. for ($j = 0; $j < $num; $j++) {
  1034. $keyattributelower = strtolower($attributeArray[$j]);
  1035. //print " Param ".$attributeArray[$j]."=".$info[$i][$keyattributelower][0]."<br>\n";
  1036. //permet de recuperer le SID avec Active Directory
  1037. if ($this->serverType == "activedirectory" && $keyattributelower == "objectsid") {
  1038. $objectsid = $this->getObjectSid($recordid);
  1039. $fulllist[$recordid][$attributeArray[$j]] = $objectsid;
  1040. } else {
  1041. if (in_array($attributeArray[$j], $attributeAsArray) && is_array($info[$i][$keyattributelower])) {
  1042. $valueTab = array();
  1043. foreach ($info[$i][$keyattributelower] as $key => $value) {
  1044. $valueTab[$key] = $this->convToOutputCharset($value, $this->ldapcharset);
  1045. }
  1046. $fulllist[$recordid][$attributeArray[$j]] = $valueTab;
  1047. } else {
  1048. $fulllist[$recordid][$attributeArray[$j]] = $this->convToOutputCharset($info[$i][$keyattributelower][0], $this->ldapcharset);
  1049. }
  1050. }
  1051. }
  1052. }
  1053. }
  1054. asort($fulllist);
  1055. return $fulllist;
  1056. }
  1057. /**
  1058. * Converts a little-endian hex-number to one, that 'hexdec' can convert
  1059. * Required by Active Directory
  1060. *
  1061. * @param string $hex Hex value
  1062. * @return string Little endian
  1063. */
  1064. public function littleEndian($hex)
  1065. {
  1066. $result = '';
  1067. for ($x = dol_strlen($hex) - 2; $x >= 0; $x = $x - 2) {
  1068. $result .= substr($hex, $x, 2);
  1069. }
  1070. return $result;
  1071. }
  1072. /**
  1073. * Recupere le SID de l'utilisateur
  1074. * Required by Active Directory
  1075. *
  1076. * @param string $ldapUser Login de l'utilisateur
  1077. * @return string Sid
  1078. */
  1079. public function getObjectSid($ldapUser)
  1080. {
  1081. $criteria = '('.$this->getUserIdentifier().'='.$ldapUser.')';
  1082. $justthese = array("objectsid");
  1083. // if the directory is AD, then bind first with the search user first
  1084. if ($this->serverType == "activedirectory") {
  1085. $this->bindauth($this->searchUser, $this->searchPassword);
  1086. }
  1087. $i = 0;
  1088. $searchDN = $this->people;
  1089. while ($i <= 2) {
  1090. $ldapSearchResult = @ldap_search($this->connection, $searchDN, $criteria, $justthese);
  1091. if (!$ldapSearchResult) {
  1092. $this->error = ldap_errno($this->connection)." ".ldap_error($this->connection);
  1093. return -1;
  1094. }
  1095. $entry = ldap_first_entry($this->connection, $ldapSearchResult);
  1096. if (!$entry) {
  1097. // Si pas de resultat on cherche dans le domaine
  1098. $searchDN = $this->domain;
  1099. $i++;
  1100. } else {
  1101. $i++;
  1102. $i++;
  1103. }
  1104. }
  1105. if ($entry) {
  1106. $ldapBinary = ldap_get_values_len($this->connection, $entry, "objectsid");
  1107. $SIDText = $this->binSIDtoText($ldapBinary[0]);
  1108. return $SIDText;
  1109. } else {
  1110. $this->error = ldap_errno($this->connection)." ".ldap_error($this->connection);
  1111. return '?';
  1112. }
  1113. }
  1114. /**
  1115. * Returns the textual SID
  1116. * Indispensable pour Active Directory
  1117. *
  1118. * @param string $binsid Binary SID
  1119. * @return string Textual SID
  1120. */
  1121. public function binSIDtoText($binsid)
  1122. {
  1123. $hex_sid = bin2hex($binsid);
  1124. $rev = hexdec(substr($hex_sid, 0, 2)); // Get revision-part of SID
  1125. $subcount = hexdec(substr($hex_sid, 2, 2)); // Get count of sub-auth entries
  1126. $auth = hexdec(substr($hex_sid, 4, 12)); // SECURITY_NT_AUTHORITY
  1127. $result = "$rev-$auth";
  1128. for ($x = 0; $x < $subcount; $x++) {
  1129. $result .= "-".hexdec($this->littleEndian(substr($hex_sid, 16 + ($x * 8), 8))); // get all SECURITY_NT_AUTHORITY
  1130. }
  1131. return $result;
  1132. }
  1133. /**
  1134. * Fonction de recherche avec filtre
  1135. * this->connection doit etre defini donc la methode bind ou bindauth doit avoir deja ete appelee
  1136. * Ne pas utiliser pour recherche d'une liste donnee de proprietes
  1137. * car conflit majuscule-minuscule. A n'utiliser que pour les pages
  1138. * 'Fiche LDAP' qui affiche champ lisibles par defaut.
  1139. *
  1140. * @param string $checkDn DN de recherche (Ex: ou=users,cn=my-domain,cn=com)
  1141. * @param string $filter Search filter (ex: (sn=nom_personne) )
  1142. * @return array|int Array with answers (key lowercased - value)
  1143. */
  1144. public function search($checkDn, $filter)
  1145. {
  1146. dol_syslog(get_class($this)."::search checkDn=".$checkDn." filter=".$filter);
  1147. $checkDn = $this->convFromOutputCharset($checkDn, $this->ldapcharset);
  1148. $filter = $this->convFromOutputCharset($filter, $this->ldapcharset);
  1149. // if the directory is AD, then bind first with the search user first
  1150. if ($this->serverType == "activedirectory") {
  1151. $this->bindauth($this->searchUser, $this->searchPassword);
  1152. }
  1153. $this->result = @ldap_search($this->connection, $checkDn, $filter);
  1154. $result = @ldap_get_entries($this->connection, $this->result);
  1155. if (!$result) {
  1156. $this->error = ldap_errno($this->connection)." ".ldap_error($this->connection);
  1157. return -1;
  1158. } else {
  1159. ldap_free_result($this->result);
  1160. return $result;
  1161. }
  1162. }
  1163. /**
  1164. * Load all attribute of a LDAP user
  1165. *
  1166. * @param User|string $user Not used.
  1167. * @param string $filter Filter for search. Must start with &.
  1168. * Examples: &(objectClass=inetOrgPerson) &(objectClass=user)(objectCategory=person) &(isMemberOf=cn=Sales,ou=Groups,dc=opencsi,dc=com)
  1169. * @return int >0 if OK, <0 if KO
  1170. */
  1171. public function fetch($user, $filter)
  1172. {
  1173. // Perform the search and get the entry handles
  1174. // if the directory is AD, then bind first with the search user first
  1175. if ($this->serverType == "activedirectory") {
  1176. $this->bindauth($this->searchUser, $this->searchPassword);
  1177. }
  1178. $searchDN = $this->people; // TODO Why searching in people then domain ?
  1179. $result = '';
  1180. $i = 0;
  1181. while ($i <= 2) {
  1182. dol_syslog(get_class($this)."::fetch search with searchDN=".$searchDN." filter=".$filter);
  1183. $this->result = @ldap_search($this->connection, $searchDN, $filter);
  1184. if ($this->result) {
  1185. $result = @ldap_get_entries($this->connection, $this->result);
  1186. if ($result['count'] > 0) {
  1187. dol_syslog('Ldap::fetch search found '.$result['count'].' records');
  1188. } else {
  1189. dol_syslog('Ldap::fetch search returns but found no records');
  1190. }
  1191. //var_dump($result);exit;
  1192. } else {
  1193. $this->error = ldap_errno($this->connection)." ".ldap_error($this->connection);
  1194. dol_syslog(get_class($this)."::fetch search fails");
  1195. return -1;
  1196. }
  1197. if (!$result) {
  1198. // Si pas de resultat on cherche dans le domaine
  1199. $searchDN = $this->domain;
  1200. $i++;
  1201. } else {
  1202. break;
  1203. }
  1204. }
  1205. if (!$result) {
  1206. $this->error = ldap_errno($this->connection)." ".ldap_error($this->connection);
  1207. return -1;
  1208. } else {
  1209. $this->name = $this->convToOutputCharset($result[0][$this->attr_name][0], $this->ldapcharset);
  1210. $this->firstname = $this->convToOutputCharset($result[0][$this->attr_firstname][0], $this->ldapcharset);
  1211. $this->login = $this->convToOutputCharset($result[0][$this->attr_login][0], $this->ldapcharset);
  1212. $this->phone = $this->convToOutputCharset($result[0][$this->attr_phone][0], $this->ldapcharset);
  1213. $this->fax = $this->convToOutputCharset($result[0][$this->attr_fax][0], $this->ldapcharset);
  1214. $this->mail = $this->convToOutputCharset($result[0][$this->attr_mail][0], $this->ldapcharset);
  1215. $this->mobile = $this->convToOutputCharset($result[0][$this->attr_mobile][0], $this->ldapcharset);
  1216. $this->uacf = $this->parseUACF($this->convToOutputCharset($result[0]["useraccountcontrol"][0], $this->ldapcharset));
  1217. if (isset($result[0]["pwdlastset"][0])) { // If expiration on password exists
  1218. $this->pwdlastset = ($result[0]["pwdlastset"][0] != 0) ? $this->convert_time($this->convToOutputCharset($result[0]["pwdlastset"][0], $this->ldapcharset)) : 0;
  1219. } else {
  1220. $this->pwdlastset = -1;
  1221. }
  1222. if (!$this->name && !$this->login) {
  1223. $this->pwdlastset = -1;
  1224. }
  1225. $this->badpwdtime = $this->convert_time($this->convToOutputCharset($result[0]["badpasswordtime"][0], $this->ldapcharset));
  1226. // FQDN domain
  1227. $domain = str_replace('dc=', '', $this->domain);
  1228. $domain = str_replace(',', '.', $domain);
  1229. $this->domainFQDN = $domain;
  1230. // Set ldapUserDn (each user can have a different dn)
  1231. //var_dump($result[0]);exit;
  1232. $this->ldapUserDN = $result[0]['dn'];
  1233. ldap_free_result($this->result);
  1234. return 1;
  1235. }
  1236. }
  1237. // helper methods
  1238. /**
  1239. * Returns the correct user identifier to use, based on the ldap server type
  1240. *
  1241. * @return string Login
  1242. */
  1243. public function getUserIdentifier()
  1244. {
  1245. if ($this->serverType == "activedirectory") {
  1246. return $this->attr_sambalogin;
  1247. } else {
  1248. return $this->attr_login;
  1249. }
  1250. }
  1251. /**
  1252. * UserAccountControl Flgs to more human understandable form...
  1253. *
  1254. * @param string $uacf UACF
  1255. * @return array
  1256. */
  1257. public function parseUACF($uacf)
  1258. {
  1259. //All flags array
  1260. $flags = array(
  1261. "TRUSTED_TO_AUTH_FOR_DELEGATION" => 16777216,
  1262. "PASSWORD_EXPIRED" => 8388608,
  1263. "DONT_REQ_PREAUTH" => 4194304,
  1264. "USE_DES_KEY_ONLY" => 2097152,
  1265. "NOT_DELEGATED" => 1048576,
  1266. "TRUSTED_FOR_DELEGATION" => 524288,
  1267. "SMARTCARD_REQUIRED" => 262144,
  1268. "MNS_LOGON_ACCOUNT" => 131072,
  1269. "DONT_EXPIRE_PASSWORD" => 65536,
  1270. "SERVER_TRUST_ACCOUNT" => 8192,
  1271. "WORKSTATION_TRUST_ACCOUNT" => 4096,
  1272. "INTERDOMAIN_TRUST_ACCOUNT" => 2048,
  1273. "NORMAL_ACCOUNT" => 512,
  1274. "TEMP_DUPLICATE_ACCOUNT" => 256,
  1275. "ENCRYPTED_TEXT_PWD_ALLOWED" => 128,
  1276. "PASSWD_CANT_CHANGE" => 64,
  1277. "PASSWD_NOTREQD" => 32,
  1278. "LOCKOUT" => 16,
  1279. "HOMEDIR_REQUIRED" => 8,
  1280. "ACCOUNTDISABLE" => 2,
  1281. "SCRIPT" => 1
  1282. );
  1283. //Parse flags to text
  1284. $retval = array();
  1285. //while (list($flag, $val) = each($flags)) {
  1286. foreach ($flags as $flag => $val) {
  1287. if ($uacf >= $val) {
  1288. $uacf -= $val;
  1289. $retval[$val] = $flag;
  1290. }
  1291. }
  1292. //Return human friendly flags
  1293. return $retval;
  1294. }
  1295. /**
  1296. * SamAccountType value to text
  1297. *
  1298. * @param string $samtype SamType
  1299. * @return string Sam string
  1300. */
  1301. public function parseSAT($samtype)
  1302. {
  1303. $stypes = array(
  1304. 805306368 => "NORMAL_ACCOUNT",
  1305. 805306369 => "WORKSTATION_TRUST",
  1306. 805306370 => "INTERDOMAIN_TRUST",
  1307. 268435456 => "SECURITY_GLOBAL_GROUP",
  1308. 268435457 => "DISTRIBUTION_GROUP",
  1309. 536870912 => "SECURITY_LOCAL_GROUP",
  1310. 536870913 => "DISTRIBUTION_LOCAL_GROUP"
  1311. );
  1312. $retval = "";
  1313. foreach ($stypes as $sat => $val) {
  1314. if ($samtype == $sat) {
  1315. $retval = $val;
  1316. break;
  1317. }
  1318. }
  1319. if (empty($retval)) {
  1320. $retval = "UNKNOWN_TYPE_".$samtype;
  1321. }
  1322. return $retval;
  1323. }
  1324. // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
  1325. /**
  1326. * Convertit le temps ActiveDirectory en Unix timestamp
  1327. *
  1328. * @param string $value AD time to convert
  1329. * @return integer Unix timestamp
  1330. */
  1331. public function convert_time($value)
  1332. {
  1333. // phpcs:enable
  1334. $dateLargeInt = $value; // nano secondes depuis 1601 !!!!
  1335. $secsAfterADEpoch = $dateLargeInt / (10000000); // secondes depuis le 1 jan 1601
  1336. $ADToUnixConvertor = ((1970 - 1601) * 365.242190) * 86400; // UNIX start date - AD start date * jours * secondes
  1337. $unixTimeStamp = intval($secsAfterADEpoch - $ADToUnixConvertor); // Unix time stamp
  1338. return $unixTimeStamp;
  1339. }
  1340. /**
  1341. * Convert a string into output/memory charset
  1342. *
  1343. * @param string $str String to convert
  1344. * @param string $pagecodefrom Page code of src string
  1345. * @return string Converted string
  1346. */
  1347. private function convToOutputCharset($str, $pagecodefrom = 'UTF-8')
  1348. {
  1349. global $conf;
  1350. if ($pagecodefrom == 'ISO-8859-1' && $conf->file->character_set_client == 'UTF-8') {
  1351. $str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
  1352. }
  1353. if ($pagecodefrom == 'UTF-8' && $conf->file->character_set_client == 'ISO-8859-1') {
  1354. $str = mb_convert_encoding($str, 'ISO-8859-1');
  1355. }
  1356. return $str;
  1357. }
  1358. /**
  1359. * Convert a string from output/memory charset
  1360. *
  1361. * @param string $str String to convert
  1362. * @param string $pagecodeto Page code for result string
  1363. * @return string Converted string
  1364. */
  1365. public function convFromOutputCharset($str, $pagecodeto = 'UTF-8')
  1366. {
  1367. global $conf;
  1368. if ($pagecodeto == 'ISO-8859-1' && $conf->file->character_set_client == 'UTF-8') {
  1369. $str = mb_convert_encoding($str, 'ISO-8859-1');
  1370. }
  1371. if ($pagecodeto == 'UTF-8' && $conf->file->character_set_client == 'ISO-8859-1') {
  1372. $str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
  1373. }
  1374. return $str;
  1375. }
  1376. /**
  1377. * Return available value of group GID
  1378. *
  1379. * @param string $keygroup Key of group
  1380. * @return int gid number
  1381. */
  1382. public function getNextGroupGid($keygroup = 'LDAP_KEY_GROUPS')
  1383. {
  1384. global $conf;
  1385. if (empty($keygroup)) {
  1386. $keygroup = 'LDAP_KEY_GROUPS';
  1387. }
  1388. $search = '(' . getDolGlobalString($keygroup).'=*)';
  1389. $result = $this->search($this->groups, $search);
  1390. if ($result) {
  1391. $c = $result['count'];
  1392. $gids = array();
  1393. for ($i = 0; $i < $c; $i++) {
  1394. $gids[] = $result[$i]['gidnumber'][0];
  1395. }
  1396. rsort($gids);
  1397. return $gids[0] + 1;
  1398. }
  1399. return 0;
  1400. }
  1401. }