Passed
Push — EXTRACT_CLASSES ( 0382f2...c25e41 )
by Rafael
52:18
created

Ldap::connectBind()   F

Complexity

Conditions 31
Paths 2392

Size

Total Lines 140
Code Lines 89

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 31
eloc 89
nc 2392
nop 0
dl 0
loc 140
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* Copyright (C) 2004       Rodolphe Quiedeville        <[email protected]>
4
 * Copyright (C) 2004		Benoit Mortier              <[email protected]>
5
 * Copyright (C) 2005-2021	Regis Houssin               <[email protected]>
6
 * Copyright (C) 2006-2021	Laurent Destailleur         <[email protected]>
7
 * Copyright (C) 2024		William Mead		        <[email protected]>
8
 * Copyright (C) 2024       Rafael San José             <[email protected]>
9
 *
10
 * This program is free software; you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation; either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 * or see https://www.gnu.org/
23
 */
24
25
namespace Dolibarr\Code\Core\Classes;
26
27
/**
28
 *  \file       htdocs/core/class/ldap.class.php
29
 *  \brief      File of class to manage LDAP features
30
 *
31
 *  Note:
32
 *  LDAP_ESCAPE_FILTER is to escape char  array('\\', '*', '(', ')', "\x00")
33
 *  LDAP_ESCAPE_DN is to escape char  array('\\', ',', '=', '+', '<', '>', ';', '"', '#')
34
 *  @phan-file-suppress PhanTypeMismatchArgumentInternal (notifications concern 'resource)
35
 */
36
37
/**
38
 *  Class to manage LDAP features
39
 */
