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

src/Connection.php (1 issue)

Labels
Severity
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
16
/**
17
 * @property resource $resource
18
 * @property bool     $bount
19
 * @property int      $errNo Error number of the last command
20
 * @property string   $lastError Error message of the last command
21
 *
22
 * @author Christopher Mota <[email protected]>
23
 * @since  1.0
24
 */
25
class Connection extends Component
26
{
27
    /**
28
     * LDAP protocol string.
29
     * @var string
30
     */
31
    const PROTOCOL = 'ldap://';
32
33
    /**
34
     * LDAP port number.
35
     * @var int
36
     */
37
    const PORT = 389;
38
39
    /**
40
     * @event Event an event that is triggered after a DB connection is established
41
     */
42
    const EVENT_AFTER_OPEN = 'afterOpen';
43
44
    /**
45
     * @var string the LDAP base dn.
46
     */
47
    public $baseDn;
48
49
    /**
50
     * https://msdn.microsoft.com/en-us/library/ms677913(v=vs.85).aspx
51
     * @var bool the integer to instruct the LDAP connection whether or not to follow referrals.
52
     */
53
    public $followReferrals = false;
54
55
    /**
56
     * @var int The LDAP port to use when connecting to the domain controllers.
57
     */
58
    public $port = self::PORT;
59
60
    /**
61
     * @var bool Determines whether or not to use TLS with the current LDAP connection.
62
     */
63
    public $useTLS = true;
64
65
    /**
66
     * @var array the domain controllers to connect to.
67
     */
68
    public $dc = [];
69
70
    /**
71
     * @var string the username for establishing LDAP connection. Defaults to `null` meaning no username to use.
72
     */
73
    public $username;
74
75
    /**
76
     * @var string the password for establishing DB connection. Defaults to `null` meaning no password to use.
77
     */
78
    public $password;
79
80
    /**
81
     * @var int The page size for the paging operation.
82
     */
83
    public $pageSize = -1;
84
85
    /**
86
     * @var integer zero-based offset from where the records are to be returned. If not set or
87
     * less than 1, it means not filter values.
88
     */
89
    public $offset = -1;
90
91
    /**
92
     * @var bool whether to enable caching.
93
     * Note that in order to enable query caching, a valid cache component as specified
94
     * by [[cache]] must be enabled and [[enableCache]] must be set true.
95
     * Also, only the results of the queries enclosed within [[cache()]] will be cached.
96
     * @see cacheDuration
97
     * @see cache
98
     */
99
    public $enableCache = true;
100
101
    /**
102
     * @var integer number of seconds that table metadata can remain valid in cache.
103
     * Use 0 to indicate that the cached data will never expire.
104
     * @see enableCache
105
     */
106
    public $cacheDuration = 3600;
107
108
    /**
109
     * @var string the cache the ID of the cache application component that
110
     * is used to cache result query.
111
     * @see enableCache
112
     */
113
    public $cache = 'cache';
114
115
    /**
116
     * @var string the attribute for authentication
117
     */
118
    public $loginAttribute = "sAMAccountName";
119
120
    /**
121
     * @var bool stores the bool whether or not the current connection is bound.
122
     */
123
    protected $_bound = false;
124
125
    /**
126
     * @var resource|false
127
     */
128
    protected $resource;
129
130
    /**
131
     *
132
     * @var string
133
     */
134
    protected $userDN;
135
136
    /**
137
     * Create AD password (Microsoft Active Directory password format)
138
     * @param string $password
139
     * @return string
140
     */
141
    protected static function encodePassword($password)
142
    {
143
        $password   = "\"" . $password . "\"";
144
        $adpassword = mb_convert_encoding($password, "UTF-16LE", "UTF-8");
145
        return $adpassword;
146
    }
147
148
    /**
149
     * Returns the current query cache information.
150
     * This method is used internally by [[Command]].
151
     * @param integer $duration the preferred caching duration. If null, it will be ignored.
152
     * @param \yii\caching\Dependency $dependency the preferred caching dependency. If null, it will be ignored.
153
     * @return array|null the current query cache information, or null if query cache is not enabled.
154
     * @internal
155
     */
156
    public function getCacheInfo($duration = 3600, $dependency = null)
157
    {
158
        if (!$this->enableCache) {
159
            return null;
160
        }
161
162
        if (($duration === 0 || $duration > 0) && Yii::$app) {
163
            $cache = Yii::$app->get($this->cache, false);
164
            if ($cache instanceof Cache) {
165
                return [$cache, $duration, $dependency];
166
            }
167
        }
168
169
        return null;
170
    }
171
172
    /**
173
     * Invalidates the cached data that are associated with any of the specified [[tags]] in this connection.
174
     * @param string|array $tags
175
     */
176
    public function clearCache($tags)
177
    {
178
        $cache = Yii::$app->get($this->cache, false);
179
        \yii\caching\TagDependency::invalidate($cache, $tags);
0 ignored issues
show
It seems like $cache can also be of type mixed; however, parameter $cache of yii\caching\TagDependency::invalidate() does only seem to accept yii\caching\CacheInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

179
        \yii\caching\TagDependency::invalidate(/** @scrutinizer ignore-type */ $cache, $tags);
Loading history...
180
    }
181
182
    /**
183
     * Connects and Binds to the Domain Controller with a administrator credentials.
184
     * @return void
185
     */
186
    public function open($anonymous = false)
187
    {
188
        $token = 'Opening LDAP connection: ' . LdapHelper::recursive_implode($this->dc, ' or ');
189
        Yii::info($token, __METHOD__);
190
        Yii::beginProfile($token, __METHOD__);
191
        // Connect to the LDAP server.
192
        $this->connect($this->dc, $this->port);
193
        Yii::endProfile($token, __METHOD__);
194
195
        try {
196
            if ($anonymous) {
197
                $this->_bound = ldap_bind($this->resource);
198
            } else {
199
                $this->_bound = ldap_bind($this->resource, $this->username, $this->password);
200
            }
201
        } catch (\Exception $e) {
202
            throw new \Exception('Invalid credential for user manager in ldap.', 0);
203
        }
204
    }
205
206
    /**
207
     * Connection.
208
     * @param string|array $hostname
209
     * @param int $port
210
     * @return void
211
     */
212
    protected function connect($hostname = [], $port = 389)
213
    {
214
        if (is_array($hostname)) {
215
            $hostname = self::PROTOCOL . implode(' ' . self::PROTOCOL, $hostname);
216
        }
217
218
        $this->close();
219
        $this->resource = ldap_connect($hostname, $port);
220
221
        // Set the LDAP options.
222
        $this->setOption(LDAP_OPT_PROTOCOL_VERSION, 3);
223
        $this->setOption(LDAP_OPT_REFERRALS, $this->followReferrals);
224
        $this->setOption(LDAP_OPT_NETWORK_TIMEOUT, 2);
225
226
        if ($this->useTLS) {
227
            $this->startTLS();
228
        }
229
230
        $this->trigger(self::EVENT_AFTER_OPEN);
231
    }
232
233
    /**
234
     * Authenticate user
235
     * @param string $username
236
     * @param string $password
237
     * @return bool indicate occurrence of error.
238
     */
239
    public function auth($username, $password)
240
    {
241
        // Open connection with manager
242
        $this->open();
243
244
        # Search for user and get user DN
245
        $searchResult = ldap_search($this->resource, $this->baseDn, "(&(objectClass=person)($this->loginAttribute=$username))", [$this->loginAttribute]);
246
        $entry        = $this->getFirstEntry($searchResult);
247
        if ($entry) {
248
            $this->userDN = $this->getDn($entry);
249
        } else {
250
            // User not found.
251
            return false;
252
        }
253
254
        // Connect to the LDAP server.
255
        $this->connect($this->dc, $this->port);
256
257
        // Try to authenticate user, but ignore any PHP warnings.
258
        return @ldap_bind($this->resource, $this->userDN, $password);
259
    }
260
261
    /**
262
     * Change the password of the current user. This must be performed over TLS.
263
     * @param string $username User for change password
264
     * @param string $oldPassword The old password
265
     * @param string $newPassword The new password
266
     * @return bool return true if change password is success
267
     * @throws \Exception
268
     */
269
    public function changePasswordAsUser($username, $oldPassword, $newPassword)
270
    {
271
        if (!$this->useTLS) {
272
            $message = 'TLS must be configured on your web server and enabled to change passwords.';
273
            throw new \Exception($message);
274
        }
275
276
        // Open connection with user
277
        if (!$this->auth($username, $oldPassword)) {
278
            return false;
279
        }
280
281
        return $this->changePasswordAsManager($this->userDN, $newPassword);
282
    }
283
284
    /**
285
     * Change the password of the user as manager. This must be performed over TLS.
286
     * @param string $userDN User Distinguished Names (DN) for change password. Ex.: cn=admin,dc=example,dc=com
287
     * @param string $newPassword The new password
288
     * @return bool return true if change password is success
289
     * @throws \Exception
290
     */
291
    public function changePasswordAsManager($userDN, $newPassword)
292
    {
293
        if (!$this->useTLS) {
294
            $message = 'TLS must be configured on your web server and enabled to change passwords.';
295
            throw new \Exception($message);
296
        }
297
298
        // Open connection with manager
299
        $this->open();
300
301
        // Replace passowrd attribute for AD
302
        // The AD password change procedure is modifying the attribute unicodePwd
303
        $modifications['unicodePwd'] = self::encodePassword($newPassword);
304
        return ldap_mod_replace($this->resource, $userDN, $modifications);
305
    }
306
307
    /**
308
     * Closes the current connection.
309
     *
310
     * @return bool
311
     */
312
    public function close()
313
    {
314
        if (is_resource($this->resource)) {
315
            ldap_close($this->resource);
316
        }
317
        return true;
318
    }
319
320
    /**
321
     * Execute ldap search like.
322
     *
323
     * @link http://php.net/manual/en/ref.ldap.php
324
     *
325
     * @param  string $function php LDAP function
326
     * @param  array $params params for execute ldap function
327
     * @return bool|DataReader
328
     */
329
    public function executeQuery($function, $params)
330
    {
331
        $this->open();
332
        $results = [];
333
        $cookie  = '';
334
        $token   = $function . ' - params: ' . LdapHelper::recursive_implode($params, ';');
335
336
        Yii::info($token, 'chrmorandi\ldap\Connection::query');
337
338
        Yii::beginProfile($token, 'chrmorandi\ldap\Connection::query');
339
        do {
340
            if ($this->pageSize > 0) {
341
                $this->setControlPagedResult($cookie);
342
            }
343
344
            // Run the search.
345
            $result = call_user_func($function, $this->resource, ...$params);
346
347
            if ($this->pageSize > 0) {
348
                $this->setControlPagedResultResponse($result, $cookie);
349
            }
350
351
            //Collect each resource result
352
            $results[] = $result;
353
        } while (!is_null($cookie) && !empty($cookie));
354
        Yii::endProfile($token, 'chrmorandi\ldap\Connection::query');
355
356
        return new DataReader($this, $results);
357
    }
358
359
    /**
360
     * Returns true/false if the current connection is bound.
361
     * @return bool
362
     */
363
    public function getBound()
364
    {
365
        return $this->_bound;
366
    }
367
368
    /**
369
     * Get the current resource of connection.
370
     * @return resource
371
     */
372
    public function getResource()
373
    {
374
        return $this->resource;
375
    }
376
377
    /**
378
     * Adds an entry to the current connection.
379
     * @param string $dn
380
     * @param array  $entry
381
     * @return bool
382
     */
383
    public function add($dn, array $entry)
384
    {
385
        return ldap_add($this->resource, $dn, $entry);
386
    }
387
388
    /**
389
     * Deletes an entry on the current connection.
390
     * @param string $dn
391
     * @return bool
392
     */
393
    public function delete($dn)
394
    {
395
        return ldap_delete($this->resource, $dn);
396
    }
397
398
    /**
399
     * Modify the name of an entry on the current connection.
400
     *
401
     * @param string $dn
402
     * @param string $newRdn
403
     * @param string $newParent
404
     * @param bool   $deleteOldRdn
405
     * @return bool
406
     */
407
    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
408
    {
409
        return ldap_rename($this->resource, $dn, $newRdn, $newParent, $deleteOldRdn);
410
    }
411
412
    /**
413
     * Batch modifies an existing entry on the current connection.
414
     * The types of modifications:
415
     *      LDAP_MODIFY_BATCH_ADD - Each value specified through values is added.
416
     *      LDAP_MODIFY_BATCH_REMOVE - Each value specified through values is removed.
417
     *          Any value of the attribute not contained in the values array will remain untouched.
418
     *      LDAP_MODIFY_BATCH_REMOVE_ALL - All values are removed from the attribute named by attrib.
419
     *      LDAP_MODIFY_BATCH_REPLACE - All current values are replaced by new one.
420
     * @param string $dn
421
     * @param array  $values array associative with three keys: "attrib", "modtype" and "values".
422
     * ```php
423
     * [
424
     *     "attrib"  => "attribute",
425
     *     "modtype" => LDAP_MODIFY_BATCH_ADD,
426
     *     "values"  => ["attribute value one"],
427
     * ],
428
     * ```
429
     * @return mixed
430
     */
431
    public function modify($dn, array $values)
432
    {
433
        return ldap_modify_batch($this->resource, $dn, $values);
434
    }
435
436
    /**
437
     * Retrieve the entries from a search result.
438
     * @param resource $searchResult
439
     * @return array|bool
440
     */
441
    public function getEntries($searchResult)
442
    {
443
        return ldap_get_entries($this->resource, $searchResult);
444
    }
445
446
    /**
447
     * Retrieves the number of entries from a search result.
448
     * @param resource $searchResult
449
     * @return int
450
     */
451
    public function countEntries($searchResult)
452
    {
453
        return ldap_count_entries($this->resource, $searchResult);
454
    }
455
456
    /**
457
     * Retrieves the first entry from a search result.
458
     * @param resource $searchResult
459
     * @return resource|false the result entry identifier for the first entry on success and FALSE on error.
460
     */
461
    public function getFirstEntry($searchResult)
462
    {
463
        return ldap_first_entry($this->resource, $searchResult);
464
    }
465
466
    /**
467
     * Retrieves the next entry from a search result.
468
     * @param resource $entry link identifier
469
     * @return resource
470
     */
471
    public function getNextEntry($entry)
472
    {
473
        return ldap_next_entry($this->resource, $entry);
474
    }
475
476
    /**
477
     * Retrieves the ldap first entry attribute.
478
     * @param resource $entry
479
     * @return string
480
     */
481
    public function getFirstAttribute($entry)
482
    {
483
        return ldap_first_attribute($this->resource, $entry);
484
    }
485
486
    /**
487
     * Retrieves the ldap next entry attribute.
488
     * @param resource $entry
489
     * @return string
490
     */
491
    public function getNextAttribute($entry)
492
    {
493
        return ldap_next_attribute($this->resource, $entry);
494
    }
495
496
    /**
497
     * Retrieves the ldap entry's attributes.
498
     * @param resource $entry
499
     * @return array
500
     */
501
    public function getAttributes($entry)
502
    {
503
        return ldap_get_attributes($this->resource, $entry);
504
    }
505
506
    /**
507
     * Retrieves all binary values from a result entry. Individual values are accessed by integer index in the array.
508
     * The first index is 0. The number of values can be found by indexing "count" in the resultant array.
509
     *
510
     * @link https://www.php.net/manual/en/function.ldap-get-values-len.php
511
     *
512
     * @param resource $entry Link identifier
513
     * @param string $attribute Name of attribute
514
     * @return array Returns an array of values for the attribute on success and empty array on error.
515
     */
516
    public function getValuesLen($entry, $attribute)
517
    {
518
        $result = ldap_get_values_len($this->resource, $entry, $attribute);
519
        return ($result == false) ? [] : $result;
520
    }
521
522
    /**
523
     * Retrieves the DN of a result entry.
524
     *
525
     * @link https://www.php.net/manual/en/function.ldap-get-dn.php
526
     *
527
     * @param resource $entry
528
     * @return string
529
     */
530
    public function getDn($entry)
531
    {
532
        return ldap_get_dn($this->resource, $entry);
533
    }
534
535
    /**
536
     * Free result memory.
537
     *
538
     * @link https://www.php.net/manual/en/function.ldap-free-result.php
539
     *
540
     * @param resource $searchResult
541
     * @return bool Returns TRUE on success or FALSE on failure.
542
     */
543
    public function freeResult($searchResult)
544
    {
545
        return ldap_free_result($searchResult);
546
    }
547
548
    /**
549
     * Sets an option on the current connection.
550
     *
551
     * @link https://www.php.net/manual/en/function.ldap-set-option.php
552
     *
553
     * @param int   $option The parameter.
554
     * @param mixed $value The new value for the specified option.
555
     * @return bool Returns TRUE on success or FALSE on failure.
556
     */
557
    public function setOption($option, $value)
558
    {
559
        return ldap_set_option($this->resource, $option, $value);
560
    }
561
562
    /**
563
     * Starts a connection using TLS.
564
     *
565
     * @link https://www.php.net/manual/en/function.ldap-start-tls.php
566
     *
567
     * @return bool
568
     */
569
    public function startTLS()
570
    {
571
        return ldap_start_tls($this->resource);
572
    }
573
574
    /**
575
     * Send LDAP pagination control.
576
     *
577
     * @link http://php.net/manual/en/function.ldap-control-paged-result.php
578
     *
579
     * @param string $cookie An opaque structure sent by the server
580
     * @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.
581
     * @return bool Returns TRUE on success or FALSE on failure.
582
     */
583
    public function setControlPagedResult($cookie = '', $isCritical = false)
584
    {
585
        return ldap_control_paged_result($this->resource, $this->pageSize, $isCritical, $cookie);
586
    }
587
588
    /**
589
     * Retrieve a paginated result response.
590
     *
591
     * @link https://www.php.net/manual/en/function.ldap-control-paged-result-response.php
592
     *
593
     * @param resource $result
594
     * @param string $cookie An opaque structure sent by the server
595
     * @return bool Returns TRUE on success or FALSE on failure.
596
     */
597
    public function setControlPagedResultResponse($result, &$cookie)
598
    {
599
        return ldap_control_paged_result_response($this->resource, $result, $cookie);
600
    }
601
602
    /**
603
     * Return the LDAP error message of the last LDAP command.
604
     *
605
     * @link https://www.php.net/manual/en/function.ldap-error.php
606
     *
607
     * @return string Error message.
608
     */
609
    public function getLastError()
610
    {
611
        return ldap_error($this->resource);
612
    }
613
614
    /**
615
     * Returns the number of the last error on the current connection.
616
     *
617
     * @link https://www.php.net/manual/en/function.ldap-errno.php
618
     *
619
     * @return int Error number
620
     */
621
    public function getErrNo()
622
    {
623
        return ldap_errno($this->resource);
624
    }
625
626
    /**
627
     * Returns the error string of the specified error number.
628
     *
629
     * @link https://www.php.net/manual/en/function.ldap-err2str.php
630
     *
631
     * @param int $number The error number.
632
     * @return string  Error message.
633
     */
634
    public function err2Str($number)
635
    {
636
        return ldap_err2str($number);
637
    }
638
639
}
640