Passed
Push — master ( 344c14...9aa841 )
by Christopher
01:46
created

src/Connection.php (1 issue)

1
<?php
2
/**
3
 * @link      https://github.com/chrmorandi/yii2-ldap for the source repository
4
 * @package   yii2-ldap
5
 * @author    Christopher Mota <[email protected]>
6
 * @license   MIT License - view the LICENSE file that was distributed with this source code.
7
 * @since     1.0.0
8
 */
9
10
namespace chrmorandi\ldap;
11
12
use Yii;
13
use yii\base\Component;
14
use yii\caching\Cache;
15
use yii\caching\CacheInterface;
16
use yii\caching\Dependency;
17
use yii\caching\TagDependency;
18
19
/**
20
 * @property resource $resource
21
 * @property bool     $bount
22
 * @property int      $errNo Error number of the last command
23
 * @property string   $lastError Error message of the last command
24
 *
25
 * @author Christopher Mota <[email protected]>
26
 * @since  1.0
27
 */
28
class Connection extends Component
29
{
30
    /**
31
     * LDAP protocol string.
32
     * @var string
33
     */
34
    const PROTOCOL = 'ldap://';
35
36
    /**
37
     * LDAP port number.
38
     * @var int
39
     */
40
    const PORT = 389;
41
42
    /**
43
     * @event Event an event that is triggered after a DB connection is established
44
     */
45
    const EVENT_AFTER_OPEN = 'afterOpen';
46
47
    /**
48
     * @var string the LDAP base dn.
49
     */
50
    public $baseDn;
51
52
    /**
53
     * https://msdn.microsoft.com/en-us/library/ms677913(v=vs.85).aspx
54
     * @var bool the integer to instruct the LDAP connection whether or not to follow referrals.
55
     */
56
    public $followReferrals = false;
57
58
    /**
59
     * @var int The LDAP port to use when connecting to the domain controllers.
60
     */
61
    public $port = self::PORT;
62
63
    /**
64
     * @var bool Determines whether or not to use TLS with the current LDAP connection.
65
     */
66
    public $useTLS = true;
67
68
    /**
69
     * @var array the domain controllers to connect to.
70
     */
71
    public $dc = [];
72
73
    /**
74
     * @var string the username for establishing LDAP connection. Defaults to `null` meaning no username to use.
75
     */
76
    public $username;
77
78
    /**
79
     * @var string the password for establishing DB connection. Defaults to `null` meaning no password to use.
80
     */
81
    public $password;
82
83
    /**
84
     * @var int The page size for the paging operation.
85
     */
86
    public $pageSize = -1;
87
88
    /**
89
     * @var integer zero-based offset from where the records are to be returned. If not set or
90
     * less than 1, it means not filter values.
91
     */
92
    public $offset = -1;
93
94
    /**
95
     * @var bool whether to enable caching.
96
     * Note that in order to enable query caching, a valid cache component as specified
97
     * by [[cache]] must be enabled and [[enableCache]] must be set true.
98
     * Also, only the results of the queries enclosed within [[cache()]] will be cached.
99
     * @see cacheDuration
100
     * @see cache
101
     */
102
    public $enableCache = true;
103
104
    /**
105
     * @var integer number of seconds that table metadata can remain valid in cache.
106
     * Use 0 to indicate that the cached data will never expire.
107
     * @see enableCache
108
     */
109
    public $cacheDuration = 3600;
110
111
    /**
112
     * @var string the cache the ID of the cache application component that
113
     * is used to cache result query.
114
     * @see enableCache
115
     */
116
    public $cache = 'cache';
117
118
    /**
119
     * @var string the attribute for authentication
120
     */
121
    public $loginAttribute = "sAMAccountName";
122
123
    /**
124
     * @var bool stores the bool whether or not the current connection is bound.
125
     */
126
    protected $_bound = false;
127
128
    /**
129
     * @var resource|false
130
     */
131
    protected $resource;
132
133
    /**
134
     *
135
     * @var string
136
     */
137
    protected $userDN;
138
139
    /**
140
     * Create AD password (Microsoft Active Directory password format)
141
     * @param string $password
142
     * @return string
143
     */
144
    protected static function encodePassword($password)
145
    {
146
        $password   = "\"" . $password . "\"";
147
        $adpassword = mb_convert_encoding($password, "UTF-16LE", "UTF-8");
148
        return $adpassword;
149
    }
150
151
    /**
152
     * Returns the current query cache information.
153
     * This method is used internally by [[Command]].
154
     * @param integer $duration the preferred caching duration. If null, it will be ignored.
155
     * @param Dependency $dependency the preferred caching dependency. If null, it will be ignored.
156
     * @return array|null the current query cache information, or null if query cache is not enabled.
157
     * @internal
158
     */
159
    public function getCacheInfo($duration = 3600, $dependency = null)
160
    {
161
        if (!$this->enableCache) {
162
            return null;
163
        }
164
165
        if (($duration === 0 || $duration > 0) && Yii::$app) {
166
            $cache = Yii::$app->get($this->cache, false);
167
            if ($cache instanceof Cache) {
168
                return [$cache, $duration, $dependency];
169
            }
170
        }
171
172
        return null;
173
    }
174
175
    /**
176
     * Invalidates the cached data that are associated with any of the specified [[tags]] in this connection.
177
     * @param string|array $tags
178
     */
179
    public function clearCache($tags)
180
    {
181
        $cache = Yii::$app->get($this->cache, false);
182
        if ($cache instanceof CacheInterface) {
183
            TagDependency::invalidate($cache, $tags);
184
        }
185
    }
186
187
    /**
188
     * Connects and Binds to the Domain Controller with a administrator credentials.
189
     * @return void
190
     */
191
    public function open($anonymous = false)
192
    {
193
        $token = 'Opening LDAP connection: ' . LdapHelper::recursive_implode($this->dc, ' or ');
194
        Yii::info($token, __METHOD__);
195
        Yii::beginProfile($token, __METHOD__);
196
        // Connect to the LDAP server.
197
        $this->connect($this->dc, $this->port);
198
        Yii::endProfile($token, __METHOD__);
199
200
        try {
201
            if ($anonymous) {
202
                $this->_bound = ldap_bind($this->resource);
203
            } else {
204
                $this->_bound = ldap_bind($this->resource, $this->username, $this->password);
205
            }
206
        } catch (\Exception $e) {
207
            throw new \Exception('Invalid credential for user manager in ldap.', 0);
208
        }
209
    }
210
211
    /**
212
     * Connection.
213
     * @param string|array $hostname
214
     * @param int $port
215
     * @return void
216
     */
217
    protected function connect($hostname = [], $port = 389)
218
    {
219
        if (is_array($hostname)) {
220
            $hostname = self::PROTOCOL . implode(' ' . self::PROTOCOL, $hostname);
221
        }
222
223
        $this->close();
224
        $this->resource = ldap_connect($hostname, $port);
225
226
        // Set the LDAP options.
227
        $this->setOption(LDAP_OPT_PROTOCOL_VERSION, 3);
228
        $this->setOption(LDAP_OPT_REFERRALS, $this->followReferrals);
229
        $this->setOption(LDAP_OPT_NETWORK_TIMEOUT, 2);
230
231
        if ($this->useTLS) {
232
            $this->startTLS();
233
        }
234
235
        $this->trigger(self::EVENT_AFTER_OPEN);
236
    }
237
238
    /**
239
     * Authenticate user
240
     * @param string $username
241
     * @param string $password
242
     * @return bool indicate occurrence of error.
243
     */
244
    public function auth($username, $password)
245
    {
246
        // Open connection with manager
247
        $this->open();
248
249
        # Search for user and get user DN
250
        $searchResult = ldap_search($this->resource, $this->baseDn, "(&(objectClass=person)($this->loginAttribute=$username))", [$this->loginAttribute]);
251
        $entry        = $this->getFirstEntry($searchResult);
252
        if ($entry) {
0 ignored issues
show
$entry is of type resource, thus it always evaluated to false.
Loading history...
253
            $this->userDN = $this->getDn($entry);
254
        } else {
255
            // User not found.
256
            return false;
257
        }
258
259
        // Connect to the LDAP server.
260
        $this->connect($this->dc, $this->port);
261
262
        // Try to authenticate user, but ignore any PHP warnings.
263
        return @ldap_bind($this->resource, $this->userDN, $password);
264
    }
265
266
    /**
267
     * Change the password of the current user. This must be performed over TLS.
268
     * @param string $username User for change password
269
     * @param string $oldPassword The old password
270
     * @param string $newPassword The new password
271
     * @return bool return true if change password is success
272
     * @throws \Exception
273
     */
274
    public function changePasswordAsUser($username, $oldPassword, $newPassword)
275
    {
276
        if (!$this->useTLS) {
277
            $message = 'TLS must be configured on your web server and enabled to change passwords.';
278
            throw new \Exception($message);
279
        }
280
281
        // Open connection with user
282
        if (!$this->auth($username, $oldPassword)) {
283
            return false;
284
        }
285
286
        return $this->changePasswordAsManager($this->userDN, $newPassword);
287
    }
288
289
    /**
290
     * Change the password of the user as manager. This must be performed over TLS.
291
     * @param string $userDN User Distinguished Names (DN) for change password. Ex.: cn=admin,dc=example,dc=com
292
     * @param string $newPassword The new password
293
     * @return bool return true if change password is success
294
     * @throws \Exception
295
     */
296
    public function changePasswordAsManager($userDN, $newPassword)
297
    {
298
        if (!$this->useTLS) {
299
            $message = 'TLS must be configured on your web server and enabled to change passwords.';
300
            throw new \Exception($message);
301
        }
302
303
        // Open connection with manager
304
        $this->open();
305
306
        // Replace passowrd attribute for AD
307
        // The AD password change procedure is modifying the attribute unicodePwd
308
        $modifications['unicodePwd'] = self::encodePassword($newPassword);
309
        return ldap_mod_replace($this->resource, $userDN, $modifications);
310
    }
311
312
    /**
313
     * Closes the current connection.
314
     *
315
     * @return bool
316
     */
317
    public function close()
318
    {
319
        if (is_resource($this->resource)) {
320
            ldap_close($this->resource);
321
        }
322
        return true;
323
    }
324
325
    /**
326
     * Execute ldap search like.
327
     *
328
     * @link http://php.net/manual/en/ref.ldap.php
329
     *
330
     * @param  string $function php LDAP function
331
     * @param  array $params params for execute ldap function
332
     * @return bool|DataReader
333
     */
334
    public function executeQuery($function, $params)
335
    {
336
        $this->open();
337
        $results = [];
338
        $cookie  = '';
339
        $token   = $function . ' - params: ' . LdapHelper::recursive_implode($params, ';');
340
341
        Yii::info($token, 'chrmorandi\ldap\Connection::query');
342
343
        Yii::beginProfile($token, 'chrmorandi\ldap\Connection::query');
344
        do {
345
            if ($this->pageSize > 0) {
346
                $this->setControlPagedResult($cookie);
347
            }
348
349
            // Run the search.
350
            $result = call_user_func($function, $this->resource, ...$params);
351
352
            if ($this->pageSize > 0) {
353
                $this->setControlPagedResultResponse($result, $cookie);
354
            }
355
356
            //Collect each resource result
357
            $results[] = $result;
358
        } while (!is_null($cookie) && !empty($cookie));
359
        Yii::endProfile($token, 'chrmorandi\ldap\Connection::query');
360
361
        return new DataReader($this, $results);
362
    }
363
364
    /**
365
     * Returns true/false if the current connection is bound.
366
     * @return bool
367
     */
368
    public function getBound()
369
    {
370
        return $this->_bound;
371
    }
372
373
    /**
374
     * Get the current resource of connection.
375
     * @return resource
376
     */
377
    public function getResource()
378
    {
379
        return $this->resource;
380
    }
381
382
    /**
383
     * Adds an entry to the current connection.
384
     * @param string $dn
385
     * @param array  $entry
386
     * @return bool
387
     */
388
    public function add($dn, array $entry)
389
    {
390
        return ldap_add($this->resource, $dn, $entry);
391
    }
392
393
    /**
394
     * Deletes an entry on the current connection.
395
     * @param string $dn
396
     * @return bool
397
     */
398
    public function delete($dn)
399
    {
400
        return ldap_delete($this->resource, $dn);
401
    }
402
403
    /**
404
     * Modify the name of an entry on the current connection.
405
     *
406
     * @param string $dn
407
     * @param string $newRdn
408
     * @param string $newParent
409
     * @param bool   $deleteOldRdn
410
     * @return bool
411
     */
412
    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
413
    {
414
        return ldap_rename($this->resource, $dn, $newRdn, $newParent, $deleteOldRdn);
415
    }
416
417
    /**
418
     * Batch modifies an existing entry on the current connection.
419
     * The types of modifications:
420
     *      LDAP_MODIFY_BATCH_ADD - Each value specified through values is added.
421
     *      LDAP_MODIFY_BATCH_REMOVE - Each value specified through values is removed.
422
     *          Any value of the attribute not contained in the values array will remain untouched.
423
     *      LDAP_MODIFY_BATCH_REMOVE_ALL - All values are removed from the attribute named by attrib.
424
     *      LDAP_MODIFY_BATCH_REPLACE - All current values are replaced by new one.
425
     * @param string $dn
426
     * @param array  $values array associative with three keys: "attrib", "modtype" and "values".
427
     * ```php
428
     * [
429
     *     "attrib"  => "attribute",
430
     *     "modtype" => LDAP_MODIFY_BATCH_ADD,
431
     *     "values"  => ["attribute value one"],
432
     * ],
433
     * ```
434
     * @return mixed
435
     */
436
    public function modify($dn, array $values)
437
    {
438
        return ldap_modify_batch($this->resource, $dn, $values);
439
    }
440
441
    /**
442
     * Retrieve the entries from a search result.
443
     * @param resource $searchResult
444
     * @return array|bool
445
     */
446
    public function getEntries($searchResult)
447
    {
448
        return ldap_get_entries($this->resource, $searchResult);
449
    }
450
451
    /**
452
     * Retrieves the number of entries from a search result.
453
     * @param resource $searchResult
454
     * @return int
455
     */
456
    public function countEntries($searchResult)
457
    {
458
        return ldap_count_entries($this->resource, $searchResult);
459
    }
460
461
    /**
462
     * Retrieves the first entry from a search result.
463
     * @param resource $searchResult
464
     * @return resource|false the result entry identifier for the first entry on success and FALSE on error.
465
     */
466
    public function getFirstEntry($searchResult)
467
    {
468
        return ldap_first_entry($this->resource, $searchResult);
469
    }
470
471
    /**
472
     * Retrieves the next entry from a search result.
473
     * @param resource $entry link identifier
474
     * @return resource
475
     */
476
    public function getNextEntry($entry)
477
    {
478
        return ldap_next_entry($this->resource, $entry);
479
    }
480
481
    /**
482
     * Retrieves the ldap first entry attribute.
483
     * @param resource $entry
484
     * @return string
485
     */
486
    public function getFirstAttribute($entry)
487
    {
488
        return ldap_first_attribute($this->resource, $entry);
489
    }
490
491
    /**
492
     * Retrieves the ldap next entry attribute.
493
     * @param resource $entry
494
     * @return string
495
     */
496
    public function getNextAttribute($entry)
497
    {
498
        return ldap_next_attribute($this->resource, $entry);
499
    }
500
501
    /**
502
     * Retrieves the ldap entry's attributes.
503
     * @param resource $entry
504
     * @return array
505
     */
506
    public function getAttributes($entry)
507
    {
508
        return ldap_get_attributes($this->resource, $entry);
509
    }
510
511
    /**
512
     * Retrieves all binary values from a result entry. Individual values are accessed by integer index in the array.
513
     * The first index is 0. The number of values can be found by indexing "count" in the resultant array.
514
     *
515
     * @link https://www.php.net/manual/en/function.ldap-get-values-len.php
516
     *
517
     * @param resource $entry Link identifier
518
     * @param string $attribute Name of attribute
519
     * @return array Returns an array of values for the attribute on success and empty array on error.
520
     */
521
    public function getValuesLen($entry, $attribute)
522
    {
523
        $result = ldap_get_values_len($this->resource, $entry, $attribute);
524
        return ($result == false) ? [] : $result;
525
    }
526
527
    /**
528
     * Retrieves the DN of a result entry.
529
     *
530
     * @link https://www.php.net/manual/en/function.ldap-get-dn.php
531
     *
532
     * @param resource $entry
533
     * @return string
534
     */
535
    public function getDn($entry)
536
    {
537
        return ldap_get_dn($this->resource, $entry);
538
    }
539
540
    /**
541
     * Free result memory.
542
     *
543
     * @link https://www.php.net/manual/en/function.ldap-free-result.php
544
     *
545
     * @param resource $searchResult
546
     * @return bool Returns TRUE on success or FALSE on failure.
547
     */
548
    public function freeResult($searchResult)
549
    {
550
        return ldap_free_result($searchResult);
551
    }
552
553
    /**
554
     * Sets an option on the current connection.
555
     *
556
     * @link https://www.php.net/manual/en/function.ldap-set-option.php
557
     *
558
     * @param int   $option The parameter.
559
     * @param mixed $value The new value for the specified option.
560
     * @return bool Returns TRUE on success or FALSE on failure.
561
     */
562
    public function setOption($option, $value)
563
    {
564
        return ldap_set_option($this->resource, $option, $value);
565
    }
566
567
    /**
568
     * Starts a connection using TLS.
569
     *
570
     * @link https://www.php.net/manual/en/function.ldap-start-tls.php
571
     *
572
     * @return bool
573
     */
574
    public function startTLS()
575
    {
576
        return ldap_start_tls($this->resource);
577
    }
578
579
    /**
580
     * Send LDAP pagination control.
581
     *
582
     * @link http://php.net/manual/en/function.ldap-control-paged-result.php
583
     *
584
     * @param string $cookie An opaque structure sent by the server
585
     * @param bool   $isCritical Indicates whether the pagination is critical or not. If true and if the server doesn't support pagination, the search will return no result.
586
     * @return bool Returns TRUE on success or FALSE on failure.
587
     */
588
    public function setControlPagedResult($cookie = '', $isCritical = false)
589
    {
590
        return ldap_control_paged_result($this->resource, $this->pageSize, $isCritical, $cookie);
591
    }
592
593
    /**
594
     * Retrieve a paginated result response.
595
     *
596
     * @link https://www.php.net/manual/en/function.ldap-control-paged-result-response.php
597
     *
598
     * @param resource $result
599
     * @param string $cookie An opaque structure sent by the server
600
     * @return bool Returns TRUE on success or FALSE on failure.
601
     */
602
    public function setControlPagedResultResponse($result, &$cookie)
603
    {
604
        return ldap_control_paged_result_response($this->resource, $result, $cookie);
605
    }
606
607
    /**
608
     * Return the LDAP error message of the last LDAP command.
609
     *
610
     * @link https://www.php.net/manual/en/function.ldap-error.php
611
     *
612
     * @return string Error message.
613
     */
614
    public function getLastError()
615
    {
616
        return ldap_error($this->resource);
617
    }
618
619
    /**
620
     * Returns the number of the last error on the current connection.
621
     *
622
     * @link https://www.php.net/manual/en/function.ldap-errno.php
623
     *
624
     * @return int Error number
625
     */
626
    public function getErrNo()
627
    {
628
        return ldap_errno($this->resource);
629
    }
630
631
    /**
632
     * Returns the error string of the specified error number.
633
     *
634
     * @link https://www.php.net/manual/en/function.ldap-err2str.php
635
     *
636
     * @param int $number The error number.
637
     * @return string  Error message.
638
     */
639
    public function err2Str($number)
640
    {
641
        return ldap_err2str($number);
642
    }
643
644
}
645