40
class Ldap
41
{
42
    /**
43
     * @var string Error code (or message)
44
     */
45
    public $error = '';
46
47
    /**
48
     * @var string[]    Array of error strings
49
     */
50
    public $errors = array();
51
52
    /**
53
     * @var array Servers (IP addresses or hostnames)
54
     */
55
    public $server = array();
56
57
    /**
58
     * @var string Current connected server
59
     */
60
    public $connectedServer;
61
62
    /**
63
     * @var int server port
64
     */
65
    public $serverPort;
66
67
    /**
68
     * @var string Base DN (e.g. "dc=foo,dc=com")
69
     */
70
71
    public $dn;
72
    /**
73
     * @var string Server type: OpenLDAP or Active Directory
74
     */
75
76
    public $serverType;
77
    /**
78
     * @var string LDAP protocol version
79
     */
80
81
    public $ldapProtocolVersion;
82
    /**
83
     * @var string Server DN
84
     */
85
86
    public $domain;
87
88
    /**
89
     * @var string Server FQDN
90
     */
91
    public $domainFQDN;
92
93
    /**
94
     * @var bool LDAP bind
95
     */
96
    public $bind;
97
98
    /**
99
     * @var string LDAP administrator user
100
     * Active Directory does not allow anonymous connections
101
     */
102
    public $searchUser;
103
    /**
104
     * @var string LDAP administrator password
105
     * Active Directory does not allow anonymous connections
106
     */
107
    public $searchPassword;
108
109
    /**
110
     * @var string Users DN
111
     */
112
    public $people;
113
114
    /**
115
     * @var string Groups DN
116
     */
117
    public $groups;
118
119
    /**
120
     * @var int|null Error code provided by the LDAP server
121
     */
122
    public $ldapErrorCode;
123
124
    /**
125
     * @var string|null Error text message
126
     */
127
    public $ldapErrorText;
128
129
    /**
130
     * @var string
131
     */
132
    public $filter;
133
134
    /**
135
     * @var string
136
     */
137
    public $filtergroup;
138
139
    /**
140
     * @var string
141
     */
142
    public $filtermember;
143
144
    /**
145
     * @var string attr_login
146
     */
147
    public $attr_login;
148
149
    /**
150
     * @var string attr_sambalogin
151
     */
152
    public $attr_sambalogin;
153
154
    /**
155
     * @var string attr_name
156
     */
157
    public $attr_name;
158
159
    /**
160
     * @var string attr_firstname
161
     */
162
    public $attr_firstname;
163
164
    /**
165
     * @var string attr_mail
166
     */
167
    public $attr_mail;
168
169
    /**
170
     * @var string attr_phone
171
     */
172
    public $attr_phone;
173
174
    /**
175
     * @var string attr_fax
176
     */
177
    public $attr_fax;
178
179
    /**
180
     * @var string attr_mobile
181
     */
182
    public $attr_mobile;
183
184
    /**
185
     * @var int badpwdtime
186
     */
187
    public $badpwdtime;
188
189
    /**
190
     * @var string LDAP user DN
191
     */
192
    public $ldapUserDN;
193
194
    /**
195
     * @var string Fetched username
196
     */
197
    public $name;
198
199
    /**
200
     * @var string Fetched user first name
201
     */
202
    public $firstname;
203
204
    /**
205
     * @var string Fetched user login
206
     */
207
    public $login;
208
209
    /**
210
     * @var string Fetched user phone number
211
     */
212
    public $phone;
213
214
    /**
215
     * @var string Fetched user fax number
216
     */
217
    public $fax;
218
219
    /**
220
     * @var string Fetched user email
221
     */
222
    public $mail;
223
224
    /**
225
     * @var string Fetched user mobile number
226
     */
227
    public $mobile;
228
229
    /**
230
     * @var array UserAccountControl Flags
231
     */
232
    public $uacf;
233
234
    /**
235
     * @var int Password last set time
236
     */
237
    public $pwdlastset;
238
239
    /**
240
     * @var string LDAP charset.
241
     * LDAP should be UTF-8 encoded
242
     */
243
    public $ldapcharset = 'UTF-8';
244
245
    /**
246
     * @var bool|resource The internal LDAP connection handle
247
     */
248
    public $connection;
249
250
    /**
251
     * @var bool|resource Result of any connections or search.
252
     */
253
    public $result;
254
255
    /**
256
     * @var int No LDAP synchronization
257
     */
258
    const SYNCHRO_NONE = 0;
259
260
    /**
261
     * @var int Dolibarr to LDAP synchronization
262
     */
263
    const SYNCHRO_DOLIBARR_TO_LDAP = 1;
264
265
    /**
266
     * @var int LDAP to Dolibarr synchronization
267
     */
268
    const SYNCHRO_LDAP_TO_DOLIBARR = 2;
269
270
    /**
271
     *  Constructor
272
     */
273
    public function __construct()
274
    {
275
276
        // Server
277
        if (getDolGlobalString('LDAP_SERVER_HOST')) {
278
            $this->server[] = getDolGlobalString('LDAP_SERVER_HOST');
279
        }
280
        if (getDolGlobalString('LDAP_SERVER_HOST_SLAVE')) {
281
            $this->server[] = getDolGlobalString('LDAP_SERVER_HOST_SLAVE');
282
        }
283
        $this->serverPort          = getDolGlobalInt('LDAP_SERVER_PORT', 389);
284
        $this->ldapProtocolVersion = getDolGlobalString('LDAP_SERVER_PROTOCOLVERSION');
285
        $this->dn                  = getDolGlobalString('LDAP_SERVER_DN');
286
        $this->serverType          = getDolGlobalString('LDAP_SERVER_TYPE');
287
288
        $this->domain              = getDolGlobalString('LDAP_SERVER_DN');
289
        $this->searchUser          = getDolGlobalString('LDAP_ADMIN_DN');
290
        $this->searchPassword      = getDolGlobalString('LDAP_ADMIN_PASS');
291
        $this->people              = getDolGlobalString('LDAP_USER_DN');
292
        $this->groups              = getDolGlobalString('LDAP_GROUP_DN');
293
294
        $this->filter              = getDolGlobalString('LDAP_FILTER_CONNECTION'); // Filter on user
295
        $this->filtergroup         = getDolGlobalString('LDAP_GROUP_FILTER'); // Filter on groups
296
        $this->filtermember        = getDolGlobalString('LDAP_MEMBER_FILTER'); // Filter on member
297
298
        // Users
299
        $this->attr_login      = getDolGlobalString('LDAP_FIELD_LOGIN'); //unix
300
        $this->attr_sambalogin = getDolGlobalString('LDAP_FIELD_LOGIN_SAMBA'); //samba, activedirectory
301
        $this->attr_name       = getDolGlobalString('LDAP_FIELD_NAME');
302
        $this->attr_firstname  = getDolGlobalString('LDAP_FIELD_FIRSTNAME');
303
        $this->attr_mail       = getDolGlobalString('LDAP_FIELD_MAIL');
304
        $this->attr_phone      = getDolGlobalString('LDAP_FIELD_PHONE');
305
        $this->attr_fax        = getDolGlobalString('LDAP_FIELD_FAX');
306
        $this->attr_mobile     = getDolGlobalString('LDAP_FIELD_MOBILE');
307
    }
308
309
    // Connection handling methods -------------------------------------------
310
311
    /**
312
     * Connect and bind
313
     * Use this->server, this->serverPort, this->ldapProtocolVersion, this->serverType, this->searchUser, this->searchPassword
314
     * After return, this->connection and $this->bind are defined
315
     *
316
     * @see connect_bind renamed
317
     * @return      int     if KO: <0 || if bind anonymous: 1 || if bind auth: 2
318
     */
319
    public function connectBind()
320
    {
321
        global $dolibarr_main_auth_ldap_debug;
322
323
        $connected = 0;
324
        $this->bind = false;
325
        $this->error = '';
326
        $this->connectedServer = '';
327
328
        $ldapdebug = ((empty($dolibarr_main_auth_ldap_debug) || $dolibarr_main_auth_ldap_debug == "false") ? false : true);
329
330
        if ($ldapdebug) {
331
            dol_syslog(get_class($this) . "::connectBind");
332
            print "DEBUG: connectBind<br>\n";
333
        }
334
335
        // Check parameters
336
        if (count($this->server) == 0 || empty($this->server[0])) {
337
            $this->error = 'LDAP setup (file conf.php) is not complete';
338
            dol_syslog(get_class($this) . "::connectBind " . $this->error, LOG_WARNING);
339
            return -1;
340
        }
341
342
        if (!function_exists("ldap_connect")) {
343
            $this->error = 'LDAPFunctionsNotAvailableOnPHP';
344
            dol_syslog(get_class($this) . "::connectBind " . $this->error, LOG_WARNING);
345
            return -1;
346
        }
347
348
        if (empty($this->error)) {
349
            // Loop on each ldap server
350
            foreach ($this->server as $host) {
351
                if ($connected) {
352
                    break;
353
                }
354
                if (empty($host)) {
355
                    continue;
356
                }
357
358
                if ($this->serverPing($host, $this->serverPort)) {
359
                    if ($ldapdebug) {
360
                        dol_syslog(get_class($this) . "::connectBind serverPing true, we try ldap_connect to " . $host, LOG_DEBUG);
361
                    }
362
                    $this->connection = ldap_connect($host, $this->serverPort);
363
                } else {
364
                    if (preg_match('/^ldaps/i', $host)) {
365
                        // With host = ldaps://server, the serverPing to ssl://server sometimes fails, even if the ldap_connect succeed, so
366
                        // we test this case and continue in such a case even if serverPing fails.
367
                        if ($ldapdebug) {
368
                            dol_syslog(get_class($this) . "::connectBind serverPing false, we try ldap_connect to " . $host, LOG_DEBUG);
369
                        }
370
                        $this->connection = ldap_connect($host, $this->serverPort);
371
                    } else {
372
                        if ($ldapdebug) {
373
                            dol_syslog(get_class($this) . "::connectBind serverPing false, no ldap_connect " . $host, LOG_DEBUG);
374
                        }
375
                        continue;
376
                    }
377
                }
378
379
                if (is_resource($this->connection) || is_object($this->connection)) {
380
                    if ($ldapdebug) {
381
                        dol_syslog(get_class($this) . "::connectBind this->connection is ok", LOG_DEBUG);
382
                    }
383
384
                    // Upgrade connection to TLS, if requested by the configuration
385
                    if (getDolGlobalString('LDAP_SERVER_USE_TLS')) {
386
                        // For test/debug
387
                        //ldap_set_option($this->connection, LDAP_OPT_DEBUG_LEVEL, 7);
388
                        //ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, 3);
389
                        //ldap_set_option($this->connection, LDAP_OPT_REFERRALS, 0);
390
391
                        $resulttls = ldap_start_tls($this->connection);
392
                        if (!$resulttls) {
393
                            dol_syslog(get_class($this) . "::connectBind failed to start tls", LOG_WARNING);
394
                            $this->error = 'ldap_start_tls Failed to start TLS ' . ldap_errno($this->connection) . ' ' . ldap_error($this->connection);
395
                            $connected = 0;
396
                            $this->unbind();
397
                        }
398
                    }
399
400
                    // Execute the ldap_set_option here (after connect and before bind)
401
                    $this->setVersion();
402
                    $this->setSizeLimit();
403
404
                    if ($this->serverType == "activedirectory") {
405
                        $result = $this->setReferrals();
406
                        dol_syslog(get_class($this) . "::connectBind try bindauth for activedirectory on " . $host . " user=" . $this->searchUser . " password=" . preg_replace('/./', '*', $this->searchPassword), LOG_DEBUG);
407
                        $this->result = $this->bindauth($this->searchUser, $this->searchPassword);
408
                        if ($this->result) {
409
                            $this->bind = $this->result;
410
                            $connected = 2;
411
                            $this->connectedServer = $host;
412
                            break;
413
                        } else {
414
                            $this->error = ldap_errno($this->connection) . ' ' . ldap_error($this->connection);
415
                        }
416
                    } else {
417
                        // Try in auth mode
418
                        if ($this->searchUser && $this->searchPassword) {
419
                            dol_syslog(get_class($this) . "::connectBind try bindauth on " . $host . " user=" . $this->searchUser . " password=" . preg_replace('/./', '*', $this->searchPassword), LOG_DEBUG);
420
                            $this->result = $this->bindauth($this->searchUser, $this->searchPassword);
421
                            if ($this->result) {
422
                                $this->bind = $this->result;
423
                                $connected = 2;
424
                                $this->connectedServer = $host;
425
                                break;
426
                            } else {
427
                                $this->error = ldap_errno($this->connection) . ' ' . ldap_error($this->connection);
428
                            }
429
                        }
430
                        // Try in anonymous
431
                        if (!$this->bind) {
432
                            dol_syslog(get_class($this) . "::connectBind try bind anonymously on " . $host, LOG_DEBUG);
433
                            $result = $this->bind();
434
                            if ($result) {
435
                                $this->bind = $this->result;
436
                                $connected = 1;
437
                                $this->connectedServer = $host;
438
                                break;
439
                            } else {
440
                                $this->error = ldap_errno($this->connection) . ' ' . ldap_error($this->connection);
441
                            }
442
                        }
443
                    }
444
                }
445
446
                if (!$connected) {
447
                    $this->unbind();
448
                }
449
            }   // End loop on each server
450
        }
451
452
        if ($connected) {
453
            dol_syslog(get_class($this) . "::connectBind " . $connected, LOG_DEBUG);
454
            return $connected;
455
        } else {
456
            $this->error = 'Failed to connect to LDAP' . ($this->error ? ': ' . $this->error : '');
457
            dol_syslog(get_class($this) . "::connectBind " . $this->error, LOG_WARNING);
458
            return -1;
459
        }
460
    }
461
462
    /**
463
     * Simply closes the connection set up earlier. Returns true if OK, false if there was an error.
464
     * This method seems a duplicate/alias of unbind().
465
     *
466
     * @return  boolean         true or false
467
     * @deprecated ldap_close is an alias of ldap_unbind, so use unbind() instead.
468
     * @see unbind()
469
     */
470
    public function close()
471
    {
472
        return $this->unbind();
473
    }
474
475
    /**
476
     * Anonymously binds to the connection. After this is done,
477
     * queries and searches can be done - but read-only.
478
     *
479
     * @return  boolean         true or false
480
     */
481
    public function bind()
482
    {
483
        if (!$this->result = @ldap_bind($this->connection)) {
484
            $this->ldapErrorCode = ldap_errno($this->connection);
485
            $this->ldapErrorText = ldap_error($this->connection);
486
            $this->error = $this->ldapErrorCode . " " . $this->ldapErrorText;
487
            return false;
488
        } else {
489
            return true;
490
        }
491
    }
492
493
    /**
494
     * Binds as an authenticated user, which usually allows for write
495
     * access. The FULL dn must be passed. For a directory manager, this is
496
     * "cn=Directory Manager" under iPlanet. For a user, it will be something
497
     * like "uid=jbloggs,ou=People,dc=foo,dc=com".
498
     *
499
     * @param   string  $bindDn         DN
500
     * @param   string  $pass           Password
501
     * @return  boolean                 true or false
502
     */
503
    public function bindauth($bindDn, $pass)
504
    {
505
        if (!$this->result = @ldap_bind($this->connection, $bindDn, $pass)) {
506
            $this->ldapErrorCode = ldap_errno($this->connection);
507
            $this->ldapErrorText = ldap_error($this->connection);
508
            $this->error = $this->ldapErrorCode . " " . $this->ldapErrorText;
509
            return false;
510
        } else {
511
            return true;
512
        }
513
    }
514
515
    /**
516
     * Unbind of LDAP server (close connection).
517
     *
518
     * @return  boolean                 true or false
519
     * @see close()
520
     */
521
    public function unbind()
522
    {
523
        $this->result = true;
524
        if (is_resource($this->connection) || is_object($this->connection)) {
525
            $this->result = @ldap_unbind($this->connection);
526
        }
527
        if ($this->result) {
528
            return true;
529
        } else {
530
            return false;
531
        }
532
    }
533
534
535
    /**
536
     * Verify LDAP server version
537
     *
538
     * @return  int     version
539
     */
540
    public function getVersion()
541
    {
542
        @ldap_get_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $version);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ldap_get_option(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

542
        /** @scrutinizer ignore-unhandled */ @ldap_get_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $version);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
543
        return $version;
544
    }
