Completed
Push — master ( 52e74d...bce1be )
by Christopher
02:13
created

Connection::changePassword()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
182
        \yii\caching\TagDependency::invalidate($cache, $tags);
183
    }
184
185
    /**
186
     * Connects and Binds to the Domain Controller with a administrator credentials.
187
     * @return void
188
     */
189
    public function open($anonymous = false)
190
    {
191
        $token = 'Opening LDAP connection: ' . LdapUtils::recursive_implode($this->dc, ' or ');
192
        Yii::info($token, __METHOD__);
193
        Yii::beginProfile($token, __METHOD__);
194
        // Connect to the LDAP server.
195
        $this->connect($this->dc, $this->port);
196
        Yii::endProfile($token, __METHOD__);
197
198
        try {
199
            if ($anonymous) {
200
                $this->_bound = ldap_bind($this->resource);
201
            } else {
202
                $this->_bound = ldap_bind($this->resource, $this->username, $this->password);
203
            }
204
        } catch (\Exception $e) {
205
            throw new \Exception('Invalid credential for user manager in ldap.', 0);
206
        }
207
    }
208
209
    /**
210
     * Connection.
211
     * @param string|array $hostname
212
     * @param type $port
213
     * @return void
214
     */
215
    protected function connect($hostname = [], $port = '389')
216
    {
217
        if (is_array($hostname)) {
218
            $hostname = self::PROTOCOL.implode(' '.self::PROTOCOL, $hostname);
219
        }
220
        
221
        $this->close();
222
        $this->resource = ldap_connect($hostname, $port);
223
224
        // Set the LDAP options.     
225
        $this->setOption(LDAP_OPT_PROTOCOL_VERSION, 3);
226
        $this->setOption(LDAP_OPT_REFERRALS, $this->followReferrals);
227
        if ($this->useTLS) {
228
            $this->startTLS();
229
        }
230
231
        $this->trigger(self::EVENT_AFTER_OPEN);
232
    }
233
    
234
    /**
235
     * Authenticate user
236
     * @param string $username
237
     * @param string $password
238
     * @return int indicate occurrence of error. 
239
     */
240
    public function auth($username, $password)
