Passed
Push — master ( c30585...90dc34 )
by Christopher
02:43
created

Connection::add()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 string
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 string 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 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);
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);
0 ignored issues
show
Bug introduced by
$this->port of type string is incompatible with the type chrmorandi\ldap\type expected by parameter $port of chrmorandi\ldap\Connection::connect(). ( Ignorable by Annotation )

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

192
        $this->connect($this->dc, /** @scrutinizer ignore-type */ $this->port);
Loading history...
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 type $port
0 ignored issues
show
Bug introduced by
The type chrmorandi\ldap\type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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);
0 ignored issues
show
Bug introduced by
$port of type chrmorandi\ldap\type|string is incompatible with the type integer expected by parameter $port of ldap_connect(). ( Ignorable by Annotation )

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

219
        $this->resource = ldap_connect($hostname, /** @scrutinizer ignore-type */ $port);
Loading history...
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) {
0 ignored issues
show
introduced by
$entry is of type resource, thus it always evaluated to false.
Loading history...
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);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$modifications was never initialized. Although not strictly required by PHP, it is generally a good practice to add $modifications = array(); before regardless.
Loading history...
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
     * Sorts an AD search result by the specified attribute.
379
     * @param resource $result
380
     * @param string   $attribute
381
     * @return bool
382
     */
383
    public function sort($result, $attribute)
384
    {
385
        return ldap_sort($this->resource, $result, $attribute);
0 ignored issues
show
Deprecated Code introduced by
The function ldap_sort() has been deprecated: 7.0 ( Ignorable by Annotation )

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

385
        return /** @scrutinizer ignore-deprecated */ ldap_sort($this->resource, $result, $attribute);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
386
    }
387
388
    /**
389
     * Adds an entry to the current connection.
390
     * @param string $dn
391
     * @param array  $entry
392
     * @return bool
393
     */
394
    public function add($dn, array $entry)
395
    {
396
        return ldap_add($this->resource, $dn, $entry);
397
    }
398
399
    /**
400
     * Deletes an entry on the current connection.
401
     * @param string $dn
402
     * @return bool
403
     */
404
    public function delete($dn)
405
    {
406
        return ldap_delete($this->resource, $dn);
407
    }
408
409
    /**
410
     * Modify the name of an entry on the current connection.
411
     *
412
     * @param string $dn
413
     * @param string $newRdn
414
     * @param string $newParent
415
     * @param bool   $deleteOldRdn
416
     * @return bool
417
     */
418
    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
419
    {
420
        return ldap_rename($this->resource, $dn, $newRdn, $newParent, $deleteOldRdn);
421
    }
422
423
    /**
424
     * Batch modifies an existing entry on the current connection.
425
     * The types of modifications:
426
     *      LDAP_MODIFY_BATCH_ADD - Each value specified through values is added.
427
     *      LDAP_MODIFY_BATCH_REMOVE - Each value specified through values is removed.
428
     *          Any value of the attribute not contained in the values array will remain untouched.
429
     *      LDAP_MODIFY_BATCH_REMOVE_ALL - All values are removed from the attribute named by attrib.
430
     *      LDAP_MODIFY_BATCH_REPLACE - All current values are replaced by new one.
431
     * @param string $dn
432
     * @param array  $values array associative with three keys: "attrib", "modtype" and "values".
433
     * ```php
434
     * [
435
     *     "attrib"  => "attribute",
436
     *     "modtype" => LDAP_MODIFY_BATCH_ADD,
437
     *     "values"  => ["attribute value one"],
438
     * ],
439
     * ```
440
     * @return mixed
441
     */
442
    public function modify($dn, array $values)
443
    {
444
        return ldap_modify_batch($this->resource, $dn, $values);
445
    }
446
447
    /**
448
     * Retrieve the entries from a search result.
449
     * @param resource $searchResult
450
     * @return array|bool
451
     */
452
    public function getEntries($searchResult)
453
    {
454
        return ldap_get_entries($this->resource, $searchResult);
455
    }
456
457
    /**
458
     * Retrieves the number of entries from a search result.
459
     * @param resource $searchResult
460
     * @return int
461
     */
462
    public function countEntries($searchResult)
463
    {
464
        return ldap_count_entries($this->resource, $searchResult);
465
    }
466
467
    /**
468
     * Retrieves the first entry from a search result.
469
     * @param resource $searchResult
470
     * @return resource link identifier
471
     */
472
    public function getFirstEntry($searchResult)
473
    {
474
        return ldap_first_entry($this->resource, $searchResult);
475
    }
476
477
    /**
478
     * Retrieves the next entry from a search result.
479
     * @param resource $entry link identifier
480
     * @return resource
481
     */
482
    public function getNextEntry($entry)
483
    {
484
        return ldap_next_entry($this->resource, $entry);
485
    }
486
487
    /**
488
     * Retrieves the ldap first entry attribute.
489
     * @param resource $entry
490
     * @return string
491
     */