545
546
    /**
547
     * Set LDAP protocol version.
548
     * LDAP_OPT_PROTOCOL_VERSION is a constant equal to 3
549
     *
550
     * @return  boolean     if set LDAP option OK: true, if KO: false
551
     */
552
    public function setVersion()
553
    {
554
        return ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->ldapProtocolVersion);
555
    }
556
557
    /**
558
     * Set LDAP size limit.
559
     *
560
     * @return  boolean     if set LDAP option OK: true, if KO: false
561
     */
562
    public function setSizeLimit()
563
    {
564
        return ldap_set_option($this->connection, LDAP_OPT_SIZELIMIT, 0);
565
    }
566
567
    /**
568
     * Set LDAP referrals.
569
     * LDAP_OPT_REFERRALS is a constant equal to ?
570
     *
571
     * @return  boolean     if set LDAP option OK: true, if KO: false
572
     */
573
    public function setReferrals()
574
    {
575
        return ldap_set_option($this->connection, LDAP_OPT_REFERRALS, 0);
576
    }
577
578
579
    /**
580
     *  Add an LDAP entry
581
     *  LDAP object connect and bind must have been done
582
     *
583
     *  @param  string  $dn         DN entry key
584
     *  @param  array   $info       Attributes array
585
     *  @param  User    $user       Object user that create
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Core\Classes\User was not found. Did you mean User? If so, make sure to prefix the type with \.
Loading history...
586
     *  @return int                 if KO: <0 || if OK: >0
587
     */
588
    public function add($dn, $info, $user)
589
    {
590
        dol_syslog(get_class($this) . "::add dn=" . $dn . " info=" . print_r($info, true));
591
592
        // Check parameters
593
        if (!$this->connection) {
594
            $this->error = "NotConnected";
595
            return -2;
596
        }
597
        if (!$this->bind) {
598
            $this->error = "NotConnected";
599
            return -3;
600
        }
601
602
        // Encode to LDAP page code
603
        $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
604
        foreach ($info as $key => $val) {
605
            if (!is_array($val)) {
606
                $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
607
            }
608
        }
609
610
        $this->dump($dn, $info);
611
612
        //print_r($info);
613
        $result = @ldap_add($this->connection, $dn, $info);
614
615
        if ($result) {
616
            dol_syslog(get_class($this) . "::add successful", LOG_DEBUG);
617
            return 1;
618
        } else {
619
            $this->ldapErrorCode = @ldap_errno($this->connection);
620
            $this->ldapErrorText = @ldap_error($this->connection);
621
            $this->error = $this->ldapErrorCode . " " . $this->ldapErrorText;
622
            dol_syslog(get_class($this) . "::add failed: " . $this->error, LOG_ERR);
623
            return -1;
624
        }
625
    }
626
627
    /**
628
     *  Modify an LDAP entry
629
     *  LDAP object connect and bind must have been done
630
     *
631
     *  @param  string      $dn         DN entry key
632
     *  @param  array       $info       Attributes array
633
     *  @param  User        $user       Object user that modify
634
     *  @return int                     if KO: <0 || if OK: >0
635
     */
636
    public function modify($dn, $info, $user)
637
    {
638
        dol_syslog(get_class($this) . "::modify dn=" . $dn . " info=" . print_r($info, true));
639
640
        // Check parameters
641
        if (!$this->connection) {
642
            $this->error = "NotConnected";
643
            return -2;
644
        }
645
        if (!$this->bind) {
646
            $this->error = "NotConnected";
647
            return -3;
648
        }
649
650
        // Encode to LDAP page code
651
        $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
652
        foreach ($info as $key => $val) {
653
            if (!is_array($val)) {
654
                $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
655
            }
656
        }
657
658
        $this->dump($dn, $info);
659
660
        //print_r($info);
661
662
        // For better compatibility with Samba4 AD
663
        if ($this->serverType == "activedirectory") {
664
            unset($info['cn']); // To avoid error : Operation not allowed on RDN (Code 67)
665
666
            // To avoid error : LDAP Error: 53 (Unwilling to perform)
667
            if (isset($info['unicodePwd'])) {
668
                $info['unicodePwd'] = mb_convert_encoding("\"" . $info['unicodePwd'] . "\"", "UTF-16LE", "UTF-8");
669
            }
670
        }
671
        $result = @ldap_mod_replace($this->connection, $dn, $info);
672
673
        if ($result) {
674
            dol_syslog(get_class($this) . "::modify successful", LOG_DEBUG);
675
            return 1;
676
        } else {
677
            $this->error = @ldap_error($this->connection);
678
            dol_syslog(get_class($this) . "::modify failed: " . $this->error, LOG_ERR);
679
            return -1;
680
        }
681
    }
682
683
    /**
684
     *  Rename an LDAP entry
685
     *  LDAP object connect and bind must have been done
686
     *
687
     *  @param  string      $dn             Old DN entry key (uid=qqq,ou=xxx,dc=aaa,dc=bbb) (before update)
688
     *  @param  string      $newrdn         New RDN entry key (uid=qqq)
689
     *  @param  string      $newparent      New parent (ou=xxx,dc=aaa,dc=bbb)
690
     *  @param  User        $user           Object user that modify
691
     *  @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.
692
     *  @return int                         if KO: <0 || if OK: >0
693
     */
694
    public function rename($dn, $newrdn, $newparent, $user, $deleteoldrdn = true)
