Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 1071
0.00% covered (danger)
0.00%
0 / 58
CRAP
0.00% covered (danger)
0.00%
0 / 1
LdapAuthenticationPlugin
0.00% covered (danger)
0.00%
0 / 1071
0.00% covered (danger)
0.00%
0 / 58
98282
0.00% covered (danger)
0.00%
0 / 1
 getInstance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 ldap_connect
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_bind
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_unbind
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 ldap_modify
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_add
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_delete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_search
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_read
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_list
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_get_entries
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_count_entries
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ldap_errno
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getConf
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 1
2162
 setOrDefault
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setOrDefaultPrivate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userExists
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 userExistsReal
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 connect
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
156
 authenticate
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
342
 markAuthFailed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 domainList
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 autoCreate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPassword
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
56
 updateExternalDB
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
306
 canCreateAccounts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 allowPasswordChange
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 allowSetLocalPassword
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addUser
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 1
650
 setDomain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDomain
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 validDomain
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 updateUser
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 initUser
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 strict
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getCanonicalName
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
72
 autoAuthSetup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchString
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getUserDN
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
56
 getUserInfo
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getUserInfoStateless
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 getPreferences
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
90
 checkGroups
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 getGroups
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
380
 searchNestedGroups
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 searchGroups
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
240
 hasLDAPGroup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isLDAPGroup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setGroups
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
110
 getPasswordHash
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 printDebug
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 bindAs
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 unbind
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 useAutoAuth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLdapEscapedString
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getBaseDN
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 loadDomain
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 saveDomain
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Copyright (C) 2004 Ryan Lane <https://www.mediawiki.org/wiki/User:Ryan_lane>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 */
20
21use MediaWiki\Context\RequestContext;
22use MediaWiki\Extension\LdapAuthentication\Hooks\HookRunner;
23use MediaWiki\Extension\LdapAuthentication\LdapAuthenticationException;
24use MediaWiki\MediaWikiServices;
25use MediaWiki\User\User;
26use Wikimedia\AtEase\AtEase;
27
28class LdapAuthenticationPlugin {
29
30    /** @var self|null */
31    private static $instance = null;
32
33    /** @var resource|null ldap connection resource */
34    public $ldapconn;
35
36    /** @var string|null */
37    public $email;
38    /** @var string|null */
39    public $lang;
40    /** @var string|null */
41    public $realname;
42    /** @var string|null */
43    public $nickname;
44    /** @var string|null */
45    public $externalid;
46
47    /** @var string username pulled from ldap */
48    public $LDAPUsername = '';
49
50    /** @var string userdn pulled from ldap */
51    public $userdn = '';
52
53    /** @var string[][] groups pulled from ldap */
54    public $userLDAPGroups = [];
55    /** @var string[][] groups pulled from ldap */
56    public $allLDAPGroups = [];
57
58    /** @var bool|null to test for failed auth */
59    public $authFailed;
60
61    /** @var bool|null to test for fetched user info */
62    public $fetchedUserInfo;
63
64    /** @var array|null the user's entry and all attributes */
65    public $userInfo;
66
67    /** @var string|null the user we are currently bound as */
68    public $boundAs;
69
70    /**
71     * Fetch the singleton instance of LdapAuthenticationPlugin
72     * @return self
73     */
74    public static function getInstance() {
75        if ( self::$instance === null ) {
76            self::$instance = new LdapAuthenticationPlugin;
77        }
78        return self::$instance;
79    }
80
81    /**
82     * Wrapper for ldap_connect
83     * @param string|null $hostname
84     * @param int $port
85     * @return resource|false
86     */
87    public static function ldap_connect( $hostname = null, $port = 389 ) {
88        AtEase::suppressWarnings();
89        $ret = ldap_connect( $hostname, $port );
90        AtEase::restoreWarnings();
91        return $ret;
92    }
93
94    /**
95     * Wrapper for ldap_bind
96     * @param resource $ldapconn
97     * @param string|null $dn
98     * @param string|null $password
99     * @return bool
100     */
101    public static function ldap_bind( $ldapconn, $dn = null, $password = null ) {
102        AtEase::suppressWarnings();
103        $ret = ldap_bind( $ldapconn, $dn, $password );
104        AtEase::restoreWarnings();
105        return $ret;
106    }
107
108    /**
109     * Wrapper for ldap_unbind
110     * @param resource $ldapconn
111     * @return bool
112     */
113    public static function ldap_unbind( $ldapconn ) {
114        if ( $ldapconn ) {
115            AtEase::suppressWarnings();
116            $ret = ldap_unbind( $ldapconn );
117            AtEase::restoreWarnings();
118        } else {
119            $ret = false;
120        }
121        return $ret;
122    }
123
124    /**
125     * Wrapper for ldap_modify
126     * @param resource $ldapconn
127     * @param string $dn
128     * @param array $entry
129     * @return bool
130     */
131    public static function ldap_modify( $ldapconn, $dn, $entry ) {
132        AtEase::suppressWarnings();
133        $ret = ldap_modify( $ldapconn, $dn, $entry );
134        AtEase::restoreWarnings();
135        return $ret;
136    }
137
138    /**
139     * Wrapper for ldap_add
140     * @param resource $ldapconn
141     * @param string $dn
142     * @param array $entry
143     * @return bool
144     */
145    public static function ldap_add( $ldapconn, $dn, $entry ) {
146        AtEase::suppressWarnings();
147        $ret = ldap_add( $ldapconn, $dn, $entry );
148        AtEase::restoreWarnings();
149        return $ret;
150    }
151
152    /**
153     * Wrapper for ldap_delete
154     * @param resource $ldapconn
155     * @param string $dn
156     * @return bool
157     */
158    public static function ldap_delete( $ldapconn, $dn ) {
159        AtEase::suppressWarnings();
160        $ret = ldap_delete( $ldapconn, $dn );
161        AtEase::restoreWarnings();
162        return $ret;
163    }
164
165    /**
166     * Wrapper for ldap_search
167     * @param resource $ldapconn
168     * @param string $basedn
169     * @param string $filter
170     * @param array|null $attributes
171     * @param int|null $attrsonly
172     * @param int|null $sizelimit
173     * @param int|null $timelimit
174     * @param int|null $deref
175     * @return resource
176     */
177    public static function ldap_search(
178        $ldapconn,
179        $basedn,
180        $filter,
181        $attributes = [],
182        $attrsonly = null,
183        $sizelimit = null,
184        $timelimit = null,
185        $deref = null
186    ) {
187        AtEase::suppressWarnings();
188        $ret = ldap_search(
189            $ldapconn,
190            $basedn,
191            $filter,
192            $attributes,
193            $attrsonly,
194            $sizelimit,
195            $timelimit,
196            $deref
197        );
198        AtEase::restoreWarnings();
199        return $ret;
200    }
201
202    /**
203     * Wrapper for ldap_read
204     * @param resource $ldapconn
205     * @param string $basedn
206     * @param string $filter
207     * @param array|null $attributes
208     * @param int|null $attrsonly
209     * @param int|null $sizelimit
210     * @param int|null $timelimit
211     * @param int|null $deref
212     * @return resource
213     */
214    public static function ldap_read(
215        $ldapconn,
216        $basedn,
217        $filter,
218        $attributes = [],
219        $attrsonly = null,
220        $sizelimit = null,
221        $timelimit = null,
222        $deref = null
223    ) {
224        AtEase::suppressWarnings();
225        $ret = ldap_read(
226            $ldapconn,
227            $basedn,
228            $filter,
229            $attributes,
230            $attrsonly,
231            $sizelimit,
232            $timelimit,
233            $deref
234        );
235        AtEase::restoreWarnings();
236        return $ret;
237    }
238
239    /**
240     * Wrapper for ldap_list
241     * @param resource $ldapconn
242     * @param string $basedn
243     * @param string $filter
244     * @param array|null $attributes
245     * @param int|null $attrsonly
246     * @param int|null $sizelimit
247     * @param int|null $timelimit
248     * @param int|null $deref
249     * @return resource
250     */
251    public static function ldap_list(
252        $ldapconn,
253        $basedn,
254        $filter,
255        $attributes = [],
256        $attrsonly = null,
257        $sizelimit = null,
258        $timelimit = null,
259        $deref = null
260    ) {
261        AtEase::suppressWarnings();
262        $ret = ldap_list(
263            $ldapconn,
264            $basedn,
265            $filter,
266            $attributes,
267            $attrsonly,
268            $sizelimit,
269            $timelimit,
270            $deref
271        );
272        AtEase::restoreWarnings();
273        return $ret;
274    }
275
276    /**
277     * Wrapper for ldap_get_entries
278     * @param resource $ldapconn
279     * @param resource $resultid
280     * @return array
281     * @phan-return array<int|string,int|array<int|string,string|int|array<int|string,int|string>>>
282     */
283    public static function ldap_get_entries( $ldapconn, $resultid ) {
284        AtEase::suppressWarnings();
285        $ret = ldap_get_entries( $ldapconn, $resultid );
286        AtEase::restoreWarnings();
287        return $ret;
288    }
289
290    /**
291     * Wrapper for ldap_count_entries
292     * @param resource $ldapconn
293     * @param resource $resultid
294     * @return int
295     */
296    public static function ldap_count_entries( $ldapconn, $resultid ) {
297        AtEase::suppressWarnings();
298        $ret = ldap_count_entries( $ldapconn, $resultid );
299        AtEase::restoreWarnings();
300        return $ret;
301    }
302
303    /**
304     * Wrapper for ldap_errno
305     * @param resource $ldapconn
306     * @return int
307     */
308    public static function ldap_errno( $ldapconn ) {
309        AtEase::suppressWarnings();
310        $ret = ldap_errno( $ldapconn );
311        AtEase::restoreWarnings();
312        return $ret;
313    }
314
315    /**
316     * Get configuration defined by admin, or return default value
317     *
318     * @param string $preference
319     * @param string $domain
320     * @return mixed
321     */
322    public function getConf( $preference, $domain = '' ) {
323        # Global preferences
324        switch ( $preference ) {
325            case 'DomainNames':
326                global $wgLDAPDomainNames;
327                return $wgLDAPDomainNames;
328            case 'UseLocal':
329                global $wgLDAPUseLocal;
330                return $wgLDAPUseLocal;
331            case 'AutoAuthUsername':
332                global $wgLDAPAutoAuthUsername;
333                return $wgLDAPAutoAuthUsername;
334            case 'AutoAuthDomain':
335                global $wgLDAPAutoAuthDomain;
336                return $wgLDAPAutoAuthDomain;
337            case 'LockOnBlock':
338                global $wgLDAPLockOnBlock;
339                return $wgLDAPLockOnBlock;
340            case 'LDAPLockPasswordPolicy':
341                global $wgLDAPLockPasswordPolicy;
342                return $wgLDAPLockPasswordPolicy;
343        }
344
345        # Domain specific preferences
346        if ( !$domain ) {
347            $domain = $this->getDomain();
348        }
349        switch ( $preference ) {
350            case 'ServerNames':
351                global $wgLDAPServerNames;
352                return self::setOrDefault( $wgLDAPServerNames, $domain );
353            case 'EncryptionType':
354                global $wgLDAPEncryptionType;
355                return self::setOrDefault( $wgLDAPEncryptionType, $domain, 'tls' );
356            case 'Options':
357                global $wgLDAPOptions;
358                return self::setOrDefault( $wgLDAPOptions, $domain, [] );
359            case 'Port':
360                global $wgLDAPPort;
361                if ( isset( $wgLDAPPort[$domain] ) ) {
362                    $this->printDebug( "Using non-standard port: " . $wgLDAPPort[$domain], SENSITIVE );
363                    return (string)$wgLDAPPort[$domain];
364                }
365
366                if ( $this->getConf( 'EncryptionType' ) == 'ssl' ) {
367                    return "636";
368                }
369
370                return "389";
371            case 'SearchString':
372                global $wgLDAPSearchStrings;
373                return self::setOrDefault( $wgLDAPSearchStrings, $domain );
374            case 'ProxyAgent':
375                global $wgLDAPProxyAgent;
376                return self::setOrDefault( $wgLDAPProxyAgent, $domain );
377            case 'ProxyAgentPassword':
378                global $wgLDAPProxyAgentPassword;
379                return self::setOrDefaultPrivate( $wgLDAPProxyAgentPassword, $domain );
380            case 'SearchAttribute':
381                global $wgLDAPSearchAttributes;
382                return self::setOrDefault( $wgLDAPSearchAttributes, $domain );
383            case 'BaseDN':
384                global $wgLDAPBaseDNs;
385                return self::setOrDefault( $wgLDAPBaseDNs, $domain );
386            case 'GroupBaseDN':
387                global $wgLDAPGroupBaseDNs;
388                return self::setOrDefault( $wgLDAPGroupBaseDNs, $domain );
389            case 'UserBaseDN':
390                global $wgLDAPUserBaseDNs;
391                return self::setOrDefault( $wgLDAPUserBaseDNs, $domain );
392            case 'WriterDN':
393                global $wgLDAPWriterDN;
394                return self::setOrDefault( $wgLDAPWriterDN, $domain );
395            case 'WriterPassword':
396                global $wgLDAPWriterPassword;
397                return self::setOrDefaultPrivate( $wgLDAPWriterPassword, $domain );
398            case 'WriteLocation':
399                global $wgLDAPWriteLocation;
400                return self::setOrDefault( $wgLDAPWriteLocation, $domain );
401            case 'AddLDAPUsers':
402                global $wgLDAPAddLDAPUsers;
403                return self::setOrDefault( $wgLDAPAddLDAPUsers, $domain, false );
404            case 'UpdateLDAP':
405                global $wgLDAPUpdateLDAP;
406                return self::setOrDefault( $wgLDAPUpdateLDAP, $domain, false );
407            case 'PasswordHash':
408                global $wgLDAPPasswordHash;
409                return self::setOrDefaultPrivate( $wgLDAPPasswordHash, $domain, 'clear' );
410            case 'MailPassword':
411                global $wgLDAPMailPassword;
412                return self::setOrDefaultPrivate( $wgLDAPMailPassword, $domain, false );
413            case 'Preferences':
414                global $wgLDAPPreferences;
415                return self::setOrDefault( $wgLDAPPreferences, $domain, [] );
416            case 'DisableAutoCreate':
417                global $wgLDAPDisableAutoCreate;
418                return self::setOrDefault( $wgLDAPDisableAutoCreate, $domain, false );
419            case 'GroupUseFullDN':
420                global $wgLDAPGroupUseFullDN;
421                return self::setOrDefault( $wgLDAPGroupUseFullDN, $domain, false );
422            case 'LowerCaseUsername':
423                global $wgLDAPLowerCaseUsername;
424                // Default set to true for backwards compatibility with
425                // versions < 2.0a
426                return self::setOrDefault( $wgLDAPLowerCaseUsername, $domain, true );
427            case 'GroupUseRetrievedUsername':
428                global $wgLDAPGroupUseRetrievedUsername;
429                return self::setOrDefault( $wgLDAPGroupUseRetrievedUsername, $domain, false );
430            case 'GroupObjectclass':
431                global $wgLDAPGroupObjectclass;
432                return self::setOrDefault( $wgLDAPGroupObjectclass, $domain );
433            case 'GroupAttribute':
434                global $wgLDAPGroupAttribute;
435                return self::setOrDefault( $wgLDAPGroupAttribute, $domain );
436            case 'GroupNameAttribute':
437                global $wgLDAPGroupNameAttribute;
438                return self::setOrDefault( $wgLDAPGroupNameAttribute, $domain );
439            case 'GroupsUseMemberOf':
440                global $wgLDAPGroupsUseMemberOf;
441                return self::setOrDefault( $wgLDAPGroupsUseMemberOf, $domain, false );
442            case 'UseLDAPGroups':
443                global $wgLDAPUseLDAPGroups;
444                return self::setOrDefault( $wgLDAPUseLDAPGroups, $domain, false );
445            case 'LocallyManagedGroups':
446                global $wgLDAPLocallyManagedGroups;
447                return self::setOrDefault( $wgLDAPLocallyManagedGroups, $domain, [] );
448            case 'GroupsPrevail':
449                global $wgLDAPGroupsPrevail;
450                return self::setOrDefault( $wgLDAPGroupsPrevail, $domain, false );
451            case 'RequiredGroups':
452                global $wgLDAPRequiredGroups;
453                return self::setOrDefault( $wgLDAPRequiredGroups, $domain, [] );
454            case 'ExcludedGroups':
455                global $wgLDAPExcludedGroups;
456                return self::setOrDefault( $wgLDAPExcludedGroups, $domain, [] );
457            case 'GroupSearchNestedGroups':
458                global $wgLDAPGroupSearchNestedGroups;
459                return self::setOrDefault( $wgLDAPGroupSearchNestedGroups, $domain, false );
460            case 'AuthAttribute':
461                global $wgLDAPAuthAttribute;
462                return self::setOrDefault( $wgLDAPAuthAttribute, $domain );
463            case 'ActiveDirectory':
464                global $wgLDAPActiveDirectory;
465                return self::setOrDefault( $wgLDAPActiveDirectory, $domain, false );
466            case 'GroupSearchPosixPrimaryGroup':
467                global $wgLDAPGroupSearchPosixPrimaryGroup;
468                return self::setOrDefault( $wgLDAPGroupSearchPosixPrimaryGroup, $domain, false );
469        }
470        return '';
471    }
472
473    /**
474     * Returns the item from $array at index $key if it is set,
475     * else, it returns $default
476     *
477     * @param array $array
478     * @param string $key
479     * @param mixed $default
480     * @return mixed
481     */
482    private static function setOrDefault( $array, $key, $default = '' ) {
483        return $array[$key] ?? $default;
484    }
485
486    /**
487     * Returns the item from $array at index $key if it is set,
488     * else, it returns $default
489     *
490     * Use for sensitive data
491     *
492     * @param array $array
493     * @param string $key
494     * @param mixed $default
495     * @return mixed
496     */
497    private static function setOrDefaultPrivate( $array, $key, $default = '' ) {
498        return $array[$key] ?? $default;
499    }
500
501    /**
502     * Check whether there exists a user account with the given name.
503     * The name will be normalized to MediaWiki's requirements, so
504     * you might need to munge it (for instance, for lowercase initial
505     * letters).
506     *
507     * @param string $username
508     * @return bool
509     */
510    public function userExists( $username ) {
511        $this->printDebug( "Entering userExists", NONSENSITIVE );
512
513        // If we can't add LDAP users, we don't really need to check
514        // if the user exists, the authenticate method will do this for
515        // us. This will decrease hits to the LDAP server.
516        // We do however, need to use this if we are using auto authentication.
517        if ( !$this->getConf( 'AddLDAPUsers' ) && !$this->useAutoAuth() ) {
518            return true;
519        }
520
521        return $this->userExistsReal( $username );
522    }
523
524    /**
525     * Like self::userExists, but always does the check
526     * @see self::userExists()
527     * @param string $username
528     * @return bool
529     */
530    public function userExistsReal( $username ) {
531        $this->printDebug( "Entering userExistsReal", NONSENSITIVE );
532
533        $ret = false;
534        if ( $this->connect() ) {
535            $searchstring = $this->getSearchString( $username );
536
537            if ( $searchstring == '' ) {
538                // It is possible that getSearchString will return an
539                // empty string, which means "no user".
540            } elseif ( $this->useAutoAuth() ) {
541                // If we are using auto authentication, and we got
542                // anything back, then the user exists.
543                $ret = true;
544            } else {
545                // Search for the entry.
546                $entry = self::ldap_read(
547                    $this->ldapconn, $searchstring, "objectclass=*"
548                );
549                if ( $entry &&
550                    self::ldap_count_entries( $this->ldapconn, $entry ) > 0
551                ) {
552                    $this->printDebug( "Found a matching user in LDAP", NONSENSITIVE );
553                    $ret = true;
554                } else {
555                    $this->printDebug( "Did not find a matching user in LDAP", NONSENSITIVE );
556                }
557            }
558            // getSearchString is going to bind, but will not unbind
559            $this->unbind();
560        }
561        return $ret;
562    }
563
564    /**
565     * Connect to LDAP
566     * @param string $domain
567     * @return true
568     */
569    public function connect( $domain = '' ): bool {
570        $this->printDebug( "Entering Connect", NONSENSITIVE );
571
572        if ( !function_exists( 'ldap_connect' ) ) {
573            throw new LdapAuthenticationException( "Missing PHP LDAP support" );
574        }
575
576        // Set the server string depending on whether we use ssl or not
577        $encryptionType = $this->getConf( 'EncryptionType', $domain );
578        switch ( $encryptionType ) {
579            case "ldapi":
580                $this->printDebug( "Using ldapi", SENSITIVE );
581                $serverpre = "ldapi://";
582                break;
583            case "ssl":
584                $this->printDebug( "Using SSL", SENSITIVE );
585                $serverpre = "ldaps://";
586                break;
587            default:
588                $this->printDebug( "Using TLS or not using encryption.", SENSITIVE );
589                $serverpre = "ldap://";
590        }
591
592        // Make a space separated list of server strings with the connection type
593        // string added.
594        $servers = "";
595        $tmpservers = $this->getConf( 'ServerNames', $domain );
596        $tok = strtok( $tmpservers, " " );
597        while ( $tok ) {
598            $servers = $servers . " " . $serverpre . $tok . ":" . $this->getConf( 'Port', $domain );
599            $tok = strtok( " " );
600        }
601        $servers = trim( $servers );
602        if ( !$servers ) {
603            throw new LdapAuthenticationException( 'No servers configured' );
604        }
605
606        $this->printDebug( "Using servers: $servers", SENSITIVE );
607
608        // Connect and set options
609        $this->ldapconn = self::ldap_connect( $servers );
610        if ( !$this->ldapconn ) {
611            throw new LdapAuthenticationException( 'Failed to connect to the LDAP server' );
612        }
613
614        ldap_set_option( $this->ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3 );
615        ldap_set_option( $this->ldapconn, LDAP_OPT_REFERRALS, 0 );
616
617        foreach ( $this->getConf( 'Options' )  as $key => $value ) {
618            if ( !ldap_set_option( $this->ldapconn, constant( $key ), $value ) ) {
619                $this->printDebug(
620                    "Can't set option to LDAP! Option code and value: " . $key . "=" . $value, 1
621                );
622            }
623        }
624
625        // TLS needs to be started after the connection resource is available
626        if ( $encryptionType == "tls" ) {
627            $this->printDebug( "Using TLS", SENSITIVE );
628            if ( !ldap_start_tls( $this->ldapconn ) ) {
629                throw new LdapAuthenticationException( 'Failed to enable TLS on the LDAP connection' );
630            }
631        }
632        $this->printDebug( "PHP's LDAP connect method returned true (note, this does not imply " .
633            "it connected to the server).", NONSENSITIVE );
634
635        // TODO: this method currently just throws exceptions if this fails, so
636        // we should be able to remove this return value
637        return true;
638    }
639
640    /**
641     * Check if a username+password pair is a valid login, or if the username
642     * is allowed access to the wiki.
643     * The name will be normalized to MediaWiki's requirements, so
644     * you might need to munge it (for instance, for lowercase initial
645     * letters).
646     *
647     * @param string $username
648     * @param string $password
649     * @return bool
650     */
651    public function authenticate( $username, $password = '' ) {
652        $this->printDebug( "Entering authenticate for username $username", NONSENSITIVE );
653
654        // We don't handle local authentication
655        if ( $this->getDomain() == 'local' ) {
656            $this->printDebug( "User is using a local domain", SENSITIVE );
657            return false;
658        }
659
660        // MediaWiki munges the username before authenticate is called,
661        // this can mess with authentication, group pulling/restriction,
662        // preference pulling, etc. Let's allow the admin to use
663        // a lowercased username if needed.
664        if ( $this->getConf( 'LowerCaseUsername' ) ) {
665            $username = strtolower( $username );
666        }
667
668        // If the user is using auto authentication, we need to ensure
669        // that he/she isn't trying to fool us by sending a username other
670        // than the one the web server got from the auto-authentication method.
671        if ( $this->useAutoAuth() && $this->getConf( 'AutoAuthUsername' ) != $username ) {
672            $this->printDebug( "The username provided ($username) doesn't match the username " .
673                "provided by the webserver (" . $this->getConf( 'AutoAuthUsername' ) . "). " .
674                "The user is probably trying to log in to the auto-authentication domain with " .
675                "password authentication via the wiki. Denying access.", SENSITIVE );
676            return false;
677        }
678
679        // We need to ensure that if we require a password, that it is
680        // not blank. We don't allow blank passwords, so we are being
681        // tricked if someone is supplying one when using password auth.
682        // auto-authentication is handled by the webserver; a blank password
683        // here is wanted.
684        if ( $password == '' && !$this->useAutoAuth() ) {
685            $this->printDebug( "User used a blank password", NONSENSITIVE );
686            return false;
687        }
688
689        if ( $this->connect() ) {
690            $this->userdn = $this->getSearchString( $username );
691
692            // It is possible that getSearchString will return an
693            // empty string; if this happens, the bind will ALWAYS
694            // return true, and will let anyone in!
695            if ( $this->userdn == '' ) {
696                $this->printDebug( "User DN is blank", NONSENSITIVE );
697                $this->unbind();
698                $this->markAuthFailed();
699                return false;
700            }
701
702            // If we are using password authentication, we need to bind as the
703            // user to make sure the password is correct.
704            if ( !$this->useAutoAuth() ) {
705                $this->printDebug( "Binding as the user", NONSENSITIVE );
706                $bind = $this->bindAs( $this->userdn, $password );
707                if ( !$bind ) {
708                    $this->markAuthFailed();
709                    return false;
710                }
711                $result = true;
712                ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
713                    ->onChainAuth( $username, $password, $result );
714                if ( !$result ) {
715                    return false;
716                }
717
718                $this->printDebug( "Bound successfully", NONSENSITIVE );
719
720                $ss = $this->getConf( 'SearchString' );
721                if ( $ss ) {
722                    if ( strstr( $ss, "@" ) || strstr( $ss, '\\' ) ) {
723                        // We are most likely configured using USER-NAME@DOMAIN, or
724                        // DOMAIN\\USER-NAME.
725                        // Get the user's full DN so we can search for groups and such.
726                        $this->userdn = $this->getUserDN( $username );
727                        $this->printDebug( "Fetched UserDN: $this->userdn", NONSENSITIVE );
728                    } else {
729                        // Now that we are bound, we can pull the user's info.
730                        $this->getUserInfo();
731                    }
732                }
733            }
734
735            // Ensure the user's entry has the required auth attribute
736            $aa = $this->getConf( 'AuthAttribute' );
737            if ( $aa ) {
738                $this->printDebug( "Checking for auth attributes: $aa", NONSENSITIVE );
739                $filter = "(" . $aa . ")";
740                $attributes = [ "dn" ];
741                $entry = self::ldap_read(
742                    $this->ldapconn, $this->userdn, $filter, $attributes
743                );
744                $info = self::ldap_get_entries( $this->ldapconn, $entry );
745                if ( $info["count"] < 1 ) {
746                    $this->printDebug( "Failed auth attribute check", NONSENSITIVE );
747                    $this->unbind();
748                    $this->markAuthFailed();
749                    return false;
750                }
751            }
752
753            $this->getGroups( $username );
754
755            if ( !$this->checkGroups() ) {
756                $this->unbind();
757                $this->markAuthFailed();
758                return false;
759            }
760
761            $this->getPreferences();
762            $this->unbind();
763        } else {
764            $this->markAuthFailed();
765            return false;
766        }
767        $this->printDebug( "Authentication passed", NONSENSITIVE );
768
769        // We made it this far; the user authenticated and didn't fail any checks, so he/she gets in
770        return true;
771    }
772
773    public function markAuthFailed() {
774        $this->authFailed = true;
775    }
776
777    /**
778     * @return array
779     */
780    public function domainList() {
781        $tempDomArr = $this->getConf( 'DomainNames' );
782        if ( $this->getConf( 'UseLocal' ) ) {
783            $this->printDebug( "Allowing the local domain, adding it to the list.", NONSENSITIVE );
784            array_push( $tempDomArr, 'local' );
785        }
786
787        if ( $this->getConf( 'AutoAuthDomain' ) ) {
788            $this->printDebug(
789                "Allowing auto-authentication login, removing the domain from the list.",
790                NONSENSITIVE
791            );
792            // There is no reason for people to log in directly to the wiki if the are using an
793            // auto-authentication domain. If they try to, they are probably up to something fishy.
794            unset( $tempDomArr[array_search( $this->getConf( 'AutoAuthDomain' ), $tempDomArr )] );
795        }
796
797        $domains = [];
798        foreach ( $tempDomArr as $tempDom ) {
799            $domains["$tempDom"] = $tempDom;
800        }
801        return $domains;
802    }
803
804    /**
805     * Return true if the wiki should create a new local account automatically
806     * when asked to login a user who doesn't exist locally but does in the
807     * external auth database.
808     *
809     * This is just a question, and shouldn't perform any actions.
810     *
811     * @return bool
812     */
813    public function autoCreate() {
814        return !$this->getConf( 'DisableAutoCreate' );
815    }
816
817    /**
818     * Set the given password in LDAP.
819     * Return true if successful.
820     *
821     * @param User $user
822     * @param string|null $password
823     * @return bool
824     */
825    public function setPassword( $user, $password ) {
826        $this->printDebug( "Entering setPassword", NONSENSITIVE );
827
828        if ( $this->getDomain() == 'local' ) {
829            $this->printDebug( "User is using a local domain", NONSENSITIVE );
830
831            // We don't set local passwords, but we don't want the wiki
832            // to send the user a failure.
833            return true;
834        }
835        if ( !$this->getConf( 'UpdateLDAP' ) ) {
836            $this->printDebug( "Wiki is set to not allow updates", NONSENSITIVE );
837
838            // We aren't allowing the user to change his/her own password
839            return false;
840        }
841
842        $writer = $this->getConf( 'WriterDN' );
843        if ( !$writer ) {
844            $this->printDebug( "Wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE );
845
846            // We can't change a user's password without an account that is
847            // allowed to do it.
848            return false;
849        }
850        $pass = $this->getPasswordHash( $password );
851
852        if ( $this->connect() ) {
853            $this->userdn = $this->getSearchString( $user->getName() );
854            $this->printDebug( "Binding as the writerDN", NONSENSITIVE );
855            $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) );
856            if ( !$bind ) {
857                return false;
858            }
859            $values = [ 'userpassword' => $pass ];
860
861            // Blank out the password in the database. We don't want to save
862            // domain credentials for security reasons.
863            // This doesn't do anything. $password isn't by reference
864            $password = '';
865
866            $success = self::ldap_modify(
867                $this->ldapconn, $this->userdn, $values
868            );
869            $this->unbind();
870            if ( $success ) {
871                $this->printDebug( "Successfully modified the user's password", NONSENSITIVE );
872                return true;
873            }
874            $this->printDebug( "Failed to modify the user's password", NONSENSITIVE );
875        }
876        return false;
877    }
878
879    /**
880     * Update user information in LDAP
881     * Return true if successful.
882     *
883     * @param User $user
884     * @return bool
885     */
886    public function updateExternalDB( $user ) {
887        $this->printDebug( "Entering updateExternalDB", NONSENSITIVE );
888        if ( !$this->getConf( 'UpdateLDAP' ) || $this->getDomain() == 'local' ) {
889            $this->printDebug(
890                "Either the user is using a local domain, or the wiki isn't allowing updates",
891                NONSENSITIVE
892            );
893            // We don't handle local preferences, but we don't want the
894            // wiki to return an error.
895            return true;
896        }
897
898        $writer = $this->getConf( 'WriterDN' );
899        if ( !$writer ) {
900            $this->printDebug( "The wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE );
901            // We can't modify LDAP preferences if we don't have a user
902            // capable of editing LDAP attributes.
903            return false;
904        }
905
906        $this->email = $user->getEmail();
907        $this->realname = $user->getRealName();
908        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
909        $this->nickname = $userOptionsLookup->getOption( $user, 'nickname' );
910        $this->lang = $userOptionsLookup->getOption( $user, 'language' );
911        if ( $this->connect() ) {
912            $this->userdn = $this->getSearchString( $user->getName() );
913            $this->printDebug( "Binding as the writerDN", NONSENSITIVE );
914            $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) );
915            if ( !$bind ) {
916                return false;
917            }
918
919            $values = [];
920            $prefs = $this->getConf( 'Preferences' );
921            foreach ( array_keys( $prefs ) as $key ) {
922                $attr = strtolower( $prefs[$key] );
923                switch ( $key ) {
924                    case "email":
925                        if ( is_string( $this->email ) ) {
926                            $values[$attr] = $this->email;
927                        }
928                        break;
929                    case "nickname":
930                        if ( is_string( $this->nickname ) ) {
931                            $values[$attr] = $this->nickname;
932                        }
933                        break;
934                    case "realname":
935                        if ( is_string( $this->realname ) ) {
936                            $values[$attr] = $this->realname;
937                        }
938                        break;
939                    case "language":
940                        if ( is_string( $this->lang ) ) {
941                            $values[$attr] = $this->lang;
942                        }
943                        break;
944                }
945            }
946
947            if ( count( $values ) &&
948                self::ldap_modify( $this->ldapconn, $this->userdn, $values )
949            ) {
950                // We changed the user, we need to invalidate the memcache key
951                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
952                $key = $cache->makeKey( 'ldapauthentication-userinfo', $this->userdn );
953                $cache->delete( $key );
954
955                $this->printDebug( "Successfully modified the user's attributes", NONSENSITIVE );
956                $this->unbind();
957                return true;
958            }
959            $this->printDebug( "Failed to modify the user's attributes", NONSENSITIVE );
960            $this->unbind();
961        }
962        return false;
963    }
964
965    /**
966     * Can the wiki create accounts in LDAP?
967     * Return true if yes.
968     *
969     * @return bool
970     */
971    public function canCreateAccounts() {
972        return $this->getConf( 'AddLDAPUsers' );
973    }
974
975    /**
976     * Can the wiki change passwords in LDAP, or can the user
977     * change passwords locally?
978     * Return true if yes.
979     *
980     * @return bool
981     */
982    public function allowPasswordChange() {
983        $this->printDebug( "Entering allowPasswordChange", NONSENSITIVE );
984
985        // Local domains need to be able to change passwords
986        return ( $this->getConf( 'UseLocal' ) && $this->getDomain() == 'local' )
987            || $this->getConf( 'UpdateLDAP' )
988            || $this->getConf( 'MailPassword' );
989    }
990
991    /**
992     * Disallow MediaWiki from setting local passwords in the database,
993     * unless UseLocal is true. Warning: if you set $wgLDAPUseLocal,
994     * it will cause MediaWiki to leak LDAP passwords into the local database.
995     * @return bool
996     */
997    public function allowSetLocalPassword() {
998        return $this->getConf( 'UseLocal' );
999    }
1000
1001    /**
1002     * Add a user to LDAP.
1003     * Return true if successful.
1004     *
1005     * @param User $user
1006     * @param string $password
1007     * @param string $email
1008     * @param string $realname
1009     * @return bool
1010     */
1011    public function addUser( $user, $password, $email = '', $realname = '' ) {
1012        $this->printDebug( "Entering addUser", NONSENSITIVE );
1013
1014        if ( !$this->getConf( 'AddLDAPUsers' ) || $this->getDomain() == 'local' ) {
1015            $this->printDebug( "Either the user is using a local domain, or the wiki isn't " .
1016                "allowing users to be added to LDAP", NONSENSITIVE );
1017
1018            // Tell the wiki not to return an error.
1019            return true;
1020        }
1021        if ( $this->getConf( 'RequiredGroups' ) ) {
1022            $this->printDebug( "The wiki is requiring users to be in specific groups, and cannot " .
1023                "add users as this would be a security hole.", NONSENSITIVE );
1024            // It is possible that later we can add users into
1025            // groups, but since we don't support it, we don't want
1026            // to open holes!
1027            return false;
1028        }
1029
1030        $writer = $this->getConf( 'WriterDN' );
1031        if ( !$writer ) {
1032            $this->printDebug( "The wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE );
1033
1034            // We can't add users without an LDAP account capable of doing so.
1035            return false;
1036        }
1037
1038        $this->email = $user->getEmail();
1039        $this->realname = $user->getRealName();
1040        $username = $user->getName();
1041        if ( $this->getConf( 'LowerCaseUsername' ) ) {
1042            $username = strtolower( $username );
1043        }
1044        $pass = $this->getPasswordHash( $password );
1045        if ( $this->connect() ) {
1046            $writeloc = $this->getConf( 'WriteLocation' );
1047            $this->userdn = $this->getSearchString( $username );
1048            if ( $this->userdn == '' ) {
1049                $this->printDebug(
1050                    "userdn is blank, attempting to use wgLDAPWriteLocation", NONSENSITIVE
1051                );
1052                if ( $writeloc ) {
1053                    $this->printDebug( "wgLDAPWriteLocation is set, using that", NONSENSITIVE );
1054                    $this->userdn = $this->getConf( 'SearchAttribute' ) . "=" .
1055                        $username . "," . $writeloc;
1056                } else {
1057                    $this->printDebug( "wgLDAPWriteLocation is not set, failing", NONSENSITIVE );
1058                    // getSearchString will bind, but will not unbind
1059                    $this->unbind();
1060                    return false;
1061                }
1062            }
1063
1064            $this->printDebug( "Binding as the writerDN", NONSENSITIVE );
1065            $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) );
1066            if ( !$bind ) {
1067                $this->printDebug( "Failed to bind as the writerDN; add failed", NONSENSITIVE );
1068                return false;
1069            }
1070
1071            // Set up LDAP objectclasses and attributes
1072            // TODO: make objectclasses and attributes configurable
1073            $values = [ 'uid' => $username ];
1074            // sn is required for objectclass inetorgperson
1075            $values["sn"] = $username;
1076            $prefs = $this->getConf( 'Preferences' );
1077            foreach ( array_keys( $prefs ) as $key ) {
1078                $attr = strtolower( $prefs[$key] );
1079                switch ( $key ) {
1080                    case "email":
1081                        if ( is_string( $this->email ) ) {
1082                            $values[$attr] = $this->email;
1083                        }
1084                        break;
1085                    case "nickname":
1086                        if ( is_string( $this->nickname ) ) {
1087                            $values[$attr] = $this->nickname;
1088                        }
1089                        break;
1090                    case "realname":
1091                        if ( is_string( $this->realname ) ) {
1092                            $values[$attr] = $this->realname;
1093                        }
1094                        break;
1095                    case "language":
1096                        if ( is_string( $this->lang ) ) {
1097                            $values[$attr] = $this->lang;
1098                        }
1099                        break;
1100                }
1101            }
1102            if ( !array_key_exists( "cn", $values ) ) {
1103                $values["cn"] = $username;
1104            }
1105            $values["userpassword"] = $pass;
1106            $values["objectclass"] = [ "inetorgperson" ];
1107
1108            $result = true;
1109            # Let other extensions modify the user object before creation
1110            $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
1111            $hookRunner->onLDAPSetCreationValues(
1112                $this, $username, $values, $writeloc, $this->userdn, $result );
1113            if ( !$result ) {
1114                $this->printDebug(
1115                    "Failed to add user because LDAPSetCreationValues returned false", NONSENSITIVE
1116                );
1117                $this->unbind();
1118                return false;
1119            }
1120
1121            $this->printDebug( "Adding user", NONSENSITIVE );
1122            if ( self::ldap_add( $this->ldapconn, $this->userdn, $values ) ) {
1123                $this->printDebug( "Successfully added user", NONSENSITIVE );
1124                $this->unbind();
1125                return true;
1126            }
1127            $errno = self::ldap_errno( $this->ldapconn );
1128            # Constraint violation, let's allow other plugins a chance to retry
1129            if ( $errno === 19 ) {
1130                $result = false;
1131                $hookRunner->onLDAPRetrySetCreationValues(
1132                    $this, $username, $values, $writeloc, $this->userdn, $result );
1133                if ( $result &&
1134                    self::ldap_add( $this->ldapconn, $this->userdn, $values )
1135                ) {
1136                    $this->printDebug( "Successfully added user", NONSENSITIVE );
1137                    $this->unbind();
1138                    return true;
1139                }
1140            }
1141            $this->printDebug( "Failed to add user (errno $errno)", NONSENSITIVE );
1142            $this->unbind();
1143        }
1144        return false;
1145    }
1146
1147    /**
1148     * Set the domain this plugin is supposed to use when authenticating.
1149     *
1150     * @param string $domain
1151     */
1152    public function setDomain( $domain ) {
1153        $this->printDebug( "Setting domain as: $domain", NONSENSITIVE );
1154        $_SESSION['wsDomain'] = $domain;
1155    }
1156
1157    /**
1158     * Get the user's domain
1159     *
1160     * @return string
1161     */
1162    public function getDomain() {
1163        $this->printDebug( "Entering getDomain", NONSENSITIVE );
1164
1165        # If there's only a single domain set, there's no reason
1166        # to bother with sessions, tokens, etc.. This works around
1167        # a number of bugs caused by supporting multiple domains.
1168        # The bugs will still exist when using multiple domains,
1169        # though.
1170        $domainNames = $this->getConf( 'DomainNames' );
1171        if ( ( count( $domainNames ) === 1 ) && !$this->getConf( 'UseLocal' ) ) {
1172            return $domainNames[0];
1173        }
1174        # First check if we already have a valid domain set
1175        if ( isset( $_SESSION['wsDomain'] ) && $_SESSION['wsDomain'] != 'invaliddomain' ) {
1176            $this->printDebug( "Pulling domain from session.", NONSENSITIVE );
1177            return $_SESSION['wsDomain'];
1178        }
1179        # If the session domain isn't set, the user may have been logged
1180        # in with a token, check the user options.
1181        $user = RequestContext::getMain()->getUser();
1182        if ( $user->isRegistered() && $user->getToken( false ) ) {
1183            $this->printDebug( "Pulling domain from user options.", NONSENSITIVE );
1184            $domain = self::loadDomain( $user );
1185            if ( $domain ) {
1186                return $domain;
1187            }
1188        }
1189        # The user must be using an invalid domain
1190        $this->printDebug( "No domain found, returning invaliddomain", NONSENSITIVE );
1191        return 'invaliddomain';
1192    }
1193
1194    /**
1195     * Check to see if the specific domain is a valid domain.
1196     * Return true if the domain is valid.
1197     *
1198     * @param string $domain
1199     * @return bool
1200     */
1201    public function validDomain( $domain ) {
1202        $this->printDebug( "Entering validDomain", NONSENSITIVE );
1203        if ( in_array( $domain, $this->getConf( 'DomainNames' ) ) ||
1204            ( $this->getConf( 'UseLocal' ) && $domain == 'local' )
1205        ) {
1206            $this->printDebug( "User is using a valid domain ($domain).", NONSENSITIVE );
1207            return true;
1208        }
1209        $this->printDebug( "User is not using a valid domain ($domain).", NONSENSITIVE );
1210        return false;
1211    }
1212
1213    /**
1214     * When a user logs in, update user with information from LDAP.
1215     *
1216     * @param User &$user
1217     * TODO: fix the setExternalID stuff
1218     */
1219    public function updateUser( &$user ) {
1220        $this->printDebug( "Entering updateUser", NONSENSITIVE );
1221        if ( $this->authFailed ) {
1222            $this->printDebug( "User didn't successfully authenticate, exiting.", NONSENSITIVE );
1223            return;
1224        }
1225
1226        $services = MediaWikiServices::getInstance();
1227        if ( $this->getConf( 'Preferences' ) ) {
1228            $this->printDebug( "Setting user preferences.", NONSENSITIVE );
1229            $userOptionsManager = $services->getUserOptionsManager();
1230            if ( is_string( $this->lang ) ) {
1231                $this->printDebug( "Setting language.", NONSENSITIVE );
1232                $userOptionsManager->setOption( $user, 'language', $this->lang );
1233            }
1234            if ( is_string( $this->nickname ) ) {
1235                $this->printDebug( "Setting nickname.", NONSENSITIVE );
1236                $userOptionsManager->setOption( $user, 'nickname', $this->nickname );
1237            }
1238            if ( is_string( $this->realname ) ) {
1239                $this->printDebug( "Setting realname.", NONSENSITIVE );
1240                $user->setRealName( $this->realname );
1241            }
1242            if ( is_string( $this->email ) ) {
1243                $this->printDebug( "Setting email.", NONSENSITIVE );
1244                $user->setEmail( $this->email );
1245                $user->confirmEmail();
1246            }
1247        }
1248
1249        if ( $this->getConf( 'UseLDAPGroups' ) ) {
1250            $this->printDebug( "Setting user groups.", NONSENSITIVE );
1251            $this->setGroups( $user );
1252        }
1253
1254        # We must set a user option if we want token based logins to work
1255        if ( $user->getToken( false ) ) {
1256            $this->printDebug( "User has a token, setting domain in user options.", NONSENSITIVE );
1257            self::saveDomain( $user, $_SESSION['wsDomain'] );
1258        }
1259
1260        # Let other extensions update the user
1261        ( new HookRunner( $services->getHookContainer() ) )->onLDAPUpdateUser( $user );
1262
1263        $this->printDebug( "Saving user settings.", NONSENSITIVE );
1264        $user->saveSettings();
1265    }
1266
1267    /**
1268     * When creating a user account, initialize user with information from LDAP.
1269     * TODO: fix setExternalID stuff
1270     *
1271     * @param User &$user
1272     * @param bool $autocreate
1273     */
1274    public function initUser( &$user, $autocreate = false ) {
1275        $this->printDebug( "Entering initUser", NONSENSITIVE );
1276
1277        if ( $this->authFailed ) {
1278            $this->printDebug( "User didn't successfully authenticate, exiting.", NONSENSITIVE );
1279            return;
1280        }
1281        if ( $this->getDomain() == 'local' ) {
1282            $this->printDebug( "User is using a local domain", NONSENSITIVE );
1283            return;
1284        }
1285
1286        if ( $autocreate && !$this->userExists( $user->mName ) ) {
1287            // Generate a random password for the account under the assumption
1288            // that either the caller will be setting a password immediately
1289            // after using User::changeAuthenticationData or that other
1290            // password recovery means will be used if the account is meant
1291            // for interactive use.
1292            $pwreq = MediaWiki\Auth\TemporaryPasswordAuthenticationRequest::newRandom();
1293            $this->addUser( $user, $pwreq->password );
1294        }
1295
1296        // The update user function does everything else we need done.
1297        $this->updateUser( $user );
1298
1299        // updateUser() won't necessarily save the user's settings
1300        $user->saveSettings();
1301    }
1302
1303    /**
1304     * Return true to prevent logins that don't authenticate here from being
1305     * checked against the local database's password fields.
1306     *
1307     * This is just a question, and shouldn't perform any actions.
1308     *
1309     * @return bool
1310     */
1311    public function strict() {
1312        $this->printDebug( "Entering strict.", NONSENSITIVE );
1313
1314        if ( $this->getConf( 'UseLocal' ) || $this->getConf( 'MailPassword' ) ) {
1315            $this->printDebug( "Returning false in strict().", NONSENSITIVE );
1316            return false;
1317        }
1318        $this->printDebug( "Returning true in strict().", NONSENSITIVE );
1319        return true;
1320    }
1321
1322    /**
1323     * Munge the username based on a scheme (lowercase, by default), by search attribute
1324     * otherwise.
1325     *
1326     * @param string $username
1327     * @return string
1328     */
1329    public function getCanonicalName( $username ) {
1330        $this->printDebug( "Entering getCanonicalName", NONSENSITIVE );
1331        $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
1332        if ( $userNameUtils->isIP( $username ) ) {
1333            $this->printDebug( "Username is an IP, not munging.", NONSENSITIVE );
1334            return $username;
1335        }
1336        $canonicalname = $username;
1337        if ( $username != '' ) {
1338            $this->printDebug( "Username is: $username", NONSENSITIVE );
1339            if ( $this->getConf( 'LowerCaseUsername' ) ) {
1340                $canonicalname = ucfirst( strtolower( $canonicalname ) );
1341            } else {
1342                # Fetch username, so that we can possibly use it.
1343                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1344                $userInfo = $cache->getWithSetCallback(
1345                    $cache->makeKey( 'ldapauthentication-canonicalname', $username ),
1346                    $cache::TTL_DAY,
1347                    function () use ( $username ) {
1348                        $canonicalname = $username;
1349
1350                        if ( $this->validDomain( $this->getDomain() ) && $this->connect() ) {
1351                            // Try to pull the username from LDAP. In the case of straight binds,
1352                            // try to fetch the username by search before bind.
1353                            $this->userdn = $this->getUserDN( $username, true );
1354                            $hookSetUsername = $this->LDAPUsername;
1355                            ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
1356                                ->onSetUsernameAttributeFromLDAP( $hookSetUsername, $this->userInfo );
1357                            if ( is_string( $hookSetUsername ) ) {
1358                                $this->printDebug(
1359                                    "Username munged by hook: $hookSetUsername", NONSENSITIVE
1360                                );
1361                                $this->LDAPUsername = $hookSetUsername;
1362                            } else {
1363                                $this->printDebug(
1364                                    "Fetched username is not a string (check your hook code...). " .
1365                                    "This message can be safely ignored if you do not have the " .
1366                                    "SetUsernameAttributeFromLDAP hook defined.", NONSENSITIVE
1367                                );
1368                            }
1369                        }
1370
1371                        // We want to use the username returned by LDAP if it exists
1372                        if ( $this->LDAPUsername != '' ) {
1373                            $canonicalname = ucfirst( $this->LDAPUsername );
1374                            $this->printDebug( "Using LDAPUsername: $canonicalname", NONSENSITIVE );
1375                        }
1376
1377                        return [ 'username' => $username, 'canonicalname' => $canonicalname ];
1378                    }
1379                );
1380
1381                $canonicalname = $userInfo['canonicalname'];
1382            }
1383        }
1384        $this->printDebug( "Munged username: $canonicalname", NONSENSITIVE );
1385        return $canonicalname;
1386    }
1387
1388    /**
1389     * Configures the authentication plugin for use with auto-authentication
1390     * plugins.
1391     */
1392    public function autoAuthSetup() {
1393        $this->setDomain( $this->getConf( 'AutoAuthDomain' ) );
1394    }
1395
1396    /**
1397     * Gets the searchstring for a user based upon settings for the domain.
1398     * Returns a full DN for a user.
1399     *
1400     * @param string $username
1401     * @return string
1402     */
1403    private function getSearchString( $username ) {
1404        $this->printDebug( "Entering getSearchString", NONSENSITIVE );
1405        $ss = $this->getConf( 'SearchString' );
1406        if ( $ss ) {
1407            // This is a straight bind
1408            $this->printDebug( "Doing a straight bind", NONSENSITIVE );
1409            $userdn = str_replace( "USER-NAME", $username, $ss );
1410        } else {
1411            $userdn = $this->getUserDN( $username, true );
1412        }
1413        $this->printDebug( "userdn is: $userdn", SENSITIVE );
1414        return $userdn;
1415    }
1416
1417    /**
1418     * Gets the DN of a user based upon settings for the domain.
1419     * This function will set $this->LDAPUsername
1420     *
1421     * @param string $username
1422     * @param bool $bind
1423     * @param string $searchattr
1424     * @return string
1425     */
1426    public function getUserDN( $username, $bind = false, $searchattr = '' ) {
1427        $this->printDebug( "Entering getUserDN", NONSENSITIVE );
1428        if ( $bind ) {
1429            // This is a proxy bind, or an anonymous bind with a search
1430            $proxyagent = $this->getConf( 'ProxyAgent' );
1431            if ( $proxyagent ) {
1432                // This is a proxy bind
1433                $this->printDebug( "Doing a proxy bind", NONSENSITIVE );
1434                $bind = $this->bindAs( $proxyagent, $this->getConf( 'ProxyAgentPassword' ) );
1435            } else {
1436                // This is an anonymous bind
1437                $this->printDebug( "Doing an anonymous bind", NONSENSITIVE );
1438                $bind = $this->bindAs();
1439            }
1440            if ( !$bind ) {
1441                $this->printDebug( "Failed to bind", NONSENSITIVE );
1442                $this->fetchedUserInfo = false;
1443                $this->userInfo = null;
1444                return '';
1445            }
1446        }
1447
1448        if ( !$searchattr ) {
1449            $searchattr = $this->getConf( 'SearchAttribute' );
1450        }
1451        // we need to do a subbase search for the entry
1452        $filter = "(" . $searchattr . "=" . $this->getLdapEscapedString( $username ) . ")";
1453        $this->printDebug( "Created a regular filter: $filter", SENSITIVE );
1454
1455        // We explicitly put memberof here because it's an operational attribute in some servers.
1456        $attributes = [ "*", "memberof" ];
1457        $base = $this->getBaseDN( USERDN );
1458        $this->printDebug( "Using base: $base", SENSITIVE );
1459        $entry = self::ldap_search(
1460            $this->ldapconn, $base, $filter, $attributes
1461        );
1462        if ( self::ldap_count_entries( $this->ldapconn, $entry ) == 0 ) {
1463            $this->printDebug( "Couldn't find an entry", NONSENSITIVE );
1464            $this->fetchedUserInfo = false;
1465            $this->userInfo = null;
1466            return '';
1467        }
1468        $this->userInfo = self::ldap_get_entries( $this->ldapconn, $entry );
1469        $this->fetchedUserInfo = true;
1470        if ( isset( $this->userInfo[0][$searchattr] ) ) {
1471            $username = $this->userInfo[0][$searchattr][0];
1472            $this->printDebug(
1473                "Setting the LDAPUsername based on fetched wgLDAPSearchAttributes: $username",
1474                NONSENSITIVE
1475            );
1476            $this->LDAPUsername = $username;
1477        }
1478        return $this->userInfo[0]["dn"];
1479    }
1480
1481    /**
1482     * Load the current user's entry
1483     *
1484     * @return bool
1485     */
1486    public function getUserInfo() {
1487        // Don't fetch the same data more than once
1488        if ( $this->fetchedUserInfo ) {
1489            return true;
1490        }
1491        $userInfo = $this->getUserInfoStateless( $this->userdn );
1492        if ( $userInfo === null ) {
1493            $this->fetchedUserInfo = false;
1494        } else {
1495            $this->fetchedUserInfo = true;
1496            $this->userInfo = $userInfo;
1497        }
1498        return $this->fetchedUserInfo;
1499    }
1500
1501    /**
1502     * @param string $userdn
1503     * @return array|null
1504     * @phan-return ?array<int|string,int|array<int|string,string|int|array<int|string,int|string>>>
1505     */
1506    public function getUserInfoStateless( $userdn ) {
1507        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1508
1509        return $cache->getWithSetCallback(
1510            $cache->makeKey( 'ldapauthentication-userinfo', $userdn ),
1511            $cache::TTL_DAY,
1512            function ( $oldValue, &$ttl ) use ( $userdn, $cache ) {
1513                $entry = self::ldap_read(
1514                    $this->ldapconn,
1515                    $userdn,
1516                    "objectclass=*",
1517                    [ '*', 'memberof' ]
1518                );
1519                $userInfo = self::ldap_get_entries( $this->ldapconn, $entry );
1520                if ( $userInfo["count"] < 1 ) {
1521                    $ttl = $cache::TTL_UNCACHEABLE;
1522
1523                    return null;
1524                }
1525
1526                return $userInfo;
1527            }
1528        );
1529    }
1530
1531    /**
1532     * Retrieve user preferences from LDAP
1533     */
1534    private function getPreferences() {
1535        $this->printDebug( "Entering getPreferences", NONSENSITIVE );
1536
1537        // Retrieve preferences
1538        $prefs = $this->getConf( 'Preferences' );
1539        if ( !$prefs ) {
1540            return;
1541        }
1542        if ( !$this->getUserInfo() ) {
1543            $this->printDebug(
1544                "Failed to get preferences, the user's entry wasn't found.", NONSENSITIVE
1545            );
1546            return;
1547        }
1548        $this->printDebug( "Retrieving preferences", NONSENSITIVE );
1549        foreach ( array_keys( $prefs ) as $key ) {
1550            $attr = strtolower( $prefs[$key] );
1551            if ( !isset( $this->userInfo[0][$attr] ) ) {
1552                continue;
1553            }
1554            $value = $this->userInfo[0][$attr][0];
1555            switch ( $key ) {
1556                case "email":
1557                    $this->email = $value;
1558                    $this->printDebug(
1559                        "Retrieved email ($this->email) using attribute ($prefs[$key])",
1560                        NONSENSITIVE
1561                    );
1562                    break;
1563                case "language":
1564                    $this->lang = $value;
1565                    $this->printDebug(
1566                        "Retrieved language ($this->lang) using attribute ($prefs[$key])",
1567                        NONSENSITIVE
1568                    );
1569                    break;
1570                case "nickname":
1571                    $this->nickname = $value;
1572                    $this->printDebug(
1573                        "Retrieved nickname ($this->nickname) using attribute ($prefs[$key])",
1574                        NONSENSITIVE
1575                    );
1576                    break;
1577                case "realname":
1578                    $this->realname = $value;
1579                    $this->printDebug(
1580                        "Retrieved realname ($this->realname) using attribute ($prefs[$key])",
1581                        NONSENSITIVE
1582                    );
1583                    break;
1584            }
1585        }
1586    }
1587
1588    /**
1589     * Checks to see whether a user is in a required group.
1590     *
1591     * @return bool
1592     */
1593    private function checkGroups() {
1594        $this->printDebug( "Entering checkGroups", NONSENSITIVE );
1595
1596        $excgroups = $this->getConf( 'ExcludedGroups' );
1597        if ( $excgroups ) {
1598            $this->printDebug( "Checking for excluded group membership", NONSENSITIVE );
1599
1600            $excgroups = array_map( 'strtolower', $excgroups );
1601
1602            $this->printDebug( "Excluded groups:", NONSENSITIVE, $excgroups );
1603
1604            foreach ( $this->userLDAPGroups["dn"] as $group ) {
1605                $this->printDebug( "Checking against: $group", NONSENSITIVE );
1606                if ( in_array( $group, $excgroups ) ) {
1607                    $this->printDebug( "Found user in an excluded group.", NONSENSITIVE );
1608                    return false;
1609                }
1610            }
1611        }
1612
1613        $reqgroups = $this->getConf( 'RequiredGroups' );
1614        if ( $reqgroups ) {
1615            $this->printDebug( "Checking for (new style) group membership", NONSENSITIVE );
1616
1617            $reqgroups = array_map( 'strtolower', $reqgroups );
1618
1619            $this->printDebug( "Required groups:", NONSENSITIVE, $reqgroups );
1620
1621            foreach ( $this->userLDAPGroups["dn"] as $group ) {
1622                $this->printDebug( "Checking against: $group", NONSENSITIVE );
1623                if ( in_array( $group, $reqgroups ) ) {
1624                    $this->printDebug( "Found user in a group.", NONSENSITIVE );
1625                    return true;
1626                }
1627            }
1628
1629            $this->printDebug( "Couldn't find the user in any groups.", NONSENSITIVE );
1630            return false;
1631        }
1632
1633        // Ensure we return true if we aren't checking groups.
1634        return true;
1635    }
1636
1637    /**
1638     * Function to get the user's groups.
1639     * @param string $username
1640     */
1641    protected function getGroups( $username ) {
1642        $this->printDebug( "Entering getGroups", NONSENSITIVE );
1643
1644        // Ensure userLDAPGroups is set, no matter what
1645        $this->userLDAPGroups = [ "dn" => [], "short" => [] ];
1646
1647        // Find groups
1648        if ( $this->getConf( 'RequiredGroups' ) || $this->getConf( 'UseLDAPGroups' ) ) {
1649            $this->printDebug( "Retrieving LDAP group membership", NONSENSITIVE );
1650
1651            // Let's figure out what we should be searching for
1652            if ( $this->getConf( 'GroupUseFullDN' ) ) {
1653                $usertopass = $this->userdn;
1654            } else {
1655                if ( $this->getConf( 'GroupUseRetrievedUsername' ) && $this->LDAPUsername != '' ) {
1656                    $usertopass = $this->LDAPUsername;
1657                } else {
1658                    $usertopass = $username;
1659                }
1660            }
1661
1662            if ( $this->getConf( 'GroupsUseMemberOf' ) ) {
1663                $this->printDebug( "Using memberOf", NONSENSITIVE );
1664                if ( !$this->getUserInfo() ) {
1665                    $this->printDebug( "Couldn't get the user's entry.", NONSENSITIVE );
1666                } elseif ( isset( $this->userInfo[0]["memberof"] ) ) {
1667                    # The first entry is always a count
1668                    $memberOfMembers = $this->userInfo[0]["memberof"];
1669                    array_shift( $memberOfMembers );
1670                    $groups = [ "dn" => [], "short" => [] ];
1671
1672                    foreach ( $memberOfMembers as $mem ) {
1673                        array_push( $groups["dn"], strtolower( $mem ) );
1674
1675                        // Get short name of group
1676                        $memAttrs = explode( ',', strtolower( $mem ) );
1677                        if ( isset( $memAttrs[0] ) ) {
1678                            $memAttrs = explode( '=', $memAttrs[0] );
1679                            if ( isset( $memAttrs[0] ) ) {
1680                                array_push( $groups["short"], strtolower( $memAttrs[1] ) );
1681                            }
1682                        }
1683                    }
1684                    $this->printDebug( "Got the following groups:", SENSITIVE, $groups["dn"] );
1685
1686                    $this->userLDAPGroups = $groups;
1687                } else {
1688                    $this->printDebug( "memberOf attribute isn't set", NONSENSITIVE );
1689                }
1690            } else {
1691                $this->printDebug( "Searching for the groups", NONSENSITIVE );
1692                $this->userLDAPGroups = $this->searchGroups( $usertopass );
1693                if ( $this->getConf( 'GroupSearchNestedGroups' ) ) {
1694                    $this->userLDAPGroups = $this->searchNestedGroups( $this->userLDAPGroups );
1695                    $this->printDebug(
1696                        "Got the following nested groups:", SENSITIVE, $this->userLDAPGroups["dn"]
1697                    );
1698                }
1699            }
1700
1701            if ( $this->getConf( 'GroupSearchPosixPrimaryGroup' ) ) {
1702                if ( !$this->getUserInfo() ) {
1703                    $this->printDebug( "Couldn't get the user's entry.", NONSENSITIVE );
1704                } elseif ( isset( $this->userInfo[0]["gidnumber"] ) ) {
1705                    $base = $this->getBaseDN( GROUPDN );
1706                    $objectclass = $this->getConf( 'GroupObjectclass' );
1707                    $filter = "(&(objectClass={$objectclass})" .
1708                        "(gidNumber={$this->userInfo[0]['gidnumber'][0]}))";
1709                    $info = self::ldap_search( $this->ldapconn, $base, $filter );
1710                    $entries = self::ldap_get_entries( $this->ldapconn, $info );
1711                    if ( empty( $entries[0] ) ) {
1712                        $this->printDebug( "Couldn't get the user's primary group.", NONSENSITIVE );
1713                    } else {
1714                        $primary_group_dn = strtolower( $entries[0]["dn"] );
1715                        $this->printDebug( "Got the user's primary group:" . $primary_group_dn, SENSITIVE );
1716                        $this->userLDAPGroups["dn"][] = $primary_group_dn;
1717                        $nameattribute = strtolower( $this->getConf( 'GroupNameAttribute' ) );
1718                        $this->userLDAPGroups["short"][] = $entries[0][$nameattribute][0];
1719                    }
1720                }
1721            }
1722
1723            // Only find all groups if the user has any groups; otherwise, we are
1724            // just wasting a search.
1725            if ( $this->getConf( 'GroupsPrevail' ) && count( $this->userLDAPGroups ) != 0 ) {
1726                $this->allLDAPGroups = $this->searchGroups( '*' );
1727            }
1728        }
1729    }
1730
1731    /**
1732     * Function to return an array of nested groups when given a group or list of groups.
1733     * $searchedgroups is used for tail recursion and shouldn't be provided
1734     * when called externally.
1735     *
1736     * @param array $groups
1737     * @param array $searchedgroups
1738     * @return array
1739     */
1740    private function searchNestedGroups( $groups, $searchedgroups = [ "dn" => [], "short" => [] ] ) {
1741        $this->printDebug( "Entering searchNestedGroups", NONSENSITIVE );
1742
1743        // base case, no more groups left to check
1744        if ( count( $groups["dn"] ) == 0 ) {
1745            $this->printDebug( "No more groups to search.", NONSENSITIVE );
1746            return $searchedgroups;
1747        }
1748
1749        $this->printDebug( "Searching groups:", SENSITIVE, $groups["dn"] );
1750        $groupstosearch = [ "short" => [], "dn" => [] ];
1751        foreach ( $groups["dn"] as $group ) {
1752            $returnedgroups = $this->searchGroups( $group );
1753            $this->printDebug(
1754                "Group $group is in the following groups:", SENSITIVE, $returnedgroups["dn"]
1755            );
1756            foreach ( $returnedgroups["dn"] as $searchme ) {
1757                if ( in_array( $searchme, $searchedgroups["dn"] ) ) {
1758                    // We already searched this, move on
1759                    continue;
1760                }
1761
1762                // We'll need to search this group's members now
1763                $this->printDebug( "Adding $searchme to the list of groups (1)", SENSITIVE );
1764                $groupstosearch["dn"][] = $searchme;
1765            }
1766            foreach ( $returnedgroups["short"] as $searchme ) {
1767                if ( in_array( $searchme, $searchedgroups["short"] ) ) {
1768                    // We already searched this, move on
1769                    continue;
1770                }
1771
1772                $this->printDebug( "Adding $searchme to the list of groups (2)", SENSITIVE );
1773                // We'll need to search this group's members now
1774                $groupstosearch["short"][] = $searchme;
1775            }
1776        }
1777        $searchedgroups = array_merge_recursive( $groups, $searchedgroups );
1778
1779        return $this->searchNestedGroups( $groupstosearch, $searchedgroups );
1780    }
1781
1782    /**
1783     * Search groups for the supplied DN
1784     *
1785     * @param string $dn
1786     * @return array
1787     */
1788    private function searchGroups( $dn ) {
1789        $this->printDebug( "Entering searchGroups", NONSENSITIVE );
1790
1791        $base = $this->getBaseDN( GROUPDN );
1792        $objectclass = $this->getConf( 'GroupObjectclass' );
1793        $attribute = $this->getConf( 'GroupAttribute' );
1794        $nameattribute = $this->getConf( 'GroupNameAttribute' );
1795
1796        // We actually want to search for * not \2a, ensure we don't escape *
1797        $value = $dn;
1798        if ( $value != "*" ) {
1799            $value = $this->getLdapEscapedString( $value );
1800        }
1801
1802        $proxyagent = $this->getConf( 'ProxyAgent' );
1803        if ( $proxyagent ) {
1804            // We'll try to bind as the proxyagent as the proxyagent should normally have more
1805            // rights than the user. If the proxyagent fails to bind, we will still be able
1806            // to search as the normal user (which is why we don't return on fail).
1807            $this->printDebug( "Binding as the proxyagent", NONSENSITIVE );
1808            $this->bindAs( $proxyagent, $this->getConf( 'ProxyAgentPassword' ) );
1809        }
1810
1811        $groups = [ "short" => [], "dn" => [] ];
1812
1813        // AD does not include the primary group in the list of groups, we have to find it ourselves
1814        if ( $dn != "*" && $this->getConf( 'ActiveDirectory' ) ) {
1815            $PGfilter = "(&(distinguishedName=$value)(objectclass=user))";
1816            $this->printDebug( "User Filter: $PGfilter", SENSITIVE );
1817            $PGinfo = self::ldap_search( $this->ldapconn, $base, $PGfilter );
1818            $PGentries = self::ldap_get_entries( $this->ldapconn, $PGinfo );
1819            if ( !empty( $PGentries[0] ) ) {
1820                $Usid = $PGentries[0]['objectsid'][0];
1821                $PGrid = $PGentries[0]['primarygroupid'][0];
1822                $PGsid = bin2hex( $Usid );
1823                $PGSID = [];
1824                for ( $i = 0; $i < 56; $i += 2 ) {
1825                    $PGSID[] = substr( $PGsid, $i, 2 );
1826                }
1827                $dPGrid = dechex( $PGrid );
1828                $dPGrid = str_pad( $dPGrid, 8, '0', STR_PAD_LEFT );
1829                $PGRID = [];
1830                for ( $i = 0; $i < 8; $i += 2 ) {
1831                    array_push( $PGRID, substr( $dPGrid, $i, 2 ) );
1832                }
1833                for ( $i = 24; $i < 28; $i++ ) {
1834                    $PGSID[$i] = array_pop( $PGRID );
1835                }
1836                $PGsid_string = '';
1837                foreach ( $PGSID as $PGsid_bit ) {
1838                    $PGsid_string .= "\\" . $PGsid_bit;
1839                }
1840                $PGfilter = "(&(objectSid=$PGsid_string)(objectclass=$objectclass))";
1841                $this->printDebug( "Primary Group Filter: $PGfilter", SENSITIVE );
1842                $info = self::ldap_search( $this->ldapconn, $base, $PGfilter );
1843                $PGentries = self::ldap_get_entries( $this->ldapconn, $info );
1844                array_shift( $PGentries );
1845                $dnMember = strtolower( $PGentries[0]['dn'] );
1846                $groups["dn"][] = $dnMember;
1847                // Get short name of group
1848                $memAttrs = explode( ',', strtolower( $dnMember ) );
1849                if ( isset( $memAttrs[0] ) ) {
1850                    $memAttrs = explode( '=', $memAttrs[0] );
1851                    if ( isset( $memAttrs[1] ) ) {
1852                        $groups["short"][] = strtolower( $memAttrs[1] );
1853                    }
1854                }
1855
1856            }
1857        }
1858
1859        $filter = "(&($attribute=$value)(objectclass=$objectclass))";
1860        $this->printDebug( "Search string: $filter", SENSITIVE );
1861        $info = self::ldap_search( $this->ldapconn, $base, $filter );
1862        if ( !$info ) {
1863            $this->printDebug( "No entries returned from search.", SENSITIVE );
1864            // Return an array so that other functions
1865            // don't error out.
1866            return [ "short" => [], "dn" => [] ];
1867        }
1868
1869        $entries = self::ldap_get_entries( $this->ldapconn, $info );
1870        if ( $entries ) {
1871            // We need to shift because the first entry will be a count
1872            array_shift( $entries );
1873            // Let's get a list of both full dn groups and shortname groups
1874            foreach ( $entries as $entry ) {
1875                $shortMember = strtolower( $entry[$nameattribute][0] );
1876                $dnMember = strtolower( $entry['dn'] );
1877                $groups["short"][] = $shortMember;
1878                $groups["dn"][] = $dnMember;
1879            }
1880        }
1881
1882        $this->printDebug( "Returned groups:", SENSITIVE, $groups["dn"] );
1883        return $groups;
1884    }
1885
1886    /**
1887     * Returns true if this group is in the list of the currently authenticated
1888     * user's groups, else false.
1889     *
1890     * @param string $group
1891     * @return bool
1892     */
1893    private function hasLDAPGroup( $group ) {
1894        $this->printDebug( "Entering hasLDAPGroup", NONSENSITIVE );
1895        return in_array( strtolower( $group ), $this->userLDAPGroups["short"] );
1896    }
1897
1898    /**
1899     * Returns true if an LDAP group with this name exists, else false.
1900     *
1901     * @param string $group
1902     * @return bool
1903     */
1904    private function isLDAPGroup( $group ) {
1905        $this->printDebug( "Entering isLDAPGroup", NONSENSITIVE );
1906        return in_array( strtolower( $group ), $this->allLDAPGroups["short"] );
1907    }
1908
1909    /**
1910     * Helper function for updateUser() and initUser(). Adds users into MediaWiki security groups
1911     * based upon groups retrieved from LDAP.
1912     *
1913     * @param User &$user
1914     */
1915    private function setGroups( &$user ) {
1916        global $wgGroupPermissions;
1917        $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
1918
1919        // TODO: this is *really* ugly code. clean it up!
1920        $this->printDebug( "Entering setGroups.", NONSENSITIVE );
1921
1922        # Add ldap groups as local groups
1923        if ( $this->getConf( 'GroupsPrevail' ) ) {
1924            $this->printDebug(
1925                "Adding all groups to wgGroupPermissions: ", SENSITIVE, $this->allLDAPGroups
1926            );
1927
1928            foreach ( $this->allLDAPGroups["short"] as $ldapgroup ) {
1929                if ( !array_key_exists( $ldapgroup, $wgGroupPermissions ) ) {
1930                    $wgGroupPermissions[$ldapgroup] = [];
1931                }
1932            }
1933        }
1934
1935        # add groups permissions
1936        $localAvailGrps = $userGroupManager->listAllGroups();
1937        $localUserGrps = $userGroupManager->getUserEffectiveGroups( $user );
1938        $defaultLocallyManagedGrps = [ 'bot', 'sysop', 'bureaucrat' ];
1939        $locallyManagedGrps = $this->getConf( 'LocallyManagedGroups' );
1940        if ( $locallyManagedGrps ) {
1941            $locallyManagedGrps = array_unique(
1942                array_merge( $defaultLocallyManagedGrps, $locallyManagedGrps ) );
1943            $this->printDebug( "Locally managed groups: ", SENSITIVE, $locallyManagedGrps );
1944        } else {
1945            $locallyManagedGrps = $defaultLocallyManagedGrps;
1946            $this->printDebug(
1947                "Locally managed groups is unset, using defaults: ", SENSITIVE, $locallyManagedGrps
1948            );
1949        }
1950
1951        $this->printDebug( "Available groups are: ", NONSENSITIVE, $localAvailGrps );
1952        $this->printDebug( "Effective groups are: ", NONSENSITIVE, $localUserGrps );
1953        # note: $localUserGrps does not need to be updated with $cGroup added,
1954        #       as $localAvailGrps contains $cGroup only once.
1955        foreach ( $localAvailGrps as $cGroup ) {
1956            # did we once add the user to the group?
1957            if ( in_array( $cGroup, $localUserGrps ) ) {
1958                $this->printDebug(
1959                    "Checking to see if we need to remove user from: $cGroup", NONSENSITIVE
1960                );
1961                if ( !$this->hasLDAPGroup( $cGroup ) &&
1962                    !in_array( $cGroup, $locallyManagedGrps )
1963                ) {
1964                    $this->printDebug( "Removing user from: $cGroup", NONSENSITIVE );
1965                    # the ldap group overrides the local group
1966                    # so as the user is currently not a member of the ldap group,
1967                    # he shall be removed from the local group
1968                    $userGroupManager->removeUserFromGroup( $user, $cGroup );
1969                }
1970            } else {
1971                # no, but maybe the user has recently been added to the ldap group?
1972                $this->printDebug( "Checking to see if user is in: $cGroup", NONSENSITIVE );
1973                if ( $this->hasLDAPGroup( $cGroup ) ) {
1974                    $this->printDebug( "Adding user to: $cGroup", NONSENSITIVE );
1975                    $userGroupManager->addUserToGroup( $user, $cGroup );
1976                }
1977            }
1978        }
1979    }
1980
1981    /**
1982     * Returns a password that is created via the configured hash settings.
1983     *
1984     * @param string $password
1985     * @return string
1986     */
1987    private function getPasswordHash( $password ) {
1988        $this->printDebug( "Entering getPasswordHash", NONSENSITIVE );
1989
1990        // Set the password hashing based upon admin preference
1991        switch ( $this->getConf( 'PasswordHash' ) ) {
1992            case 'crypt':
1993                // @phan-suppress-next-line PhanParamTooFewInternal FIXME Second arg is optional but emit E_NOTICE
1994                $pass = '{CRYPT}' . crypt( $password );
1995                break;
1996            case 'clear':
1997                $pass = $password;
1998                break;
1999            default:
2000                $pwd_sha = base64_encode( pack( 'H*', sha1( $password ) ) );
2001                $pass = "{SHA}" . $pwd_sha;
2002                break;
2003        }
2004
2005        return $pass;
2006    }
2007
2008    /**
2009     * Prints debugging information. $debugText is what you want to print, $debugVal
2010     * is the level at which you want to print the information.
2011     *
2012     * @param string $debugText
2013     * @param int $debugVal One of NONSENSITIVE, SENSITIVE or HIGHLYSENSITIVE
2014     * @param array|null $debugArr
2015     */
2016    public function printDebug( $debugText, $debugVal, $debugArr = null ) {
2017        if ( !function_exists( 'wfDebugLog' ) ) {
2018            return;
2019        }
2020
2021        global $wgLDAPDebug;
2022
2023        if ( $wgLDAPDebug >= $debugVal ) {
2024            if ( $debugArr !== null ) {
2025                $debugText = $debugText . " " . implode( "::", $debugArr );
2026            }
2027            /* Update second parameter when bumping versions */
2028            wfDebugLog( 'ldap', '2.2.0' . ' ' . $debugText, false );
2029        }
2030    }
2031
2032    /**
2033     * Binds as $userdn with $password. This can be called with only the ldap
2034     * connection resource for an anonymous bind.
2035     *
2036     * @param string|null $userdn
2037     * @param string|null $password
2038     * @return bool
2039     */
2040    public function bindAs( $userdn = null, $password = null ) {
2041        // Let's see if the user can authenticate.
2042        if ( $userdn == null || $password == null ) {
2043            $bind = self::ldap_bind( $this->ldapconn );
2044        } else {
2045            $bind = self::ldap_bind( $this->ldapconn, $userdn, $password );
2046        }
2047        if ( !$bind ) {
2048            $this->printDebug( "Failed to bind as $userdn", NONSENSITIVE );
2049            return false;
2050        }
2051        $this->boundAs = $userdn;
2052        return true;
2053    }
2054
2055    /**
2056     * Unbind and destroy the current LDAP connection.
2057     * @return void
2058     */
2059    public function unbind() {
2060        self::ldap_unbind( $this->ldapconn );
2061        // ldap_unbind marks the connection resource as unusable, so discard
2062        // it so that we know to recreate it when needed in the future.
2063        $this->ldapconn = null;
2064        $this->boundAs = null;
2065    }
2066
2067    /**
2068     * Returns true if auto-authentication is allowed, and the user is
2069     * authenticating using the auto-authentication domain.
2070     *
2071     * @return bool
2072     */
2073    private function useAutoAuth() {
2074        return $this->getDomain() == $this->getConf( 'AutoAuthDomain' );
2075    }
2076
2077    /**
2078     * Returns a string which has the chars *, (, ), \ & NUL escaped to LDAP compliant
2079     * syntax as per RFC 2254
2080     * Thanks and credit to Iain Colledge for the research and function.
2081     *
2082     * @param string $string
2083     * @return string
2084     */
2085    public function getLdapEscapedString( $string ) {
2086        // Make the string LDAP compliant by escaping *, (, ) , \ & NUL
2087        return str_replace(
2088            [ "\\", "(", ")", "*", "\x00" ],
2089            [ "\\5c", "\\28", "\\29", "\\2a", "\\00" ],
2090            $string
2091            );
2092    }
2093
2094    /**
2095     * Returns a basedn by the type of entry we are searching for.
2096     *
2097     * @param int $type
2098     * @return string
2099     */
2100    public function getBaseDN( $type ) {
2101        $this->printDebug( "Entering getBaseDN", NONSENSITIVE );
2102
2103        $ret = '';
2104        switch ( $type ) {
2105            case USERDN:
2106                $ret = $this->getConf( 'UserBaseDN' );
2107                break;
2108            case GROUPDN:
2109                $ret = $this->getConf( 'GroupBaseDN' );
2110                break;
2111            case DEFAULTDN:
2112                $ret = $this->getConf( 'BaseDN' );
2113                if ( $ret ) {
2114                    return $ret;
2115                }
2116
2117                $this->printDebug( "basedn is not set.", NONSENSITIVE );
2118                return '';
2119        }
2120
2121        if ( $ret == '' ) {
2122            $this->printDebug(
2123                "basedn is not set for this type of entry, trying to get the default basedn.",
2124                NONSENSITIVE
2125            );
2126            // We will never reach here if $type is self::DEFAULTDN, so to avoid code
2127            // code duplication, we'll get the default by re-calling the function.
2128            return $this->getBaseDN( DEFAULTDN );
2129        }
2130
2131        $this->printDebug( "basedn is $ret", NONSENSITIVE );
2132        return $ret;
2133    }
2134
2135    /**
2136     * @param User $user
2137     * @return string|null
2138     */
2139    public static function loadDomain( $user ) {
2140        $user_id = $user->getId();
2141        if ( $user_id != 0 ) {
2142            $row = MediaWikiServices::getInstance()
2143                ->getConnectionProvider()
2144                ->getReplicaDatabase()
2145                ->newSelectQueryBuilder()
2146                ->select( 'domain' )
2147                ->from( 'ldap_domains' )
2148                ->where( [ 'user_id' => $user_id ] )
2149                ->caller( __METHOD__ )
2150                ->fetchRow();
2151
2152            if ( $row ) {
2153                return $row->domain;
2154            }
2155        }
2156
2157        return null;
2158    }
2159
2160    /**
2161     * @param User $user
2162     * @param string $domain
2163     */
2164    public static function saveDomain( $user, $domain ) {
2165        $user_id = $user->getId();
2166        if ( $user_id != 0 ) {
2167            $olddomain = self::loadDomain( $user );
2168            if ( $olddomain ) {
2169                // Check we really need to update domain.
2170                // Otherwise we can receive an error when logging in with
2171                // $wgReadOnly.
2172                if ( $olddomain != $domain ) {
2173                    MediaWikiServices::getInstance()
2174                        ->getConnectionProvider()
2175                        ->getPrimaryDatabase()
2176                        ->newUpdateQueryBuilder()
2177                        ->update( 'ldap_domains' )
2178                        ->set( [ 'domain' => $domain ] )
2179                        ->where( [ 'user_id' => $user_id ] )
2180                        ->caller( __METHOD__ )
2181                        ->execute();
2182                }
2183            } else {
2184                MediaWikiServices::getInstance()
2185                    ->getConnectionProvider()
2186                    ->getPrimaryDatabase()
2187                    ->newInsertQueryBuilder()
2188                    ->insertInto( 'ldap_domains' )
2189                    ->row( [
2190                        'domain' => $domain,
2191                        'user_id' => $user_id
2192                    ] )
2193                    ->caller( __METHOD__ )
2194                    ->execute();
2195            }
2196        }
2197    }
2198
2199}