241
    {
242
        // Open connection with manager
243
        $this->open();
244
        
245
        # Search for user and get user DN
246
        $searchResult = ldap_search($this->resource, $this->baseDn, "(&(objectClass=person)($this->loginAttribute=$username))", [$this->loginAttribute]);
247
        $entry = $this->getFirstEntry($searchResult);
248
        if($entry) {
249
            $this->userDn = $this->getDn($entry);        
250
        } else {
251
            $this->userDn = null;
252
        }
253
254
        // Connect to the LDAP server.
255
        $this->connect($this->dc, $this->port);
256
257
        // Authenticate user
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 changePassword($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
        // Open connection with manager
282
        $this->open();
283
        
284
        // Replace passowrd
285
        $modifications[$this->unicodePassword] = self::encodePassword($newPassword);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$modifications was never initialized. Although not strictly required by PHP, it is generally a good practice to add $modifications = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
Bug introduced by
The method encodePassword() does not seem to exist on object<chrmorandi\ldap\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
286
        return ldap_mod_replace($this->resource, $this->userDn, $modifications);
287
    }
288
    
289
    /**
290
     * Closes the current connection.
291
     *
292
     * @return boolean
293
     */
294
    public function close()
295
    {
296
        if (is_resource($this->resource)) {
297
            ldap_close($this->resource);
298
        }
299
        return true;
300
    }
301
302
    /**
303
     * Execute ldap search like.
304
     *
305
     * http://php.net/manual/en/ref.ldap.php
306
     *
307
     * @param  string $function php LDAP function
308
     * @param  array $params params for execute ldap function
309
     * @return bool|DataReader
310
     */
311
    public function executeQuery($function, $params)
312
    {
313
        $this->open();
314
        $results = [];
315
        $cookie = '';        
316
        $token = $function . ' - params: ' . LdapUtils::recursive_implode($params, ';');
317
318
        Yii::info($token , 'chrmorandi\ldap\Connection::query');
319
       
320
        Yii::beginProfile($token, 'chrmorandi\ldap\Connection::query');
321
        do {
322
            if($this->pageSize > 0) {
323
                $this->setControlPagedResult($cookie);
324
            }
325
            
326
            // Run the search.
327
            $result = call_user_func($function, $this->resource, ...$params);
328
            
329
            if($this->pageSize > 0) {
330
                $this->setControlPagedResultResponse($result, $cookie);
331
            }
332
            
333
            //Collect each resource result
334
            $results[] = $result;            
335
        } while (!is_null($cookie) && !empty($cookie));
336
        Yii::endProfile($token, 'chrmorandi\ldap\Connection::query');
337
338
        return new DataReader($this, $results);
339
    }
340
    
341
    /**
342
     * Returns true/false if the current connection is bound.
343
     * @return bool
344
     */
345
    public function getBound()
346
    {
347
        return $this->_bound;
348
    }
349
    
350
    /**
351
     * Get the current resource of connection.
352
     * @return resource
353
     */
354
    public function getResource()
355
    {
356
        return $this->resource;
357
    }
358
    
359
    /**
360
     * Sorts an AD search result by the specified attribute.
361
     * @param resource $result
362
     * @param string   $attribute
363
     * @return bool
364
     */
365
    public function sort($result, $attribute)
366
    {
367
        return ldap_sort($this->resource, $result, $attribute);
368
    }
369
370
    /**
371
     * Adds an entry to the current connection.
372
     * @param string $dn
373
     * @param array  $entry
374
     * @return bool
375
     */
376
    public function add($dn, array $entry)
377
    {
378
        return ldap_add($this->resource, $dn, $entry);
379
    }
380
381
    /**
382
     * Deletes an entry on the current connection.
383
     * @param string $dn
384
     * @return bool
385
     */
386
    public function delete($dn)
387
    {
388
        return ldap_delete($this->resource, $dn);
389
    }
390
391
    /**
392
     * Modify the name of an entry on the current connection.
393
     *
394
     * @param string $dn
395
     * @param string $newRdn
396
     * @param string $newParent
397
     * @param bool   $deleteOldRdn
398
     * @return bool
399
     */
400
    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
401
    {
402
        return ldap_rename($this->resource, $dn, $newRdn, $newParent, $deleteOldRdn);
403
    }
404
405
    /**
406
     * Batch modifies an existing entry on the current connection.
407
     * The types of modifications:
408
     *      LDAP_MODIFY_BATCH_ADD - Each value specified through values is added.
409
     *      LDAP_MODIFY_BATCH_REMOVE - Each value specified through values is removed. 
410
     *          Any value of the attribute not contained in the values array will remain untouched.
411
     *      LDAP_MODIFY_BATCH_REMOVE_ALL - All values are removed from the attribute named by attrib.
412
     *      LDAP_MODIFY_BATCH_REPLACE - All current values are replaced by new one.
413
     * @param string $dn
414
     * @param array  $values array associative with three keys: "attrib", "modtype" and "values".
415
     * ```php
416
     * [
417
     *     "attrib"  => "attribute",
418
     *     "modtype" => LDAP_MODIFY_BATCH_ADD,
419
     *     "values"  => ["attribute value one"],
420
     * ],
421
     * ```
422
     * @return mixed
423
     */
424
    public function modify($dn, array $values)
425
    {
426
        $this->clearCache(DataReader::CACHE_TAG);
427
        return ldap_modify_batch($this->resource, $dn, $values);
428
    }    
429
    
430
    /**
431
     * Retrieve the entries from a search result.
432
     * @param resource $searchResult
433
     * @return array|boolean
434
     */
435
    public function getEntries($searchResult)
436
    {
437
        return ldap_get_entries($this->resource, $searchResult);
438
    }
439
    
440
    /**
441
     * Retrieves the number of entries from a search result.
442
     * @param resource $searchResult
443
     * @return int
444
     */
445
    public function countEntries($searchResult)
446
    {
447
        return ldap_count_entries($this->resource, $searchResult);
448
    }
449
450
    /**
451
     * Retrieves the first entry from a search result.
452
     * @param resource $searchResult
453
     * @return resource link identifier
454
     */
455
    public function getFirstEntry($searchResult)
456
    {
457
        return ldap_first_entry($this->resource, $searchResult);
458
    }
459
460
    /**
461
     * Retrieves the next entry from a search result.
462
     * @param resource $entry link identifier
463
     * @return resource
464
     */
465
    public function getNextEntry($entry)
466
    {
467
        return ldap_next_entry($this->resource, $entry);
468
    }
469
    
470
    /**
471
     * Retrieves the ldap first entry attribute.
472
     * @param resource $entry
473
     * @return string
474
     */
475
    public function getFirstAttribute($entry)
476
    {
477
        return ldap_first_attribute($this->resource, $entry);
478
    }
479
    
480
    /**
481
     * Retrieves the ldap next entry attribute.
482
     * @param resource $entry
483
     * @return string
484
     */
485
    public function getNextAttribute($entry)
486
    {
487
        return ldap_next_attribute($this->resource, $entry);
488
    }
489
490
    /**
491
     * Retrieves the ldap entry's attributes.
492
     * @param resource $entry
493
     * @return array
494
     */
495
    public function getAttributes($entry)
496
    {
497
        return ldap_get_attributes($this->resource, $entry);
498
    }
499
    
500
    /**
501
     * Retrieves all binary values from a result entry.
502
     * @param resource $entry link identifier
503
     * @param string $attribute name of attribute
504
     * @return array
505
     */
506
    public function getValuesLen($entry, $attribute)
507
    {
508
        return ldap_get_values_len($this->resource, $entry, $attribute);
509
    }
510
    
511
    /**
512
     * Retrieves the DN of a result entry.
513
     * @param resource $entry
514
     * @return string
515
     */
516
    public function getDn($entry)
517
    {
518
        return ldap_get_dn($this->resource, $entry);
519
    }
520
521
    /**
522
     * Free result memory.
523
     * @param resource $searchResult
524
     * @return bool
525
     */
526
    public function freeResult($searchResult)
527
    {
528
        return ldap_free_result($searchResult);
529
    }
530
531
    /**
532
     * Sets an option on the current connection.
533
     * @param int   $option
534
     * @param mixed $value
535
     * @return boolean
536
     */
537
    public function setOption($option, $value)
538
    {
539
        return ldap_set_option($this->resource, $option, $value);
540
    }
541
542
    /**
543
     * Starts a connection using TLS.
544
     * @return bool
545
     */
546
    public function startTLS()
547
    {
548
        return ldap_start_tls($this->resource);
549
    }
550
    
551
    /**
552
     * Send LDAP pagination control.
553
     * @param int    $pageSize
0 ignored issues
show
Bug introduced by
There is no parameter named $pageSize. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
554
     * @param bool   $isCritical
0 ignored issues
show
Bug introduced by
There is no parameter named $isCritical. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
555
     * @param string $cookie
556
     * @return bool
557
     */
558
    public function setControlPagedResult($cookie)
559
    {
560
        return ldap_control_paged_result($this->resource, $this->pageSize, false, $cookie);
561
    }
562
563
    /**
564
     * Retrieve a paginated result response.
565
     * @param resource $result
566
     * @param string $cookie
567
     * @return bool
568
     */
569
    public function setControlPagedResultResponse($result, &$cookie)
570
    {
571
        return ldap_control_paged_result_response($this->resource, $result, $cookie);
572
    }
573
       
574
    /**
575
     * Retrieve the last error on the current connection.
576
     * @return string
577
     */
578
    public function getLastError()
579
    {
580
        return ldap_error($this->resource);
581
    }
582
    
583
    /**
584
     * Returns the number of the last error on the current connection.
585
     * @return int
586
     */
587
    public function getErrNo()
588
    {
589
        return ldap_errno($this->resource);
590
    }
591
592
    /**
593
     * Returns the error string of the specified error number.
594
     * @param int $number
595
     * @return string
596
     */
597
    public function err2Str($number)
598
    {
599
        return ldap_err2str($number);
600
    }
601
}
602