695
    {
696
        dol_syslog(get_class($this) . "::modify dn=" . $dn . " newrdn=" . $newrdn . " newparent=" . $newparent . " deleteoldrdn=" . ($deleteoldrdn ? 1 : 0));
697
698
        // Check parameters
699
        if (!$this->connection) {
700
            $this->error = "NotConnected";
701
            return -2;
702
        }
703
        if (!$this->bind) {
704
            $this->error = "NotConnected";
705
            return -3;
706
        }
707
708
        // Encode to LDAP page code
709
        $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
710
        $newrdn = $this->convFromOutputCharset($newrdn, $this->ldapcharset);
711
        $newparent = $this->convFromOutputCharset($newparent, $this->ldapcharset);
712
713
        //print_r($info);
714
        $result = @ldap_rename($this->connection, $dn, $newrdn, $newparent, $deleteoldrdn);
715
716
        if ($result) {
717
            dol_syslog(get_class($this) . "::rename successful", LOG_DEBUG);
718
            return 1;
719
        } else {
720
            $this->error = @ldap_error($this->connection);
721
            dol_syslog(get_class($this) . "::rename failed: " . $this->error, LOG_ERR);
722
            return -1;
723
        }
724
    }
725
726
    /**
727
     *  Modify an LDAP entry (to use if dn != olddn)
728
     *  LDAP object connect and bind must have been done
729
     *
730
     *  @param  string  $dn         DN entry key
731
     *  @param  array   $info       Attributes array
732
     *  @param  User    $user       Object user that update
733
     *  @param  string  $olddn      Old DN entry key (before update)
734
     *  @param  string  $newrdn     New RDN entry key (uid=qqq) (for ldap_rename)
735
     *  @param  string  $newparent  New parent (ou=xxx,dc=aaa,dc=bbb) (for ldap_rename)
736
     *  @return int                 if KO: <0 || if OK: >0
737
     */
738
    public function update($dn, $info, $user, $olddn, $newrdn = '', $newparent = '')
739
    {
740
        dol_syslog(get_class($this) . "::update dn=" . $dn . " olddn=" . $olddn);
741
742
        // Check parameters
743
        if (!$this->connection) {
744
            $this->error = "NotConnected";
745
            return -2;
746
        }
747
        if (!$this->bind) {
748
            $this->error = "NotConnected";
749
            return -3;
750
        }
751
752
        if (!$olddn || $olddn != $dn) {
753
            if (!empty($olddn) && !empty($newrdn) && !empty($newparent) && $this->ldapProtocolVersion === '3') {
754
                // This function currently only works with LDAPv3
755
                $result = $this->rename($olddn, $newrdn, $newparent, $user, true);
756
                $result = $this->modify($dn, $info, $user); // We force "modify" for avoid some fields not modify
757
            } else {
758
                // If change we make is rename the key of LDAP record, we create new one and if ok, we delete old one.
759
                $result = $this->add($dn, $info, $user);
760
                if ($result > 0 && $olddn && $olddn != $dn) {
761
                    $result = $this->delete($olddn); // If add fails, we do not try to delete old one
762
                }
763
            }
764
        } else {
765
            //$result = $this->delete($olddn);
766
            $result = $this->add($dn, $info, $user); // If record has been deleted from LDAP, we recreate it. We ignore error if it already exists.
767
            $result = $this->modify($dn, $info, $user); // We use add/modify instead of delete/add when olddn is received
768
        }
769
        if ($result <= 0) {
770
            $this->error = ldap_error($this->connection) . ' (Code ' . ldap_errno($this->connection) . ") " . $this->error;
771
            dol_syslog(get_class($this) . "::update " . $this->error, LOG_ERR);
772
            //print_r($info);
773
            return -1;
774
        } else {
775
            dol_syslog(get_class($this) . "::update done successfully");
776
            return 1;
777
        }
778
    }
779
780
781
    /**
782
     *  Delete an LDAP entry
783
     *  LDAP object connect and bind must have been done
784
     *
785
     *  @param  string  $dn         DN entry key
786
     *  @return int                 if KO: <0 || if OK: >0
787
     */
788
    public function delete($dn)
789
    {
790
        dol_syslog(get_class($this) . "::delete Delete LDAP entry dn=" . $dn);
791
792
        // Check parameters
793
        if (!$this->connection) {
794
            $this->error = "NotConnected";
795
            return -2;
796
        }
797
        if (!$this->bind) {
798
            $this->error = "NotConnected";
799
            return -3;
800
        }
801
802
        // Encode to LDAP page code
803
        $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
804
805
        $result = @ldap_delete($this->connection, $dn);
806
807
        if ($result) {
808
            return 1;
809
        }
810
        return -1;
811
    }
812
813
    /**
814
     * Build an LDAP message
815
     *
816
     * @see dump_content renamed
817
     * @param   string      $dn         DN entry key
818
     * @param   array       $info       Attributes array
819
     * @return  string                  Content of file
820
     */
821
    public function dumpContent($dn, $info)
822
    {
823
        $content = '';
824
825
        // Create file content
826
        if (preg_match('/^ldap/', $this->server[0])) {
827
            $target = "-H " . implode(',', $this->server);
828
        } else {
829
            $target = "-h " . implode(',', $this->server) . " -p " . $this->serverPort;
830
        }
831
        $content .= "# ldapadd $target -c -v -D " . $this->searchUser . " -W -f ldapinput.in\n";
832
        $content .= "# ldapmodify $target -c -v -D " . $this->searchUser . " -W -f ldapinput.in\n";
833
        $content .= "# ldapdelete $target -c -v -D " . $this->searchUser . " -W -f ldapinput.in\n";
834
        if (in_array('localhost', $this->server)) {
835
            $content .= "# If commands fails to connect, try without -h and -p\n";
836
        }
837
        $content .= "dn: " . $dn . "\n";
838
        foreach ($info as $key => $value) {
839
            if (!is_array($value)) {
840
                $content .= "$key: $value\n";
841
            } else {
842
                foreach ($value as $valuevalue) {
843
                    $content .= "$key: $valuevalue\n";
844
                }
845
            }
846
        }
847
        return $content;
848
    }
849
850
    /**
851
     *  Dump an LDAP message to ldapinput.in file
852
     *
853
     *  @param  string      $dn         DN entry key
854
     *  @param  array       $info       Attributes array
855
     *  @return int                     if KO: <0 || if OK: >0
856
     */
857
    public function dump($dn, $info)
858
    {
859
        global $conf;
860
        $ldapDirTemp = $conf->ldap->dir_temp;
861
        // Create content
862
        $content = $this->dumpContent($dn, $info);
863
864
        //Create directory & file
865
        $result = dol_mkdir($ldapDirTemp);
866
        if ($result != 0) {
867
            $outputfile = $ldapDirTemp . '/ldapinput.in';
868
            $fp = fopen($outputfile, "w");
869
            if ($fp) {
870
                fwrite($fp, $content);
871
                fclose($fp);
872
                dolChmod($outputfile);
873
                return 1;
874
            } else {
875
                return -1;
876
            }
877
        } else {
878
            return -1;
879
        }
880
    }
881
882
    /**
883
     * Ping a server before ldap_connect for avoid waiting
884
     *
885
     * @param   string  $host       Server host or address
886
     * @param   int     $port       Server port (default 389)
887
     * @param   int     $timeout    Timeout in second (default 1s)
888
     * @return  boolean             true or false
889
     */
890
    public function serverPing($host, $port = 389, $timeout = 1)