492
    public function getFirstAttribute($entry)
493
    {
494
        return ldap_first_attribute($this->resource, $entry);
495
    }
496
497
    /**
498
     * Retrieves the ldap next entry attribute.
499
     * @param resource $entry
500
     * @return string
501
     */
502
    public function getNextAttribute($entry)
503
    {
504
        return ldap_next_attribute($this->resource, $entry);
505
    }
506
507
    /**
508
     * Retrieves the ldap entry's attributes.
509
     * @param resource $entry
510
     * @return array
511
     */
512
    public function getAttributes($entry)
513
    {
514
        return ldap_get_attributes($this->resource, $entry);
515
    }
516
517
    /**
518
     * Retrieves all binary values from a result entry. Individual values are accessed by integer index in the array.
519
     * The first index is 0. The number of values can be found by indexing "count" in the resultant array.
520
     *
521
     * @link https://www.php.net/manual/en/function.ldap-get-values-len.php
522
     *
523
     * @param resource $entry Link identifier
524
     * @param string $attribute Name of attribute
525
     * @return array Returns an array of values for the attribute on success and empty array on error.
526
     */
527
    public function getValuesLen($entry, $attribute)
528
    {
529
        $result = ldap_get_values_len($this->resource, $entry, $attribute);
530
        return is_array($result) ? $result : [];
0 ignored issues
show
introduced by
The condition is_array($result) is always true.
Loading history...
531
    }
532
533
    /**
534
     * Retrieves the DN of a result entry.
535
     *
536
     * @link https://www.php.net/manual/en/function.ldap-get-dn.php
537
     *
538
     * @param resource $entry
539
     * @return string
540
     */
541
    public function getDn($entry)
542
    {
543
        return ldap_get_dn($this->resource, $entry);
544
    }
545
546
    /**
547
     * Free result memory.
548
     *
549
     * @link https://www.php.net/manual/en/function.ldap-free-result.php
550
     *
551
     * @param resource $searchResult
552
     * @return bool Returns TRUE on success or FALSE on failure.
553
     */
554
    public function freeResult($searchResult)
555
    {
556
        return ldap_free_result($searchResult);
557
    }
558
559
    /**
560
     * Sets an option on the current connection.
561
     *
562
     * @link https://www.php.net/manual/en/function.ldap-set-option.php
563
     *
564
     * @param int   $option The parameter.
565
     * @param mixed $value The new value for the specified option.
566
     * @return bool Returns TRUE on success or FALSE on failure.
567
     */
568
    public function setOption($option, $value)
569
    {
570
        return ldap_set_option($this->resource, $option, $value);
571
    }
572
573
    /**
574
     * Starts a connection using TLS.
575
     *
576
     * @link https://www.php.net/manual/en/function.ldap-start-tls.php
577
     *
578
     * @return bool
579
     */
580
    public function startTLS()
581
    {
582
        return ldap_start_tls($this->resource);
583
    }
584
585
    /**
586
     * Send LDAP pagination control.
587
     *
588
     * @link http://php.net/manual/en/function.ldap-control-paged-result.php
589
     *
590
     * @param string $cookie An opaque structure sent by the server
591
     * @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.
592
     * @return bool Returns TRUE on success or FALSE on failure.
593
     */
594
    public function setControlPagedResult($cookie = '', $isCritical = false)
595
    {
596
        return ldap_control_paged_result($this->resource, $this->pageSize, $isCritical, $cookie);
597
    }
598
599
    /**
600
     * Retrieve a paginated result response.
601
     *
602
     * @link https://www.php.net/manual/en/function.ldap-control-paged-result-response.php
603
     *
604
     * @param resource $result
605
     * @param string $cookie An opaque structure sent by the server
606
     * @return bool Returns TRUE on success or FALSE on failure.
607
     */
608
    public function setControlPagedResultResponse($result, &$cookie)
609
    {
610
        return ldap_control_paged_result_response($this->resource, $result, $cookie);
611
    }
612
613
    /**
614
     * Return the LDAP error message of the last LDAP command.
615
     *
616
     * @link https://www.php.net/manual/en/function.ldap-error.php
617
     *
618
     * @return string Error message.
619
     */
620
    public function getLastError()
621
    {
622
        return ldap_error($this->resource);
623
    }
624
625
    /**
626
     * Returns the number of the last error on the current connection.
627
     *
628
     * @link https://www.php.net/manual/en/function.ldap-errno.php
629
     *
630
     * @return int Error number
631
     */
632
    public function getErrNo()
633
    {
634
        return ldap_errno($this->resource);
635
    }
636
637
    /**
638
     * Returns the error string of the specified error number.
639
     *
640
     * @link https://www.php.net/manual/en/function.ldap-err2str.php
641
     *
642
     * @param int $number The error number.
643
     * @return string  Error message.
644
     */
645
    public function err2Str($number)
646
    {
647
        return ldap_err2str($number);
648
    }
649
650
}
651