891
    {
892
        $regs = array();
893
        if (preg_match('/^ldaps:\/\/([^\/]+)\/?$/', $host, $regs)) {
894
            // Replace ldaps:// by ssl://
895
            $host = 'ssl://' . $regs[1];
896
        } elseif (preg_match('/^ldap:\/\/([^\/]+)\/?$/', $host, $regs)) {
897
            // Remove ldap://
898
            $host = $regs[1];
899
        }
900
901
        //var_dump($newhostforstream); var_dump($host); var_dump($port);
902
        //$host = 'ssl://ldap.test.local:636';
903
        //$port = 636;
904
905
        $errno = $errstr = 0;
906
        /*
907
        if ($methodtochecktcpconnect == 'socket') {
908
            Try to use socket_create() method.
909
            Method that use stream_context_create() works only on registered listed in stream stream_get_wrappers(): http, https, ftp, ...
910
        }
911
        */
912
913
        // Use the method fsockopen to test tcp connect. No way to ignore ssl certificate errors with this method !
914
        $op = @fsockopen($host, $port, $errno, $errstr, $timeout);
915
916
        //var_dump($op);
917
        if (!$op) {
918
            return false; //DC is N/A
919
        } else {
920
            fclose($op); //explicitly close open socket connection
921
            return true; //DC is up & running, we can safely connect with ldap_connect
922
        }
923
    }
924
925
926
    // Attribute methods -----------------------------------------------------
927
928
    /**
929
     *  Add an LDAP attribute in entry
930
     *  LDAP object connect and bind must have been done
931
     *
932
     *  @param  string      $dn         DN entry key
933
     *  @param  array       $info       Attributes array
934
     *  @param  User        $user       Object user that create
935
     *  @return int                     if KO: <0 || if OK: >0
936
     */
937
    public function addAttribute($dn, $info, $user)
938
    {
939
        dol_syslog(get_class($this) . "::addAttribute dn=" . $dn . " info=" . implode(',', $info));
940
941
        // Check parameters
942
        if (!$this->connection) {
943
            $this->error = "NotConnected";
944
            return -2;
945
        }
946
        if (!$this->bind) {
947
            $this->error = "NotConnected";
948
            return -3;
949
        }
950
951
        // Encode to LDAP page code
952
        $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
953
        foreach ($info as $key => $val) {
954
            if (!is_array($val)) {
955
                $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
956
            }
957
        }
958
959
        $this->dump($dn, $info);
960
961
        //print_r($info);
962
        $result = @ldap_mod_add($this->connection, $dn, $info);
963
964
        if ($result) {
965
            dol_syslog(get_class($this) . "::add_attribute successful", LOG_DEBUG);
966
            return 1;
967
        } else {
968
            $this->error = @ldap_error($this->connection);
969
            dol_syslog(get_class($this) . "::add_attribute failed: " . $this->error, LOG_ERR);
970
            return -1;
971
        }
972
    }
973
974
    /**
975
     *  Update an LDAP attribute in entry
976
     *  LDAP object connect and bind must have been done
977
     *
978
     *  @param  string      $dn         DN entry key
979
     *  @param  array       $info       Attributes array
980
     *  @param  User        $user       Object user that create
981
     *  @return int                     if KO: <0 || if OK: >0
982
     */
983
    public function updateAttribute($dn, $info, $user)
984
    {
985
        dol_syslog(get_class($this) . "::updateAttribute dn=" . $dn . " info=" . implode(',', $info));
986
987
        // Check parameters
988
        if (!$this->connection) {
989
            $this->error = "NotConnected";
990
            return -2;
991
        }
992
        if (!$this->bind) {
993
            $this->error = "NotConnected";
994
            return -3;
995
        }
996
997
        // Encode to LDAP page code
998
        $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
999
        foreach ($info as $key => $val) {
1000
            if (!is_array($val)) {
1001
                $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
1002
            }
1003
        }
1004
1005
        $this->dump($dn, $info);
1006
1007
        //print_r($info);
1008
        $result = @ldap_mod_replace($this->connection, $dn, $info);
1009
1010
        if ($result) {
1011
            dol_syslog(get_class($this) . "::updateAttribute successful", LOG_DEBUG);
1012
            return 1;
1013
        } else {
1014
            $this->error = @ldap_error($this->connection);
1015
            dol_syslog(get_class($this) . "::updateAttribute failed: " . $this->error, LOG_ERR);
1016
            return -1;
1017
        }
1018
    }
1019
1020
    /**
1021
     *  Delete an LDAP attribute in entry
1022
     *  LDAP object connect and bind must have been done
1023
     *
1024
     *  @param  string      $dn         DN entry key
1025
     *  @param  array       $info       Attributes array
1026
     *  @param  User        $user       Object user that create
1027
     *  @return int                     if KO: <0 || if OK: >0
1028
     */
1029
    public function deleteAttribute($dn, $info, $user)
1030
    {
1031
        dol_syslog(get_class($this) . "::deleteAttribute dn=" . $dn . " info=" . implode(',', $info));
1032
1033
        // Check parameters
1034
        if (!$this->connection) {
1035
            $this->error = "NotConnected";
1036
            return -2;
1037
        }
1038
        if (!$this->bind) {
1039
            $this->error = "NotConnected";
1040
            return -3;
1041
        }
1042
1043
        // Encode to LDAP page code
1044
        $dn = $this->convFromOutputCharset($dn, $this->ldapcharset);
1045
        foreach ($info as $key => $val) {
1046
            if (!is_array($val)) {
1047
                $info[$key] = $this->convFromOutputCharset($val, $this->ldapcharset);
1048
            }
1049
        }
1050
1051
        $this->dump($dn, $info);
1052
1053
        //print_r($info);
1054
        $result = @ldap_mod_del($this->connection, $dn, $info);
1055
1056
        if ($result) {
1057
            dol_syslog(get_class($this) . "::deleteAttribute successful", LOG_DEBUG);
1058
            return 1;
1059
        } else {
1060
            $this->error = @ldap_error($this->connection);
1061
            dol_syslog(get_class($this) . "::deleteAttribute failed: " . $this->error, LOG_ERR);
1062
            return -1;
1063
        }
1064
    }
1065
1066
    /**
1067
     *  Returns an array containing attributes and values for first record
1068
     *
1069
     *  @param  string  $dn         DN entry key
1070
     *  @param  string  $filter     Filter
1071
     *  @return int|array           if KO: <=0 || if OK: array
1072
     */
1073
    public function getAttribute($dn, $filter)
1074
    {
1075
        // Check parameters
1076
        if (!$this->connection) {
1077
            $this->error = "NotConnected";
1078
            return -2;
1079
        }
1080
        if (!$this->bind) {
1081
            $this->error = "NotConnected";
1082
            return -3;
1083
        }
1084
1085
        $search = @ldap_search($this->connection, $dn, $filter);
1086
1087
        // Only one entry should ever be returned
1088
        $entry = @ldap_first_entry($this->connection, $search);
1089
1090
        if (!$entry) {
1091
            $this->ldapErrorCode = -1;
1092
            $this->ldapErrorText = "Couldn't find entry";
1093
            return 0; // Couldn't find entry...
1094
        }
1095
1096
        // Get values
1097
        if (!($values = ldap_get_attributes($this->connection, $entry))) {
1098
            $this->ldapErrorCode = ldap_errno($this->connection);
1099
            $this->ldapErrorText = ldap_error($this->connection);
1100
            return 0; // No matching attributes
1101
        }
1102
1103
        // Return an array containing the attributes.
1104
        return $values;
1105
    }
1106
1107
    /**
1108
     *  Returns an array containing values for an attribute and for first record matching filterrecord
1109
     *
1110
     *  @param  string          $filterrecord       Record
1111
     *  @param  string          $attribute          Attributes
1112
     *  @return array|boolean
1113
     */
1114
    public function getAttributeValues($filterrecord, $attribute)
1115
    {
1116
        $attributes = array();
1117
        $attributes[0] = $attribute;
1118
1119
        // We need to search for this user in order to get their entry.
1120
        $this->result = @ldap_search($this->connection, $this->people, $filterrecord, $attributes);
1121
1122
        // Pourquoi cette ligne ?
1123
        //$info = ldap_get_entries($this->connection, $this->result);
1124
1125
        // Only one entry should ever be returned (no user will have the same uid)
1126
        $entry = ldap_first_entry($this->connection, $this->result);
1127
1128
        if (!$entry) {
1129
            $this->ldapErrorCode = -1;
1130
            $this->ldapErrorText = "Couldn't find user";
1131
            return false; // Couldn't find the user...
1132
        }
1133
1134
        // Get values
1135
        if (!$values = @ldap_get_values_len($this->connection, $entry, $attribute)) {
1136
            $this->ldapErrorCode = ldap_errno($this->connection);
1137
            $this->ldapErrorText = ldap_error($this->connection);
1138
            return false; // No matching attributes
1139
        }
1140
1141
        // Return an array containing the attributes.
1142
        return $values;
1143
    }
1144
1145
    /**
1146
     *  Returns an array containing a details or list of LDAP record(s).
1147
     *  ldapsearch -LLLx -hlocalhost -Dcn=admin,dc=parinux,dc=org -w password -b "ou=adherents,ou=people,dc=parinux,dc=org" userPassword
1148
     *
1149
     *  @param  string  $search             Value of field to search, '*' for all. Not used if $activefilter is set.
1150
     *  @param  string  $userDn             DN (Ex: ou=adherents,ou=people,dc=parinux,dc=org)
1151
     *  @param  string  $useridentifier     Name of key field (Ex: uid).
1152
     *  @param  array   $attributeArray     Array of fields required. Note this array must also contain field $useridentifier (Ex: sn,userPassword)
1153
     *  @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
1154
     *  @param  array   $attributeAsArray   Array of fields wanted as an array not a string
1155
     *  @return array|int                   if KO: <0 || if OK: array of [id_record][ldap_field]=value
1156
     */
1157
    public function getRecords($search, $userDn, $useridentifier, $attributeArray, $activefilter = 0, $attributeAsArray = array())
1158
    {
1159
        $fulllist = array();
1160
1161
        dol_syslog(get_class($this) . "::getRecords search=" . $search . " userDn=" . $userDn . " useridentifier=" . $useridentifier . " attributeArray=array(" . implode(',', $attributeArray) . ") activefilter=" . $activefilter);
1162
1163
        // if the directory is AD, then bind first with the search user first
1164
        if ($this->serverType == "activedirectory") {
1165
            $this->bindauth($this->searchUser, $this->searchPassword);
1166
            dol_syslog(get_class($this) . "::bindauth serverType=activedirectory searchUser=" . $this->searchUser);
1167
        }
1168
1169
        // Define filter
1170
        if (!empty($activefilter)) {    // Use a predefined trusted filter (defined into setup by admin).
1171
            if (((string) $activefilter == '1' || (string) $activefilter == 'user') && $this->filter) {
1172
                $filter = '(' . $this->filter . ')';
1173
            } elseif (((string) $activefilter == 'group') && $this->filtergroup) {
1174
                $filter = '(' . $this->filtergroup . ')';
1175
            } elseif (((string) $activefilter == 'member') && $this->filter) {
1176
                $filter = '(' . $this->filtermember . ')';
1177
            } else {
1178
                // If this->filter/this->filtergroup is empty, make filter on * (all)
1179
                $filter = '(' . ldap_escape($useridentifier, '', LDAP_ESCAPE_FILTER) . '=*)';
1180
            }
1181
        } else {                        // Use a filter forged using the $search value
1182
            $filter = '(' . ldap_escape($useridentifier, '', LDAP_ESCAPE_FILTER) . '=' . ldap_escape($search, '', LDAP_ESCAPE_FILTER) . ')';
1183
        }
1184
1185
        if (is_array($attributeArray)) {
1186
            // Return list with required fields
1187
            $attributeArray = array_values($attributeArray); // This is to force to have index reordered from 0 (not make ldap_search fails)
1188
            dol_syslog(get_class($this) . "::getRecords connection=" . $this->connectedServer . ":" . $this->serverPort . " userDn=" . $userDn . " filter=" . $filter . " attributeArray=(" . implode(',', $attributeArray) . ")");
1189
            //var_dump($attributeArray);
1190
            $this->result = @ldap_search($this->connection, $userDn, $filter, $attributeArray);
1191
        } else {
1192
            // Return list with fields selected by default
1193
            dol_syslog(get_class($this) . "::getRecords connection=" . $this->connectedServer . ":" . $this->serverPort . " userDn=" . $userDn . " filter=" . $filter);
1194
            $this->result = @ldap_search($this->connection, $userDn, $filter);
1195
        }
1196
        if (!$this->result) {
1197
            $this->error = 'LDAP search failed: ' . ldap_errno($this->connection) . " " . ldap_error($this->connection);
1198
            return -1;
1199
        }
1200
1201
        $info = @ldap_get_entries($this->connection, $this->result);
1202
1203
        // Warning: Dans info, les noms d'attributs sont en minuscule meme si passe
1204
        // a ldap_search en majuscule !!!
1205
        //print_r($info);
1206
1207
        for ($i = 0; $i < $info["count"]; $i++) {
1208
            $recordid = $this->convToOutputCharset($info[$i][strtolower($useridentifier)][0], $this->ldapcharset);
1209
            if ($recordid) {
1210
                //print "Found record with key $useridentifier=".$recordid."<br>\n";
1211
                $fulllist[$recordid][$useridentifier] = $recordid;
1212
1213
                // Add to the array for each attribute in my list
1214
                $num = count($attributeArray);
1215
                for ($j = 0; $j < $num; $j++) {
1216
                    $keyattributelower = strtolower($attributeArray[$j]);
1217
                    //print " Param ".$attributeArray[$j]."=".$info[$i][$keyattributelower][0]."<br>\n";
1218
1219
                    //permet de recuperer le SID avec Active Directory
1220
                    if ($this->serverType == "activedirectory" && $keyattributelower == "objectsid") {
1221
                        $objectsid = $this->getObjectSid($recordid);
1222
                        $fulllist[$recordid][$attributeArray[$j]] = $objectsid;
1223
                    } else {
1224
                        if (in_array($attributeArray[$j], $attributeAsArray) && is_array($info[$i][$keyattributelower])) {
1225
                            $valueTab = array();
1226
                            foreach ($info[$i][$keyattributelower] as $key => $value) {
1227
                                $valueTab[$key] = $this->convToOutputCharset($value, $this->ldapcharset);
1228
                            }
1229
                            $fulllist[$recordid][$attributeArray[$j]] = $valueTab;
1230
                        } else {
1231
                            $fulllist[$recordid][$attributeArray[$j]] = $this->convToOutputCharset($info[$i][$keyattributelower][0], $this->ldapcharset);
1232
                        }
1233
                    }
1234
                }
1235
            }
1236
        }
1237
1238
        asort($fulllist);
1239
        return $fulllist;
1240
    }
1241
1242
    /**
1243
     *  Converts a little-endian hex-number to one, that 'hexdec' can convert
1244
     *  Required by Active Directory
1245
     *
1246
     *  @param  string      $hex            Hex value
1247
     *  @return string                      Little endian
1248
     */
1249
    public function littleEndian($hex)
1250
    {
1251
        $result = '';
1252
        for ($x = dol_strlen($hex) - 2; $x >= 0; $x = $x - 2) {
1253
            $result .= substr($hex, $x, 2);
1254
        }
1255
        return $result;
1256
    }
1257
1258
1259
    /**
1260
     *  Gets LDAP user SID.
1261
     *  Required by Active Directory
1262
     *
1263
     *  @param  string      $ldapUser       User login
1264
     *  @return int|string                  if SID OK: SID string, if KO: -1
1265
     */
1266
    public function getObjectSid($ldapUser)
1267
    {
1268
        $criteria = '(' . $this->getUserIdentifier() . '=' . $ldapUser . ')';
1269
        $justthese = array("objectsid");
1270
1271
        // if the directory is AD, then bind first with the search user first
1272
        if ($this->serverType == "activedirectory") {
1273
            $this->bindauth($this->searchUser, $this->searchPassword);
1274
        }
1275
1276
        $i = 0;
1277
        $searchDN = $this->people;
1278
1279
        while ($i <= 2) {
1280
            $ldapSearchResult = @ldap_search($this->connection, $searchDN, $criteria, $justthese);
1281
1282
            if (!$ldapSearchResult) {
1283
                $this->error = ldap_errno($this->connection) . " " . ldap_error($this->connection);
1284
                return -1;
1285
            }
1286
1287
            $entry = ldap_first_entry($this->connection, $ldapSearchResult);
1288
1289
            if (!$entry) {
1290
                // Si pas de resultat on cherche dans le domaine
1291
                $searchDN = $this->domain;
1292
                $i++;
1293
            } else {
1294
                $i++;
1295
                $i++;
1296
            }
1297
        }
1298
1299
        if ($entry) {
1300
            $ldapBinary = ldap_get_values_len($this->connection, $entry, "objectsid");
1301
            $SIDText = $this->binSIDtoText($ldapBinary[0]);
1302
            return $SIDText;
1303
        } else {
1304
            $this->error = ldap_errno($this->connection) . " " . ldap_error($this->connection);
1305
            return -1;
1306
        }
1307
    }
1308
1309
    /**
1310
     * Returns the textual SID
1311
     * Required by Active Directory
1312
     *
1313
     * @param   string  $binsid     Binary SID
1314
     * @return  string              Textual SID
1315
     */
1316
    public function binSIDtoText($binsid)
1317
    {
1318
        $hex_sid = bin2hex($binsid);
1319
        $rev = hexdec(substr($hex_sid, 0, 2)); // Get revision-part of SID
1320
        $subcount = hexdec(substr($hex_sid, 2, 2)); // Get count of sub-auth entries
1321
        $auth = hexdec(substr($hex_sid, 4, 12)); // SECURITY_NT_AUTHORITY
1322
        $result = "$rev-$auth";
1323
        for ($x = 0; $x < $subcount; $x++) {
1324
            $result .= "-" . hexdec($this->littleEndian(substr($hex_sid, 16 + ($x * 8), 8))); // get all SECURITY_NT_AUTHORITY
1325
        }
1326
        return $result;
1327
    }
1328
1329
1330
    /**
1331
     *  Search method with filter
1332
     *  this->connection must be defined. The bind or bindauth methods must already have been called.
1333
     *  Do not use for search of a given properties list because of upper-lower case conflict.
1334
     *  Only use for pages.
1335
     *  'Fiche LDAP' shows readable fields by default.
1336
     *  @see bind
1337
     *  @see bindauth
1338
     *
1339
     *  @param  string      $checkDn        Search DN (Ex: ou=users,cn=my-domain,cn=com)
1340
     *  @param  string      $filter         Search filter (ex: (sn=name_person) )
1341
     *  @return array|int                   Array with answers (lowercase key - value)
1342
     */
1343
    public function search($checkDn, $filter)
1344
    {
1345
        dol_syslog(get_class($this) . "::search checkDn=" . $checkDn . " filter=" . $filter);
1346
1347
        $checkDn = $this->convFromOutputCharset($checkDn, $this->ldapcharset);
1348
        $filter = $this->convFromOutputCharset($filter, $this->ldapcharset);
1349
1350
        // if the directory is AD, then bind first with the search user first
1351
        if ($this->serverType == "activedirectory") {
1352
            $this->bindauth($this->searchUser, $this->searchPassword);
1353
        }
1354
1355
        $this->result = @ldap_search($this->connection, $checkDn, $filter);
1356
1357
        $result = @ldap_get_entries($this->connection, $this->result);
1358
        if (!$result) {
1359
            $this->error = ldap_errno($this->connection) . " " . ldap_error($this->connection);
1360
            return -1;
1361
        } else {
1362
            ldap_free_result($this->result);
1363
            return $result;
1364
        }
1365
    }
1366
1367
1368
    /**
1369
     *  Load all attributes of an LDAP user
1370
     *
1371
     *  @param  User|string $user       Not used.
1372
     *  @param  string      $filter     Filter for search. Must start with &.
1373
     *                                  Examples: &(objectClass=inetOrgPerson) &(objectClass=user)(objectCategory=person) &(isMemberOf=cn=Sales,ou=Groups,dc=opencsi,dc=com)
1374
     *  @return int                     if KO: <0 || if OK: > 0
1375
     */
1376
    public function fetch($user, $filter)
1377
    {
1378
        // Perform the search and get the entry handles
1379
1380
        // if the directory is AD, then bind first with the search user first
1381
        if ($this->serverType == "activedirectory") {
1382
            $this->bindauth($this->searchUser, $this->searchPassword);
1383
        }
1384
1385
        $searchDN = $this->people; // TODO Why searching in people then domain ?
1386
1387
        $result = '';
1388
        $i = 0;
1389
        while ($i <= 2) {
1390
            dol_syslog(get_class($this) . "::fetch search with searchDN=" . $searchDN . " filter=" . $filter);
1391
            $this->result = @ldap_search($this->connection, $searchDN, $filter);
1392
            if ($this->result) {
1393
                $result = @ldap_get_entries($this->connection, $this->result);
1394
                if ($result['count'] > 0) {
1395
                    dol_syslog('Ldap::fetch search found ' . $result['count'] . ' records');
1396
                } else {
1397
                    dol_syslog('Ldap::fetch search returns but found no records');
1398
                }
1399
                //var_dump($result);exit;
1400
            } else {
1401
                $this->error = ldap_errno($this->connection) . " " . ldap_error($this->connection);
1402
                dol_syslog(get_class($this) . "::fetch search fails");
1403
                return -1;
1404
            }
1405
1406
            if (!$result) {
1407
                // Si pas de resultat on cherche dans le domaine
1408
                $searchDN = $this->domain;
1409
                $i++;
1410
            } else {
1411
                break;
1412
            }
1413
        }
1414
1415
        if (!$result) {
1416
            $this->error = ldap_errno($this->connection) . " " . ldap_error($this->connection);
1417
            return -1;
1418
        } else {
1419
            $this->name       = $this->convToOutputCharset($result[0][$this->attr_name][0], $this->ldapcharset);
1420
            $this->firstname  = $this->convToOutputCharset($result[0][$this->attr_firstname][0], $this->ldapcharset);
1421
            $this->login      = $this->convToOutputCharset($result[0][$this->attr_login][0], $this->ldapcharset);
1422
            $this->phone      = $this->convToOutputCharset($result[0][$this->attr_phone][0], $this->ldapcharset);
1423
            $this->fax        = $this->convToOutputCharset($result[0][$this->attr_fax][0], $this->ldapcharset);
1424
            $this->mail       = $this->convToOutputCharset($result[0][$this->attr_mail][0], $this->ldapcharset);
1425
            $this->mobile     = $this->convToOutputCharset($result[0][$this->attr_mobile][0], $this->ldapcharset);
1426
1427
            $this->uacf       = $this->parseUACF($this->convToOutputCharset($result[0]["useraccountcontrol"][0], $this->ldapcharset));
1428
            if (isset($result[0]["pwdlastset"][0])) {   // If expiration on password exists
1429
                $this->pwdlastset = ($result[0]["pwdlastset"][0] != 0) ? $this->convertTime($this->convToOutputCharset($result[0]["pwdlastset"][0], $this->ldapcharset)) : 0;
1430
            } else {
1431
                $this->pwdlastset = -1;
1432
            }
1433
            if (!$this->name && !$this->login) {
1434
                $this->pwdlastset = -1;
1435
            }
1436
            $this->badpwdtime = $this->convertTime($this->convToOutputCharset($result[0]["badpasswordtime"][0], $this->ldapcharset));
1437
1438
            // FQDN domain
1439
            $domain = str_replace('dc=', '', $this->domain);
1440
            $domain = str_replace(',', '.', $domain);
1441
            $this->domainFQDN = $domain;
1442
1443
            // Set ldapUserDn (each user can have a different dn)
1444
            //var_dump($result[0]);exit;
1445
            $this->ldapUserDN = $result[0]['dn'];
1446
1447
            ldap_free_result($this->result);
1448
            return 1;
1449
        }
1450
    }
1451
1452
1453
    // helper methods
1454
1455
    /**
1456
     *  Returns the correct user identifier to use, based on the LDAP server type
1457
     *
1458
     *  @return string              Login
1459
     */
1460
    public function getUserIdentifier()
1461
    {
1462
        if ($this->serverType == "activedirectory") {
1463
            return $this->attr_sambalogin;
1464
        } else {
1465
            return $this->attr_login;
1466
        }
1467
    }
1468
1469
    /**
1470
     *  UserAccountControl Flags to more human understandable form...
1471
     *
1472
     *  @param  string      $uacf       UACF
1473
     *  @return array
1474
     */
1475
    public function parseUACF($uacf)
1476
    {
1477
        //All flags array
1478
        $flags = array(
1479
            "TRUSTED_TO_AUTH_FOR_DELEGATION"  =>    16777216,
1480
            "PASSWORD_EXPIRED"                =>    8388608,
1481
            "DONT_REQ_PREAUTH"                =>    4194304,
1482
            "USE_DES_KEY_ONLY"                =>    2097152,
1483
            "NOT_DELEGATED"                   =>    1048576,
1484
            "TRUSTED_FOR_DELEGATION"          =>    524288,
1485
            "SMARTCARD_REQUIRED"              =>    262144,
1486
            "MNS_LOGON_ACCOUNT"               =>    131072,
1487
            "DONT_EXPIRE_PASSWORD"            =>    65536,
1488
            "SERVER_TRUST_ACCOUNT"            =>    8192,
1489
            "WORKSTATION_TRUST_ACCOUNT"       =>    4096,
1490
            "INTERDOMAIN_TRUST_ACCOUNT"       =>    2048,
1491
            "NORMAL_ACCOUNT"                  =>    512,
1492
            "TEMP_DUPLICATE_ACCOUNT"          =>    256,
1493
            "ENCRYPTED_TEXT_PWD_ALLOWED"      =>    128,
1494
            "PASSWD_CANT_CHANGE"              =>    64,
1495
            "PASSWD_NOTREQD"                  =>    32,
1496
            "LOCKOUT"                         =>    16,
1497
            "HOMEDIR_REQUIRED"                =>    8,
1498
            "ACCOUNTDISABLE"                  =>    2,
1499
            "SCRIPT"                          =>    1
1500
        );
1501
1502
        //Parse flags to text
1503
        $retval = array();
1504
        //while (list($flag, $val) = each($flags)) {
1505
        foreach ($flags as $flag => $val) {
1506
            if ($uacf >= $val) {
1507
                $uacf -= $val;
1508
                $retval[$val] = $flag;
1509
            }
1510
        }
1511
1512
        //Return human friendly flags
1513
        return $retval;
1514
    }
1515
1516
    /**
1517
     *  SamAccountType value to text
1518
     *
1519
     *  @param  string  $samtype    SamType
1520
     *  @return string              Sam string
1521
     */
1522
    public function parseSAT($samtype)
1523
    {
1524
        $stypes = array(
1525
            805306368 => "NORMAL_ACCOUNT",
1526
            805306369 => "WORKSTATION_TRUST",
1527
            805306370 => "INTERDOMAIN_TRUST",
1528
            268435456 => "SECURITY_GLOBAL_GROUP",
1529
            268435457 => "DISTRIBUTION_GROUP",
1530
            536870912 => "SECURITY_LOCAL_GROUP",
1531
            536870913 => "DISTRIBUTION_LOCAL_GROUP"
1532
        );
1533
1534
        $retval = "";
1535
        foreach ($stypes as $sat => $val) {
1536
            if ($samtype == $sat) {
1537
                $retval = $val;
1538
                break;
1539
            }
1540
        }
1541
        if (empty($retval)) {
1542
            $retval = "UNKNOWN_TYPE_" . $samtype;
1543
        }
1544
1545
        return $retval;
1546
    }
1547
1548
    /**
1549
     *  Converts ActiveDirectory time to Unix timestamp
1550
     *
1551
     *  @param  string  $value      AD time to convert
1552
     *  @return integer             Unix timestamp
1553
     */
1554
    public function convertTime($value)
1555
    {
1556
        $dateLargeInt = $value; // nano secondes depuis 1601 !!!!
1557
        $secsAfterADEpoch = $dateLargeInt / (10000000); // secondes depuis le 1 jan 1601
1558
        $ADToUnixConvertor = ((1970 - 1601) * 365.242190) * 86400; // UNIX start date - AD start date * jours * secondes
1559
        $unixTimeStamp = intval($secsAfterADEpoch - $ADToUnixConvertor); // Unix time stamp
1560
        return $unixTimeStamp;
1561
    }
1562
1563
1564
    /**
1565
     *  Convert a string into output/memory charset
1566
     *
1567
     *  @param  string  $str            String to convert
1568
     *  @param  string  $pagecodefrom   Page code of src string
1569
     *  @return string                  Converted string
1570
     */
1571
    private function convToOutputCharset($str, $pagecodefrom = 'UTF-8')
1572
    {
1573
        global $conf;
1574
        if ($pagecodefrom == 'ISO-8859-1' && $conf->file->character_set_client == 'UTF-8') {
1575
            $str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
1576
        }
1577
        if ($pagecodefrom == 'UTF-8' && $conf->file->character_set_client == 'ISO-8859-1') {
1578
            $str = mb_convert_encoding($str, 'ISO-8859-1');
1579
        }
1580
        return $str;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $str also could return the type array which is incompatible with the documented return type string.
Loading history...
1581
    }
1582
1583
    /**
1584
     *  Convert a string from output/memory charset
1585
     *
1586
     *  @param  string  $str            String to convert
1587
     *  @param  string  $pagecodeto     Page code for result string
1588
     *  @return string                  Converted string
1589
     */
1590
    public function convFromOutputCharset($str, $pagecodeto = 'UTF-8')
1591
    {
1592
        global $conf;
1593
        if ($pagecodeto == 'ISO-8859-1' && $conf->file->character_set_client == 'UTF-8') {
1594
            $str = mb_convert_encoding($str, 'ISO-8859-1');
1595
        }
1596
        if ($pagecodeto == 'UTF-8' && $conf->file->character_set_client == 'ISO-8859-1') {
1597
            $str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
1598
        }
1599
        return $str;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $str also could return the type array which is incompatible with the documented return type string.
Loading history...
1600
    }
1601
1602
1603
    /**
1604
     *  Return available value of group GID
1605
     *
1606
     *  @param  string  $keygroup   Key of group
1607
     *  @return int                 gid number
1608
     */
1609
    public function getNextGroupGid($keygroup = 'LDAP_KEY_GROUPS')
1610
    {
1611
1612
        if (empty($keygroup)) {
1613
            $keygroup = 'LDAP_KEY_GROUPS';
1614
        }
1615
1616
        $search = '(' . getDolGlobalString($keygroup) . '=*)';
1617
        $result = $this->search($this->groups, $search);
1618
        if ($result) {
1619
            $c = $result['count'];
1620
            $gids = array();
1621
            for ($i = 0; $i < $c; $i++) {
1622
                $gids[] = $result[$i]['gidnumber'][0];
1623
            }
1624
            rsort($gids);
1625
1626
            return $gids[0] + 1;
1627
        }
1628
1629
        return 0;
1630
    }
1631
}
1632