Completed
Push — master ( d156bb...f314d9 )
by
unknown
26:01 queued 13s
created
apps/user_ldap/lib/Command/Search.php 1 patch
Indentation   +85 added lines, -85 removed lines patch added patch discarded remove patch
@@ -21,95 +21,95 @@
 block discarded – undo
21 21
 use Symfony\Component\Console\Output\OutputInterface;
22 22
 
23 23
 class Search extends Command {
24
-	public function __construct(
25
-		protected IConfig $ocConfig,
26
-		private User_Proxy $userProxy,
27
-		private Group_Proxy $groupProxy,
28
-	) {
29
-		parent::__construct();
30
-	}
24
+    public function __construct(
25
+        protected IConfig $ocConfig,
26
+        private User_Proxy $userProxy,
27
+        private Group_Proxy $groupProxy,
28
+    ) {
29
+        parent::__construct();
30
+    }
31 31
 
32
-	protected function configure(): void {
33
-		$this
34
-			->setName('ldap:search')
35
-			->setDescription('executes a user or group search')
36
-			->addArgument(
37
-				'search',
38
-				InputArgument::REQUIRED,
39
-				'the search string (can be empty)'
40
-			)
41
-			->addOption(
42
-				'group',
43
-				null,
44
-				InputOption::VALUE_NONE,
45
-				'searches groups instead of users'
46
-			)
47
-			->addOption(
48
-				'offset',
49
-				null,
50
-				InputOption::VALUE_REQUIRED,
51
-				'The offset of the result set. Needs to be a multiple of limit. defaults to 0.',
52
-				'0'
53
-			)
54
-			->addOption(
55
-				'limit',
56
-				null,
57
-				InputOption::VALUE_REQUIRED,
58
-				'limit the results. 0 means no limit, defaults to 15',
59
-				'15'
60
-			)
61
-		;
62
-	}
32
+    protected function configure(): void {
33
+        $this
34
+            ->setName('ldap:search')
35
+            ->setDescription('executes a user or group search')
36
+            ->addArgument(
37
+                'search',
38
+                InputArgument::REQUIRED,
39
+                'the search string (can be empty)'
40
+            )
41
+            ->addOption(
42
+                'group',
43
+                null,
44
+                InputOption::VALUE_NONE,
45
+                'searches groups instead of users'
46
+            )
47
+            ->addOption(
48
+                'offset',
49
+                null,
50
+                InputOption::VALUE_REQUIRED,
51
+                'The offset of the result set. Needs to be a multiple of limit. defaults to 0.',
52
+                '0'
53
+            )
54
+            ->addOption(
55
+                'limit',
56
+                null,
57
+                InputOption::VALUE_REQUIRED,
58
+                'limit the results. 0 means no limit, defaults to 15',
59
+                '15'
60
+            )
61
+        ;
62
+    }
63 63
 
64
-	/**
65
-	 * Tests whether the offset and limit options are valid
66
-	 *
67
-	 * @throws \InvalidArgumentException
68
-	 */
69
-	protected function validateOffsetAndLimit(int $offset, int $limit): void {
70
-		if ($limit < 0) {
71
-			throw new \InvalidArgumentException('limit must be  0 or greater');
72
-		}
73
-		if ($offset < 0) {
74
-			throw new \InvalidArgumentException('offset must be 0 or greater');
75
-		}
76
-		if ($limit === 0 && $offset !== 0) {
77
-			throw new \InvalidArgumentException('offset must be 0 if limit is also set to 0');
78
-		}
79
-		if ($offset > 0 && ($offset % $limit !== 0)) {
80
-			throw new \InvalidArgumentException('offset must be a multiple of limit');
81
-		}
82
-	}
64
+    /**
65
+     * Tests whether the offset and limit options are valid
66
+     *
67
+     * @throws \InvalidArgumentException
68
+     */
69
+    protected function validateOffsetAndLimit(int $offset, int $limit): void {
70
+        if ($limit < 0) {
71
+            throw new \InvalidArgumentException('limit must be  0 or greater');
72
+        }
73
+        if ($offset < 0) {
74
+            throw new \InvalidArgumentException('offset must be 0 or greater');
75
+        }
76
+        if ($limit === 0 && $offset !== 0) {
77
+            throw new \InvalidArgumentException('offset must be 0 if limit is also set to 0');
78
+        }
79
+        if ($offset > 0 && ($offset % $limit !== 0)) {
80
+            throw new \InvalidArgumentException('offset must be a multiple of limit');
81
+        }
82
+    }
83 83
 
84
-	protected function execute(InputInterface $input, OutputInterface $output): int {
85
-		$helper = Server::get(Helper::class);
86
-		$configPrefixes = $helper->getServerConfigurationPrefixes(true);
87
-		$ldapWrapper = new LDAP();
84
+    protected function execute(InputInterface $input, OutputInterface $output): int {
85
+        $helper = Server::get(Helper::class);
86
+        $configPrefixes = $helper->getServerConfigurationPrefixes(true);
87
+        $ldapWrapper = new LDAP();
88 88
 
89
-		$offset = (int)$input->getOption('offset');
90
-		$limit = (int)$input->getOption('limit');
91
-		$this->validateOffsetAndLimit($offset, $limit);
89
+        $offset = (int)$input->getOption('offset');
90
+        $limit = (int)$input->getOption('limit');
91
+        $this->validateOffsetAndLimit($offset, $limit);
92 92
 
93
-		if ($input->getOption('group')) {
94
-			$proxy = $this->groupProxy;
95
-			$getMethod = 'getGroups';
96
-			$printID = false;
97
-			// convert the limit of groups to null. This will show all the groups available instead of
98
-			// nothing, and will match the same behaviour the search for users has.
99
-			if ($limit === 0) {
100
-				$limit = null;
101
-			}
102
-		} else {
103
-			$proxy = $this->userProxy;
104
-			$getMethod = 'getDisplayNames';
105
-			$printID = true;
106
-		}
93
+        if ($input->getOption('group')) {
94
+            $proxy = $this->groupProxy;
95
+            $getMethod = 'getGroups';
96
+            $printID = false;
97
+            // convert the limit of groups to null. This will show all the groups available instead of
98
+            // nothing, and will match the same behaviour the search for users has.
99
+            if ($limit === 0) {
100
+                $limit = null;
101
+            }
102
+        } else {
103
+            $proxy = $this->userProxy;
104
+            $getMethod = 'getDisplayNames';
105
+            $printID = true;
106
+        }
107 107
 
108
-		$result = $proxy->$getMethod($input->getArgument('search'), $limit, $offset);
109
-		foreach ($result as $id => $name) {
110
-			$line = $name . ($printID ? ' (' . $id . ')' : '');
111
-			$output->writeln($line);
112
-		}
113
-		return self::SUCCESS;
114
-	}
108
+        $result = $proxy->$getMethod($input->getArgument('search'), $limit, $offset);
109
+        foreach ($result as $id => $name) {
110
+            $line = $name . ($printID ? ' (' . $id . ')' : '');
111
+            $output->writeln($line);
112
+        }
113
+        return self::SUCCESS;
114
+    }
115 115
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Command/SetConfig.php 1 patch
Indentation   +46 added lines, -46 removed lines patch added patch discarded remove patch
@@ -18,54 +18,54 @@
 block discarded – undo
18 18
 use Symfony\Component\Console\Output\OutputInterface;
19 19
 
20 20
 class SetConfig extends Command {
21
-	protected function configure(): void {
22
-		$this
23
-			->setName('ldap:set-config')
24
-			->setDescription('modifies an LDAP configuration')
25
-			->addArgument(
26
-				'configID',
27
-				InputArgument::REQUIRED,
28
-				'the configuration ID'
29
-			)
30
-			->addArgument(
31
-				'configKey',
32
-				InputArgument::REQUIRED,
33
-				'the configuration key'
34
-			)
35
-			->addArgument(
36
-				'configValue',
37
-				InputArgument::REQUIRED,
38
-				'the new configuration value'
39
-			)
40
-		;
41
-	}
21
+    protected function configure(): void {
22
+        $this
23
+            ->setName('ldap:set-config')
24
+            ->setDescription('modifies an LDAP configuration')
25
+            ->addArgument(
26
+                'configID',
27
+                InputArgument::REQUIRED,
28
+                'the configuration ID'
29
+            )
30
+            ->addArgument(
31
+                'configKey',
32
+                InputArgument::REQUIRED,
33
+                'the configuration key'
34
+            )
35
+            ->addArgument(
36
+                'configValue',
37
+                InputArgument::REQUIRED,
38
+                'the new configuration value'
39
+            )
40
+        ;
41
+    }
42 42
 
43
-	protected function execute(InputInterface $input, OutputInterface $output): int {
44
-		$helper = Server::get(Helper::class);
45
-		$availableConfigs = $helper->getServerConfigurationPrefixes();
46
-		$configID = $input->getArgument('configID');
47
-		if (!in_array($configID, $availableConfigs)) {
48
-			$output->writeln('Invalid configID');
49
-			return self::FAILURE;
50
-		}
43
+    protected function execute(InputInterface $input, OutputInterface $output): int {
44
+        $helper = Server::get(Helper::class);
45
+        $availableConfigs = $helper->getServerConfigurationPrefixes();
46
+        $configID = $input->getArgument('configID');
47
+        if (!in_array($configID, $availableConfigs)) {
48
+            $output->writeln('Invalid configID');
49
+            return self::FAILURE;
50
+        }
51 51
 
52
-		$this->setValue(
53
-			$configID,
54
-			$input->getArgument('configKey'),
55
-			$input->getArgument('configValue')
56
-		);
57
-		return self::SUCCESS;
58
-	}
52
+        $this->setValue(
53
+            $configID,
54
+            $input->getArgument('configKey'),
55
+            $input->getArgument('configValue')
56
+        );
57
+        return self::SUCCESS;
58
+    }
59 59
 
60
-	/**
61
-	 * save the configuration value as provided
62
-	 */
63
-	protected function setValue(string $configID, string $key, string $value): void {
64
-		$configHolder = new Configuration($configID);
65
-		$configHolder->$key = $value;
66
-		$configHolder->saveConfiguration();
60
+    /**
61
+     * save the configuration value as provided
62
+     */
63
+    protected function setValue(string $configID, string $key, string $value): void {
64
+        $configHolder = new Configuration($configID);
65
+        $configHolder->$key = $value;
66
+        $configHolder->saveConfiguration();
67 67
 
68
-		$connectionFactory = new ConnectionFactory(new LDAP());
69
-		$connectionFactory->get($configID)->clearCache();
70
-	}
68
+        $connectionFactory = new ConnectionFactory(new LDAP());
69
+        $connectionFactory->get($configID)->clearCache();
70
+    }
71 71
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Settings/Admin.php 1 patch
Indentation   +60 added lines, -60 removed lines patch added patch discarded remove patch
@@ -14,74 +14,74 @@
 block discarded – undo
14 14
 use OCP\Template\ITemplateManager;
15 15
 
16 16
 class Admin implements IDelegatedSettings {
17
-	public function __construct(
18
-		private IL10N $l,
19
-		private ITemplateManager $templateManager,
20
-	) {
21
-	}
17
+    public function __construct(
18
+        private IL10N $l,
19
+        private ITemplateManager $templateManager,
20
+    ) {
21
+    }
22 22
 
23
-	/**
24
-	 * @return TemplateResponse
25
-	 */
26
-	public function getForm() {
27
-		$helper = Server::get(Helper::class);
28
-		$prefixes = $helper->getServerConfigurationPrefixes();
29
-		if (count($prefixes) === 0) {
30
-			$newPrefix = $helper->getNextServerConfigurationPrefix();
31
-			$config = new Configuration($newPrefix, false);
32
-			$config->setConfiguration($config->getDefaults());
33
-			$config->saveConfiguration();
34
-			$prefixes[] = $newPrefix;
35
-		}
23
+    /**
24
+     * @return TemplateResponse
25
+     */
26
+    public function getForm() {
27
+        $helper = Server::get(Helper::class);
28
+        $prefixes = $helper->getServerConfigurationPrefixes();
29
+        if (count($prefixes) === 0) {
30
+            $newPrefix = $helper->getNextServerConfigurationPrefix();
31
+            $config = new Configuration($newPrefix, false);
32
+            $config->setConfiguration($config->getDefaults());
33
+            $config->saveConfiguration();
34
+            $prefixes[] = $newPrefix;
35
+        }
36 36
 
37
-		$hosts = $helper->getServerConfigurationHosts();
37
+        $hosts = $helper->getServerConfigurationHosts();
38 38
 
39
-		$wControls = $this->templateManager->getTemplate('user_ldap', 'part.wizardcontrols');
40
-		$wControls = $wControls->fetchPage();
41
-		$sControls = $this->templateManager->getTemplate('user_ldap', 'part.settingcontrols');
42
-		$sControls = $sControls->fetchPage();
39
+        $wControls = $this->templateManager->getTemplate('user_ldap', 'part.wizardcontrols');
40
+        $wControls = $wControls->fetchPage();
41
+        $sControls = $this->templateManager->getTemplate('user_ldap', 'part.settingcontrols');
42
+        $sControls = $sControls->fetchPage();
43 43
 
44
-		$parameters = [];
45
-		$parameters['serverConfigurationPrefixes'] = $prefixes;
46
-		$parameters['serverConfigurationHosts'] = $hosts;
47
-		$parameters['settingControls'] = $sControls;
48
-		$parameters['wizardControls'] = $wControls;
44
+        $parameters = [];
45
+        $parameters['serverConfigurationPrefixes'] = $prefixes;
46
+        $parameters['serverConfigurationHosts'] = $hosts;
47
+        $parameters['settingControls'] = $sControls;
48
+        $parameters['wizardControls'] = $wControls;
49 49
 
50
-		// assign default values
51
-		if (!isset($config)) {
52
-			$config = new Configuration('', false);
53
-		}
54
-		$defaults = $config->getDefaults();
55
-		foreach ($defaults as $key => $default) {
56
-			$parameters[$key . '_default'] = $default;
57
-		}
50
+        // assign default values
51
+        if (!isset($config)) {
52
+            $config = new Configuration('', false);
53
+        }
54
+        $defaults = $config->getDefaults();
55
+        foreach ($defaults as $key => $default) {
56
+            $parameters[$key . '_default'] = $default;
57
+        }
58 58
 
59
-		return new TemplateResponse('user_ldap', 'settings', $parameters);
60
-	}
59
+        return new TemplateResponse('user_ldap', 'settings', $parameters);
60
+    }
61 61
 
62
-	/**
63
-	 * @return string the section ID, e.g. 'sharing'
64
-	 */
65
-	public function getSection() {
66
-		return 'ldap';
67
-	}
62
+    /**
63
+     * @return string the section ID, e.g. 'sharing'
64
+     */
65
+    public function getSection() {
66
+        return 'ldap';
67
+    }
68 68
 
69
-	/**
70
-	 * @return int whether the form should be rather on the top or bottom of
71
-	 *             the admin section. The forms are arranged in ascending order of the
72
-	 *             priority values. It is required to return a value between 0 and 100.
73
-	 *
74
-	 * E.g.: 70
75
-	 */
76
-	public function getPriority() {
77
-		return 5;
78
-	}
69
+    /**
70
+     * @return int whether the form should be rather on the top or bottom of
71
+     *             the admin section. The forms are arranged in ascending order of the
72
+     *             priority values. It is required to return a value between 0 and 100.
73
+     *
74
+     * E.g.: 70
75
+     */
76
+    public function getPriority() {
77
+        return 5;
78
+    }
79 79
 
80
-	public function getName(): ?string {
81
-		return null; // Only one setting in this section
82
-	}
80
+    public function getName(): ?string {
81
+        return null; // Only one setting in this section
82
+    }
83 83
 
84
-	public function getAuthorizedAppConfig(): array {
85
-		return []; // Custom controller
86
-	}
84
+    public function getAuthorizedAppConfig(): array {
85
+        return []; // Custom controller
86
+    }
87 87
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Helper.php 2 patches
Indentation   +286 added lines, -286 removed lines patch added patch discarded remove patch
@@ -14,290 +14,290 @@
 block discarded – undo
14 14
 use OCP\Server;
15 15
 
16 16
 class Helper {
17
-	/** @var CappedMemoryCache<string> */
18
-	protected CappedMemoryCache $sanitizeDnCache;
19
-
20
-	public function __construct(
21
-		private IAppConfig $appConfig,
22
-		private IDBConnection $connection,
23
-	) {
24
-		$this->sanitizeDnCache = new CappedMemoryCache(10000);
25
-	}
26
-
27
-	/**
28
-	 * returns prefixes for each saved LDAP/AD server configuration.
29
-	 *
30
-	 * @param bool $activeConfigurations optional, whether only active configuration shall be
31
-	 *                                   retrieved, defaults to false
32
-	 * @return array with a list of the available prefixes
33
-	 *
34
-	 * Configuration prefixes are used to set up configurations for n LDAP or
35
-	 * AD servers. Since configuration is stored in the database, table
36
-	 * appconfig under appid user_ldap, the common identifiers in column
37
-	 * 'configkey' have a prefix. The prefix for the very first server
38
-	 * configuration is empty.
39
-	 * Configkey Examples:
40
-	 * Server 1: ldap_login_filter
41
-	 * Server 2: s1_ldap_login_filter
42
-	 * Server 3: s2_ldap_login_filter
43
-	 *
44
-	 * The prefix needs to be passed to the constructor of Connection class,
45
-	 * except the default (first) server shall be connected to.
46
-	 *
47
-	 */
48
-	public function getServerConfigurationPrefixes(bool $activeConfigurations = false): array {
49
-		$all = $this->getAllServerConfigurationPrefixes();
50
-		if (!$activeConfigurations) {
51
-			return $all;
52
-		}
53
-		return array_values(array_filter(
54
-			$all,
55
-			fn (string $prefix): bool => ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') === '1')
56
-		));
57
-	}
58
-
59
-	protected function getAllServerConfigurationPrefixes(): array {
60
-		$unfilled = ['UNFILLED'];
61
-		$prefixes = $this->appConfig->getValueArray('user_ldap', 'configuration_prefixes', $unfilled);
62
-		if ($prefixes !== $unfilled) {
63
-			return $prefixes;
64
-		}
65
-
66
-		/* Fallback to browsing key for migration from Nextcloud<32 */
67
-		$referenceConfigkey = 'ldap_configuration_active';
68
-
69
-		$keys = $this->getServersConfig($referenceConfigkey);
70
-
71
-		$prefixes = [];
72
-		foreach ($keys as $key) {
73
-			$len = strlen($key) - strlen($referenceConfigkey);
74
-			$prefixes[] = substr($key, 0, $len);
75
-		}
76
-		sort($prefixes);
77
-
78
-		$this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', $prefixes);
79
-
80
-		return $prefixes;
81
-	}
82
-
83
-	/**
84
-	 *
85
-	 * determines the host for every configured connection
86
-	 *
87
-	 * @return array<string,string> an array with configprefix as keys
88
-	 *
89
-	 */
90
-	public function getServerConfigurationHosts(): array {
91
-		$prefixes = $this->getServerConfigurationPrefixes();
92
-
93
-		$referenceConfigkey = 'ldap_host';
94
-		$result = [];
95
-		foreach ($prefixes as $prefix) {
96
-			$result[$prefix] = $this->appConfig->getValueString('user_ldap', $prefix . $referenceConfigkey);
97
-		}
98
-
99
-		return $result;
100
-	}
101
-
102
-	/**
103
-	 * return the next available configuration prefix and register it as used
104
-	 */
105
-	public function getNextServerConfigurationPrefix(): string {
106
-		$prefixes = $this->getServerConfigurationPrefixes();
107
-
108
-		if (count($prefixes) === 0) {
109
-			$prefix = 's01';
110
-		} else {
111
-			sort($prefixes);
112
-			$lastKey = array_pop($prefixes);
113
-			$lastNumber = (int)str_replace('s', '', $lastKey);
114
-			$prefix = 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT);
115
-		}
116
-
117
-		$prefixes[] = $prefix;
118
-		$this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', $prefixes);
119
-		return $prefix;
120
-	}
121
-
122
-	private function getServersConfig(string $value): array {
123
-		$regex = '/' . $value . '$/S';
124
-
125
-		$keys = $this->appConfig->getKeys('user_ldap');
126
-		$result = [];
127
-		foreach ($keys as $key) {
128
-			if (preg_match($regex, $key) === 1) {
129
-				$result[] = $key;
130
-			}
131
-		}
132
-
133
-		return $result;
134
-	}
135
-
136
-	/**
137
-	 * deletes a given saved LDAP/AD server configuration.
138
-	 *
139
-	 * @param string $prefix the configuration prefix of the config to delete
140
-	 * @return bool true on success, false otherwise
141
-	 */
142
-	public function deleteServerConfiguration($prefix) {
143
-		$prefixes = $this->getServerConfigurationPrefixes();
144
-		$index = array_search($prefix, $prefixes);
145
-		if ($index === false) {
146
-			return false;
147
-		}
148
-
149
-		$query = $this->connection->getQueryBuilder();
150
-		$query->delete('appconfig')
151
-			->where($query->expr()->eq('appid', $query->createNamedParameter('user_ldap')))
152
-			->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string)$prefix . '%')))
153
-			->andWhere($query->expr()->notIn('configkey', $query->createNamedParameter([
154
-				'enabled',
155
-				'installed_version',
156
-				'types',
157
-				'bgjUpdateGroupsLastRun',
158
-			], IQueryBuilder::PARAM_STR_ARRAY)));
159
-
160
-		if (empty($prefix)) {
161
-			$query->andWhere($query->expr()->notLike('configkey', $query->createNamedParameter('s%')));
162
-		}
163
-
164
-		$deletedRows = $query->executeStatement();
165
-
166
-		unset($prefixes[$index]);
167
-		$this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', array_values($prefixes));
168
-
169
-		return $deletedRows !== 0;
170
-	}
171
-
172
-	/**
173
-	 * checks whether there is one or more disabled LDAP configurations
174
-	 */
175
-	public function haveDisabledConfigurations(): bool {
176
-		$all = $this->getServerConfigurationPrefixes();
177
-		foreach ($all as $prefix) {
178
-			if ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') !== '1') {
179
-				return true;
180
-			}
181
-		}
182
-		return false;
183
-	}
184
-
185
-	/**
186
-	 * extracts the domain from a given URL
187
-	 *
188
-	 * @param string $url the URL
189
-	 * @return string|false domain as string on success, false otherwise
190
-	 */
191
-	public function getDomainFromURL($url) {
192
-		$uinfo = parse_url($url);
193
-		if (!is_array($uinfo)) {
194
-			return false;
195
-		}
196
-
197
-		$domain = false;
198
-		if (isset($uinfo['host'])) {
199
-			$domain = $uinfo['host'];
200
-		} elseif (isset($uinfo['path'])) {
201
-			$domain = $uinfo['path'];
202
-		}
203
-
204
-		return $domain;
205
-	}
206
-
207
-	/**
208
-	 * sanitizes a DN received from the LDAP server
209
-	 *
210
-	 * This is used and done to have a stable format of DNs that can be compared
211
-	 * and identified again. The input DN value is modified as following:
212
-	 *
213
-	 * 1) whitespaces after commas are removed
214
-	 * 2) the DN is turned to lower-case
215
-	 * 3) the DN is escaped according to RFC 2253
216
-	 *
217
-	 * When a future DN is supposed to be used as a base parameter, it has to be
218
-	 * run through DNasBaseParameter() first, to recode \5c into a backslash
219
-	 * again, otherwise the search or read operation will fail with LDAP error
220
-	 * 32, NO_SUCH_OBJECT. Regular usage in LDAP filters requires the backslash
221
-	 * being escaped, however.
222
-	 *
223
-	 * Internally, DNs are stored in their sanitized form.
224
-	 *
225
-	 * @param array|string $dn the DN in question
226
-	 * @return array|string the sanitized DN
227
-	 */
228
-	public function sanitizeDN($dn) {
229
-		//treating multiple base DNs
230
-		if (is_array($dn)) {
231
-			$result = [];
232
-			foreach ($dn as $singleDN) {
233
-				$result[] = $this->sanitizeDN($singleDN);
234
-			}
235
-			return $result;
236
-		}
237
-
238
-		if (!is_string($dn)) {
239
-			throw new \LogicException('String expected ' . \gettype($dn) . ' given');
240
-		}
241
-
242
-		if (($sanitizedDn = $this->sanitizeDnCache->get($dn)) !== null) {
243
-			return $sanitizedDn;
244
-		}
245
-
246
-		//OID sometimes gives back DNs with whitespace after the comma
247
-		// a la "uid=foo, cn=bar, dn=..." We need to tackle this!
248
-		$sanitizedDn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
249
-
250
-		//make comparisons and everything work
251
-		$sanitizedDn = mb_strtolower($sanitizedDn, 'UTF-8');
252
-
253
-		//escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
254
-		//to use the DN in search filters, \ needs to be escaped to \5c additionally
255
-		//to use them in bases, we convert them back to simple backslashes in readAttribute()
256
-		$replacements = [
257
-			'\,' => '\5c2C',
258
-			'\=' => '\5c3D',
259
-			'\+' => '\5c2B',
260
-			'\<' => '\5c3C',
261
-			'\>' => '\5c3E',
262
-			'\;' => '\5c3B',
263
-			'\"' => '\5c22',
264
-			'\#' => '\5c23',
265
-			'(' => '\28',
266
-			')' => '\29',
267
-			'*' => '\2A',
268
-		];
269
-		$sanitizedDn = str_replace(array_keys($replacements), array_values($replacements), $sanitizedDn);
270
-		$this->sanitizeDnCache->set($dn, $sanitizedDn);
271
-
272
-		return $sanitizedDn;
273
-	}
274
-
275
-	/**
276
-	 * converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
277
-	 *
278
-	 * @param string $dn the DN
279
-	 * @return string
280
-	 */
281
-	public function DNasBaseParameter($dn) {
282
-		return str_ireplace('\\5c', '\\', $dn);
283
-	}
284
-
285
-	/**
286
-	 * listens to a hook thrown by server2server sharing and replaces the given
287
-	 * login name by a username, if it matches an LDAP user.
288
-	 *
289
-	 * @param array $param contains a reference to a $uid var under 'uid' key
290
-	 * @throws \Exception
291
-	 */
292
-	public static function loginName2UserName($param): void {
293
-		if (!isset($param['uid'])) {
294
-			throw new \Exception('key uid is expected to be set in $param');
295
-		}
296
-
297
-		$userBackend = Server::get(User_Proxy::class);
298
-		$uid = $userBackend->loginName2UserName($param['uid']);
299
-		if ($uid !== false) {
300
-			$param['uid'] = $uid;
301
-		}
302
-	}
17
+    /** @var CappedMemoryCache<string> */
18
+    protected CappedMemoryCache $sanitizeDnCache;
19
+
20
+    public function __construct(
21
+        private IAppConfig $appConfig,
22
+        private IDBConnection $connection,
23
+    ) {
24
+        $this->sanitizeDnCache = new CappedMemoryCache(10000);
25
+    }
26
+
27
+    /**
28
+     * returns prefixes for each saved LDAP/AD server configuration.
29
+     *
30
+     * @param bool $activeConfigurations optional, whether only active configuration shall be
31
+     *                                   retrieved, defaults to false
32
+     * @return array with a list of the available prefixes
33
+     *
34
+     * Configuration prefixes are used to set up configurations for n LDAP or
35
+     * AD servers. Since configuration is stored in the database, table
36
+     * appconfig under appid user_ldap, the common identifiers in column
37
+     * 'configkey' have a prefix. The prefix for the very first server
38
+     * configuration is empty.
39
+     * Configkey Examples:
40
+     * Server 1: ldap_login_filter
41
+     * Server 2: s1_ldap_login_filter
42
+     * Server 3: s2_ldap_login_filter
43
+     *
44
+     * The prefix needs to be passed to the constructor of Connection class,
45
+     * except the default (first) server shall be connected to.
46
+     *
47
+     */
48
+    public function getServerConfigurationPrefixes(bool $activeConfigurations = false): array {
49
+        $all = $this->getAllServerConfigurationPrefixes();
50
+        if (!$activeConfigurations) {
51
+            return $all;
52
+        }
53
+        return array_values(array_filter(
54
+            $all,
55
+            fn (string $prefix): bool => ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') === '1')
56
+        ));
57
+    }
58
+
59
+    protected function getAllServerConfigurationPrefixes(): array {
60
+        $unfilled = ['UNFILLED'];
61
+        $prefixes = $this->appConfig->getValueArray('user_ldap', 'configuration_prefixes', $unfilled);
62
+        if ($prefixes !== $unfilled) {
63
+            return $prefixes;
64
+        }
65
+
66
+        /* Fallback to browsing key for migration from Nextcloud<32 */
67
+        $referenceConfigkey = 'ldap_configuration_active';
68
+
69
+        $keys = $this->getServersConfig($referenceConfigkey);
70
+
71
+        $prefixes = [];
72
+        foreach ($keys as $key) {
73
+            $len = strlen($key) - strlen($referenceConfigkey);
74
+            $prefixes[] = substr($key, 0, $len);
75
+        }
76
+        sort($prefixes);
77
+
78
+        $this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', $prefixes);
79
+
80
+        return $prefixes;
81
+    }
82
+
83
+    /**
84
+     *
85
+     * determines the host for every configured connection
86
+     *
87
+     * @return array<string,string> an array with configprefix as keys
88
+     *
89
+     */
90
+    public function getServerConfigurationHosts(): array {
91
+        $prefixes = $this->getServerConfigurationPrefixes();
92
+
93
+        $referenceConfigkey = 'ldap_host';
94
+        $result = [];
95
+        foreach ($prefixes as $prefix) {
96
+            $result[$prefix] = $this->appConfig->getValueString('user_ldap', $prefix . $referenceConfigkey);
97
+        }
98
+
99
+        return $result;
100
+    }
101
+
102
+    /**
103
+     * return the next available configuration prefix and register it as used
104
+     */
105
+    public function getNextServerConfigurationPrefix(): string {
106
+        $prefixes = $this->getServerConfigurationPrefixes();
107
+
108
+        if (count($prefixes) === 0) {
109
+            $prefix = 's01';
110
+        } else {
111
+            sort($prefixes);
112
+            $lastKey = array_pop($prefixes);
113
+            $lastNumber = (int)str_replace('s', '', $lastKey);
114
+            $prefix = 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT);
115
+        }
116
+
117
+        $prefixes[] = $prefix;
118
+        $this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', $prefixes);
119
+        return $prefix;
120
+    }
121
+
122
+    private function getServersConfig(string $value): array {
123
+        $regex = '/' . $value . '$/S';
124
+
125
+        $keys = $this->appConfig->getKeys('user_ldap');
126
+        $result = [];
127
+        foreach ($keys as $key) {
128
+            if (preg_match($regex, $key) === 1) {
129
+                $result[] = $key;
130
+            }
131
+        }
132
+
133
+        return $result;
134
+    }
135
+
136
+    /**
137
+     * deletes a given saved LDAP/AD server configuration.
138
+     *
139
+     * @param string $prefix the configuration prefix of the config to delete
140
+     * @return bool true on success, false otherwise
141
+     */
142
+    public function deleteServerConfiguration($prefix) {
143
+        $prefixes = $this->getServerConfigurationPrefixes();
144
+        $index = array_search($prefix, $prefixes);
145
+        if ($index === false) {
146
+            return false;
147
+        }
148
+
149
+        $query = $this->connection->getQueryBuilder();
150
+        $query->delete('appconfig')
151
+            ->where($query->expr()->eq('appid', $query->createNamedParameter('user_ldap')))
152
+            ->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string)$prefix . '%')))
153
+            ->andWhere($query->expr()->notIn('configkey', $query->createNamedParameter([
154
+                'enabled',
155
+                'installed_version',
156
+                'types',
157
+                'bgjUpdateGroupsLastRun',
158
+            ], IQueryBuilder::PARAM_STR_ARRAY)));
159
+
160
+        if (empty($prefix)) {
161
+            $query->andWhere($query->expr()->notLike('configkey', $query->createNamedParameter('s%')));
162
+        }
163
+
164
+        $deletedRows = $query->executeStatement();
165
+
166
+        unset($prefixes[$index]);
167
+        $this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', array_values($prefixes));
168
+
169
+        return $deletedRows !== 0;
170
+    }
171
+
172
+    /**
173
+     * checks whether there is one or more disabled LDAP configurations
174
+     */
175
+    public function haveDisabledConfigurations(): bool {
176
+        $all = $this->getServerConfigurationPrefixes();
177
+        foreach ($all as $prefix) {
178
+            if ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') !== '1') {
179
+                return true;
180
+            }
181
+        }
182
+        return false;
183
+    }
184
+
185
+    /**
186
+     * extracts the domain from a given URL
187
+     *
188
+     * @param string $url the URL
189
+     * @return string|false domain as string on success, false otherwise
190
+     */
191
+    public function getDomainFromURL($url) {
192
+        $uinfo = parse_url($url);
193
+        if (!is_array($uinfo)) {
194
+            return false;
195
+        }
196
+
197
+        $domain = false;
198
+        if (isset($uinfo['host'])) {
199
+            $domain = $uinfo['host'];
200
+        } elseif (isset($uinfo['path'])) {
201
+            $domain = $uinfo['path'];
202
+        }
203
+
204
+        return $domain;
205
+    }
206
+
207
+    /**
208
+     * sanitizes a DN received from the LDAP server
209
+     *
210
+     * This is used and done to have a stable format of DNs that can be compared
211
+     * and identified again. The input DN value is modified as following:
212
+     *
213
+     * 1) whitespaces after commas are removed
214
+     * 2) the DN is turned to lower-case
215
+     * 3) the DN is escaped according to RFC 2253
216
+     *
217
+     * When a future DN is supposed to be used as a base parameter, it has to be
218
+     * run through DNasBaseParameter() first, to recode \5c into a backslash
219
+     * again, otherwise the search or read operation will fail with LDAP error
220
+     * 32, NO_SUCH_OBJECT. Regular usage in LDAP filters requires the backslash
221
+     * being escaped, however.
222
+     *
223
+     * Internally, DNs are stored in their sanitized form.
224
+     *
225
+     * @param array|string $dn the DN in question
226
+     * @return array|string the sanitized DN
227
+     */
228
+    public function sanitizeDN($dn) {
229
+        //treating multiple base DNs
230
+        if (is_array($dn)) {
231
+            $result = [];
232
+            foreach ($dn as $singleDN) {
233
+                $result[] = $this->sanitizeDN($singleDN);
234
+            }
235
+            return $result;
236
+        }
237
+
238
+        if (!is_string($dn)) {
239
+            throw new \LogicException('String expected ' . \gettype($dn) . ' given');
240
+        }
241
+
242
+        if (($sanitizedDn = $this->sanitizeDnCache->get($dn)) !== null) {
243
+            return $sanitizedDn;
244
+        }
245
+
246
+        //OID sometimes gives back DNs with whitespace after the comma
247
+        // a la "uid=foo, cn=bar, dn=..." We need to tackle this!
248
+        $sanitizedDn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
249
+
250
+        //make comparisons and everything work
251
+        $sanitizedDn = mb_strtolower($sanitizedDn, 'UTF-8');
252
+
253
+        //escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
254
+        //to use the DN in search filters, \ needs to be escaped to \5c additionally
255
+        //to use them in bases, we convert them back to simple backslashes in readAttribute()
256
+        $replacements = [
257
+            '\,' => '\5c2C',
258
+            '\=' => '\5c3D',
259
+            '\+' => '\5c2B',
260
+            '\<' => '\5c3C',
261
+            '\>' => '\5c3E',
262
+            '\;' => '\5c3B',
263
+            '\"' => '\5c22',
264
+            '\#' => '\5c23',
265
+            '(' => '\28',
266
+            ')' => '\29',
267
+            '*' => '\2A',
268
+        ];
269
+        $sanitizedDn = str_replace(array_keys($replacements), array_values($replacements), $sanitizedDn);
270
+        $this->sanitizeDnCache->set($dn, $sanitizedDn);
271
+
272
+        return $sanitizedDn;
273
+    }
274
+
275
+    /**
276
+     * converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
277
+     *
278
+     * @param string $dn the DN
279
+     * @return string
280
+     */
281
+    public function DNasBaseParameter($dn) {
282
+        return str_ireplace('\\5c', '\\', $dn);
283
+    }
284
+
285
+    /**
286
+     * listens to a hook thrown by server2server sharing and replaces the given
287
+     * login name by a username, if it matches an LDAP user.
288
+     *
289
+     * @param array $param contains a reference to a $uid var under 'uid' key
290
+     * @throws \Exception
291
+     */
292
+    public static function loginName2UserName($param): void {
293
+        if (!isset($param['uid'])) {
294
+            throw new \Exception('key uid is expected to be set in $param');
295
+        }
296
+
297
+        $userBackend = Server::get(User_Proxy::class);
298
+        $uid = $userBackend->loginName2UserName($param['uid']);
299
+        if ($uid !== false) {
300
+            $param['uid'] = $uid;
301
+        }
302
+    }
303 303
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -52,7 +52,7 @@  discard block
 block discarded – undo
52 52
 		}
53 53
 		return array_values(array_filter(
54 54
 			$all,
55
-			fn (string $prefix): bool => ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') === '1')
55
+			fn (string $prefix): bool => ($this->appConfig->getValueString('user_ldap', $prefix.'ldap_configuration_active') === '1')
56 56
 		));
57 57
 	}
58 58
 
@@ -93,7 +93,7 @@  discard block
 block discarded – undo
93 93
 		$referenceConfigkey = 'ldap_host';
94 94
 		$result = [];
95 95
 		foreach ($prefixes as $prefix) {
96
-			$result[$prefix] = $this->appConfig->getValueString('user_ldap', $prefix . $referenceConfigkey);
96
+			$result[$prefix] = $this->appConfig->getValueString('user_ldap', $prefix.$referenceConfigkey);
97 97
 		}
98 98
 
99 99
 		return $result;
@@ -110,8 +110,8 @@  discard block
 block discarded – undo
110 110
 		} else {
111 111
 			sort($prefixes);
112 112
 			$lastKey = array_pop($prefixes);
113
-			$lastNumber = (int)str_replace('s', '', $lastKey);
114
-			$prefix = 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT);
113
+			$lastNumber = (int) str_replace('s', '', $lastKey);
114
+			$prefix = 's'.str_pad((string) ($lastNumber + 1), 2, '0', STR_PAD_LEFT);
115 115
 		}
116 116
 
117 117
 		$prefixes[] = $prefix;
@@ -120,7 +120,7 @@  discard block
 block discarded – undo
120 120
 	}
121 121
 
122 122
 	private function getServersConfig(string $value): array {
123
-		$regex = '/' . $value . '$/S';
123
+		$regex = '/'.$value.'$/S';
124 124
 
125 125
 		$keys = $this->appConfig->getKeys('user_ldap');
126 126
 		$result = [];
@@ -149,7 +149,7 @@  discard block
 block discarded – undo
149 149
 		$query = $this->connection->getQueryBuilder();
150 150
 		$query->delete('appconfig')
151 151
 			->where($query->expr()->eq('appid', $query->createNamedParameter('user_ldap')))
152
-			->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string)$prefix . '%')))
152
+			->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string) $prefix.'%')))
153 153
 			->andWhere($query->expr()->notIn('configkey', $query->createNamedParameter([
154 154
 				'enabled',
155 155
 				'installed_version',
@@ -175,7 +175,7 @@  discard block
 block discarded – undo
175 175
 	public function haveDisabledConfigurations(): bool {
176 176
 		$all = $this->getServerConfigurationPrefixes();
177 177
 		foreach ($all as $prefix) {
178
-			if ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') !== '1') {
178
+			if ($this->appConfig->getValueString('user_ldap', $prefix.'ldap_configuration_active') !== '1') {
179 179
 				return true;
180 180
 			}
181 181
 		}
@@ -236,7 +236,7 @@  discard block
 block discarded – undo
236 236
 		}
237 237
 
238 238
 		if (!is_string($dn)) {
239
-			throw new \LogicException('String expected ' . \gettype($dn) . ' given');
239
+			throw new \LogicException('String expected '.\gettype($dn).' given');
240 240
 		}
241 241
 
242 242
 		if (($sanitizedDn = $this->sanitizeDnCache->get($dn)) !== null) {
Please login to merge, or discard this patch.
apps/user_ldap/lib/Connection.php 1 patch
Indentation   +683 added lines, -683 removed lines patch added patch discarded remove patch
@@ -94,687 +94,687 @@
 block discarded – undo
94 94
  * @property string $ldapAttributePronouns
95 95
  */
96 96
 class Connection extends LDAPUtility {
97
-	private ?\LDAP\Connection $ldapConnectionRes = null;
98
-	private bool $configured = false;
99
-
100
-	/**
101
-	 * @var bool whether connection should be kept on __destruct
102
-	 */
103
-	private bool $dontDestruct = false;
104
-
105
-	/**
106
-	 * @var bool runtime flag that indicates whether supported primary groups are available
107
-	 */
108
-	public $hasPrimaryGroups = true;
109
-
110
-	/**
111
-	 * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
112
-	 */
113
-	public $hasGidNumber = true;
114
-
115
-	/**
116
-	 * @var ICache|null
117
-	 */
118
-	protected $cache = null;
119
-
120
-	/** @var Configuration settings handler * */
121
-	protected $configuration;
122
-
123
-	/**
124
-	 * @var bool
125
-	 */
126
-	protected $doNotValidate = false;
127
-
128
-	/**
129
-	 * @var bool
130
-	 */
131
-	protected $ignoreValidation = false;
132
-
133
-	/**
134
-	 * @var array{sum?: string, result?: bool}
135
-	 */
136
-	protected $bindResult = [];
137
-
138
-	protected LoggerInterface $logger;
139
-	private IL10N $l10n;
140
-
141
-	/**
142
-	 * Constructor
143
-	 * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
144
-	 * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
145
-	 */
146
-	public function __construct(
147
-		ILDAPWrapper $ldap,
148
-		private string $configPrefix = '',
149
-		private ?string $configID = 'user_ldap',
150
-	) {
151
-		parent::__construct($ldap);
152
-		$this->configuration = new Configuration($this->configPrefix, !is_null($this->configID));
153
-		$memcache = Server::get(ICacheFactory::class);
154
-		if ($memcache->isAvailable()) {
155
-			$this->cache = $memcache->createDistributed();
156
-		}
157
-		$helper = Server::get(Helper::class);
158
-		$this->doNotValidate = !in_array($this->configPrefix,
159
-			$helper->getServerConfigurationPrefixes());
160
-		$this->logger = Server::get(LoggerInterface::class);
161
-		$this->l10n = Util::getL10N('user_ldap');
162
-	}
163
-
164
-	public function __destruct() {
165
-		if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
166
-			@$this->ldap->unbind($this->ldapConnectionRes);
167
-			$this->bindResult = [];
168
-		}
169
-	}
170
-
171
-	/**
172
-	 * defines behaviour when the instance is cloned
173
-	 */
174
-	public function __clone() {
175
-		$this->configuration = new Configuration($this->configPrefix,
176
-			!is_null($this->configID));
177
-		if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
178
-			$this->bindResult = [];
179
-		}
180
-		$this->ldapConnectionRes = null;
181
-		$this->dontDestruct = true;
182
-	}
183
-
184
-	public function __get(string $name) {
185
-		if (!$this->configured) {
186
-			$this->readConfiguration();
187
-		}
188
-
189
-		return $this->configuration->$name;
190
-	}
191
-
192
-	/**
193
-	 * @param string $name
194
-	 * @param mixed $value
195
-	 */
196
-	public function __set($name, $value) {
197
-		$this->doNotValidate = false;
198
-		$before = $this->configuration->$name;
199
-		$this->configuration->$name = $value;
200
-		$after = $this->configuration->$name;
201
-		if ($before !== $after) {
202
-			if ($this->configID !== '' && $this->configID !== null) {
203
-				$this->configuration->saveConfiguration();
204
-			}
205
-			$this->validateConfiguration();
206
-		}
207
-	}
208
-
209
-	/**
210
-	 * @param string $rule
211
-	 * @return array
212
-	 * @throws \RuntimeException
213
-	 */
214
-	public function resolveRule($rule) {
215
-		return $this->configuration->resolveRule($rule);
216
-	}
217
-
218
-	/**
219
-	 * sets whether the result of the configuration validation shall
220
-	 * be ignored when establishing the connection. Used by the Wizard
221
-	 * in early configuration state.
222
-	 * @param bool $state
223
-	 */
224
-	public function setIgnoreValidation($state) {
225
-		$this->ignoreValidation = (bool)$state;
226
-	}
227
-
228
-	/**
229
-	 * initializes the LDAP backend
230
-	 * @param bool $force read the config settings no matter what
231
-	 */
232
-	public function init($force = false) {
233
-		$this->readConfiguration($force);
234
-		$this->establishConnection();
235
-	}
236
-
237
-	/**
238
-	 * @return \LDAP\Connection The LDAP resource
239
-	 */
240
-	public function getConnectionResource(): \LDAP\Connection {
241
-		if (!$this->ldapConnectionRes) {
242
-			$this->init();
243
-		}
244
-		if (is_null($this->ldapConnectionRes)) {
245
-			$this->logger->error(
246
-				'No LDAP Connection to server ' . $this->configuration->ldapHost,
247
-				['app' => 'user_ldap']
248
-			);
249
-			throw new ServerNotAvailableException('Connection to LDAP server could not be established');
250
-		}
251
-		return $this->ldapConnectionRes;
252
-	}
253
-
254
-	/**
255
-	 * resets the connection resource
256
-	 */
257
-	public function resetConnectionResource(): void {
258
-		if (!is_null($this->ldapConnectionRes)) {
259
-			@$this->ldap->unbind($this->ldapConnectionRes);
260
-			$this->ldapConnectionRes = null;
261
-			$this->bindResult = [];
262
-		}
263
-	}
264
-
265
-	/**
266
-	 * @param string|null $key
267
-	 */
268
-	private function getCacheKey($key): string {
269
-		$prefix = 'LDAP-' . $this->configID . '-' . $this->configPrefix . '-';
270
-		if (is_null($key)) {
271
-			return $prefix;
272
-		}
273
-		return $prefix . hash('sha256', $key);
274
-	}
275
-
276
-	/**
277
-	 * @param string $key
278
-	 * @return mixed|null
279
-	 */
280
-	public function getFromCache($key) {
281
-		if (!$this->configured) {
282
-			$this->readConfiguration();
283
-		}
284
-		if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
285
-			return null;
286
-		}
287
-		$key = $this->getCacheKey($key);
288
-
289
-		return json_decode(base64_decode($this->cache->get($key) ?? ''), true);
290
-	}
291
-
292
-	public function getConfigPrefix(): string {
293
-		return $this->configPrefix;
294
-	}
295
-
296
-	/**
297
-	 * @param string $key
298
-	 * @param mixed $value
299
-	 */
300
-	public function writeToCache($key, $value, ?int $ttlOverride = null): void {
301
-		if (!$this->configured) {
302
-			$this->readConfiguration();
303
-		}
304
-		if (is_null($this->cache)
305
-			|| !$this->configuration->ldapCacheTTL
306
-			|| !$this->configuration->ldapConfigurationActive) {
307
-			return;
308
-		}
309
-		$key = $this->getCacheKey($key);
310
-		$value = base64_encode(json_encode($value));
311
-		$ttl = $ttlOverride ?? $this->configuration->ldapCacheTTL;
312
-		$this->cache->set($key, $value, $ttl);
313
-	}
314
-
315
-	public function clearCache() {
316
-		if (!is_null($this->cache)) {
317
-			$this->cache->clear($this->getCacheKey(null));
318
-		}
319
-	}
320
-
321
-	/**
322
-	 * Caches the general LDAP configuration.
323
-	 * @param bool $force optional. true, if the re-read should be forced. defaults
324
-	 *                    to false.
325
-	 */
326
-	private function readConfiguration(bool $force = false): void {
327
-		if ((!$this->configured || $force) && !is_null($this->configID)) {
328
-			$this->configuration->readConfiguration();
329
-			$this->configured = $this->validateConfiguration();
330
-		}
331
-	}
332
-
333
-	/**
334
-	 * set LDAP configuration with values delivered by an array, not read from configuration
335
-	 * @param array $config array that holds the config parameters in an associated array
336
-	 * @param array &$setParameters optional; array where the set fields will be given to
337
-	 * @param bool $throw if true, throw ConfigurationIssueException with details instead of returning false
338
-	 * @return bool true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
339
-	 */
340
-	public function setConfiguration(array $config, ?array &$setParameters = null, bool $throw = false): bool {
341
-		if (is_null($setParameters)) {
342
-			$setParameters = [];
343
-		}
344
-		$this->doNotValidate = false;
345
-		$this->configuration->setConfiguration($config, $setParameters);
346
-		if (count($setParameters) > 0) {
347
-			$this->configured = $this->validateConfiguration($throw);
348
-		}
349
-
350
-
351
-		return $this->configured;
352
-	}
353
-
354
-	/**
355
-	 * saves the current Configuration in the database and empties the
356
-	 * cache
357
-	 * @return null
358
-	 */
359
-	public function saveConfiguration() {
360
-		$this->configuration->saveConfiguration();
361
-		$this->clearCache();
362
-	}
363
-
364
-	/**
365
-	 * get the current LDAP configuration
366
-	 * @return array
367
-	 */
368
-	public function getConfiguration() {
369
-		$this->readConfiguration();
370
-		$config = $this->configuration->getConfiguration();
371
-		$cta = $this->configuration->getConfigTranslationArray();
372
-		$result = [];
373
-		foreach ($cta as $dbkey => $configkey) {
374
-			switch ($configkey) {
375
-				case 'homeFolderNamingRule':
376
-					if (str_starts_with($config[$configkey], 'attr:')) {
377
-						$result[$dbkey] = substr($config[$configkey], 5);
378
-					} else {
379
-						$result[$dbkey] = '';
380
-					}
381
-					break;
382
-				case 'ldapBase':
383
-				case 'ldapBaseUsers':
384
-				case 'ldapBaseGroups':
385
-				case 'ldapAttributesForUserSearch':
386
-				case 'ldapAttributesForGroupSearch':
387
-					if (is_array($config[$configkey])) {
388
-						$result[$dbkey] = implode("\n", $config[$configkey]);
389
-						break;
390
-					} //else follows default
391
-					// no break
392
-				default:
393
-					$result[$dbkey] = $config[$configkey];
394
-			}
395
-		}
396
-		return $result;
397
-	}
398
-
399
-	private function doSoftValidation(): void {
400
-		//if User or Group Base are not set, take over Base DN setting
401
-		foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
402
-			$val = $this->configuration->$keyBase;
403
-			if (empty($val)) {
404
-				$this->configuration->$keyBase = $this->configuration->ldapBase;
405
-			}
406
-		}
407
-
408
-		foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
409
-			'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute'] as $expertSetting => $effectiveSetting) {
410
-			$uuidOverride = $this->configuration->$expertSetting;
411
-			if (!empty($uuidOverride)) {
412
-				$this->configuration->$effectiveSetting = $uuidOverride;
413
-			} else {
414
-				$uuidAttributes = Access::UUID_ATTRIBUTES;
415
-				array_unshift($uuidAttributes, 'auto');
416
-				if (!in_array($this->configuration->$effectiveSetting, $uuidAttributes)
417
-					&& !is_null($this->configID)) {
418
-					$this->configuration->$effectiveSetting = 'auto';
419
-					$this->configuration->saveConfiguration();
420
-					$this->logger->info(
421
-						'Illegal value for the ' . $effectiveSetting . ', reset to autodetect.',
422
-						['app' => 'user_ldap']
423
-					);
424
-				}
425
-			}
426
-		}
427
-
428
-		$backupPort = (int)$this->configuration->ldapBackupPort;
429
-		if ($backupPort <= 0) {
430
-			$this->configuration->ldapBackupPort = $this->configuration->ldapPort;
431
-		}
432
-
433
-		//make sure empty search attributes are saved as simple, empty array
434
-		$saKeys = ['ldapAttributesForUserSearch',
435
-			'ldapAttributesForGroupSearch'];
436
-		foreach ($saKeys as $key) {
437
-			$val = $this->configuration->$key;
438
-			if (is_array($val) && count($val) === 1 && empty($val[0])) {
439
-				$this->configuration->$key = [];
440
-			}
441
-		}
442
-
443
-		if ((stripos((string)$this->configuration->ldapHost, 'ldaps://') === 0)
444
-			&& $this->configuration->ldapTLS) {
445
-			$this->configuration->ldapTLS = (string)false;
446
-			$this->logger->info(
447
-				'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
448
-				['app' => 'user_ldap']
449
-			);
450
-		}
451
-	}
452
-
453
-	/**
454
-	 * @throws ConfigurationIssueException
455
-	 */
456
-	private function doCriticalValidation(): void {
457
-		//options that shall not be empty
458
-		$options = ['ldapHost', 'ldapUserDisplayName',
459
-			'ldapGroupDisplayName', 'ldapLoginFilter'];
460
-
461
-		//ldapPort should not be empty either unless ldapHost is pointing to a socket
462
-		if (!$this->configuration->usesLdapi()) {
463
-			$options[] = 'ldapPort';
464
-		}
465
-
466
-		foreach ($options as $key) {
467
-			$val = $this->configuration->$key;
468
-			if (empty($val)) {
469
-				switch ($key) {
470
-					case 'ldapHost':
471
-						$subj = 'LDAP Host';
472
-						break;
473
-					case 'ldapPort':
474
-						$subj = 'LDAP Port';
475
-						break;
476
-					case 'ldapUserDisplayName':
477
-						$subj = 'LDAP User Display Name';
478
-						break;
479
-					case 'ldapGroupDisplayName':
480
-						$subj = 'LDAP Group Display Name';
481
-						break;
482
-					case 'ldapLoginFilter':
483
-						$subj = 'LDAP Login Filter';
484
-						break;
485
-					default:
486
-						$subj = $key;
487
-						break;
488
-				}
489
-				throw new ConfigurationIssueException(
490
-					'No ' . $subj . ' given!',
491
-					$this->l10n->t('Mandatory field "%s" left empty', $subj),
492
-				);
493
-			}
494
-		}
495
-
496
-		//combinations
497
-		$agent = $this->configuration->ldapAgentName;
498
-		$pwd = $this->configuration->ldapAgentPassword;
499
-		if ($agent === '' && $pwd !== '') {
500
-			throw new ConfigurationIssueException(
501
-				'A password is given, but not an LDAP agent',
502
-				$this->l10n->t('A password is given, but not an LDAP agent'),
503
-			);
504
-		}
505
-		if ($agent !== '' && $pwd === '') {
506
-			throw new ConfigurationIssueException(
507
-				'No password is given for the user agent',
508
-				$this->l10n->t('No password is given for the user agent'),
509
-			);
510
-		}
511
-
512
-		$base = $this->configuration->ldapBase;
513
-		$baseUsers = $this->configuration->ldapBaseUsers;
514
-		$baseGroups = $this->configuration->ldapBaseGroups;
515
-
516
-		if (empty($base)) {
517
-			throw new ConfigurationIssueException(
518
-				'Not a single Base DN given',
519
-				$this->l10n->t('No LDAP base DN was given'),
520
-			);
521
-		}
522
-
523
-		if (!empty($baseUsers) && !$this->checkBasesAreValid($baseUsers, $base)) {
524
-			throw new ConfigurationIssueException(
525
-				'User base is not in root base',
526
-				$this->l10n->t('User base DN is not a subnode of global base DN'),
527
-			);
528
-		}
529
-
530
-		if (!empty($baseGroups) && !$this->checkBasesAreValid($baseGroups, $base)) {
531
-			throw new ConfigurationIssueException(
532
-				'Group base is not in root base',
533
-				$this->l10n->t('Group base DN is not a subnode of global base DN'),
534
-			);
535
-		}
536
-
537
-		if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
538
-			throw new ConfigurationIssueException(
539
-				'Login filter does not contain %uid placeholder.',
540
-				$this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']),
541
-			);
542
-		}
543
-	}
544
-
545
-	/**
546
-	 * Checks that all bases are subnodes of one of the root bases
547
-	 */
548
-	private function checkBasesAreValid(array $bases, array $rootBases): bool {
549
-		foreach ($bases as $base) {
550
-			$ok = false;
551
-			foreach ($rootBases as $rootBase) {
552
-				if (str_ends_with($base, $rootBase)) {
553
-					$ok = true;
554
-					break;
555
-				}
556
-			}
557
-			if (!$ok) {
558
-				return false;
559
-			}
560
-		}
561
-		return true;
562
-	}
563
-
564
-	/**
565
-	 * Validates the user specified configuration
566
-	 * @return bool true if configuration seems OK, false otherwise
567
-	 */
568
-	private function validateConfiguration(bool $throw = false): bool {
569
-		if ($this->doNotValidate) {
570
-			//don't do a validation if it is a new configuration with pure
571
-			//default values. Will be allowed on changes via __set or
572
-			//setConfiguration
573
-			return false;
574
-		}
575
-
576
-		// first step: "soft" checks: settings that are not really
577
-		// necessary, but advisable. If left empty, give an info message
578
-		$this->doSoftValidation();
579
-
580
-		//second step: critical checks. If left empty or filled wrong, mark as
581
-		//not configured and give a warning.
582
-		try {
583
-			$this->doCriticalValidation();
584
-			return true;
585
-		} catch (ConfigurationIssueException $e) {
586
-			if ($throw) {
587
-				throw $e;
588
-			}
589
-			$this->logger->warning(
590
-				'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(),
591
-				['exception' => $e]
592
-			);
593
-			return false;
594
-		}
595
-	}
596
-
597
-
598
-	/**
599
-	 * Connects and Binds to LDAP
600
-	 *
601
-	 * @throws ServerNotAvailableException
602
-	 */
603
-	private function establishConnection(): ?bool {
604
-		if (!$this->configuration->ldapConfigurationActive) {
605
-			return null;
606
-		}
607
-		static $phpLDAPinstalled = true;
608
-		if (!$phpLDAPinstalled) {
609
-			return false;
610
-		}
611
-		if (!$this->ignoreValidation && !$this->configured) {
612
-			$this->logger->warning(
613
-				'Configuration is invalid, cannot connect',
614
-				['app' => 'user_ldap']
615
-			);
616
-			return false;
617
-		}
618
-		if (!$this->ldapConnectionRes) {
619
-			if (!$this->ldap->areLDAPFunctionsAvailable()) {
620
-				$phpLDAPinstalled = false;
621
-				$this->logger->error(
622
-					'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
623
-					['app' => 'user_ldap']
624
-				);
625
-
626
-				return false;
627
-			}
628
-
629
-			$hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== '');
630
-			$hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== '');
631
-			$useBackgroundHost = (\OC::$CLI && $hasBackgroundHost);
632
-			$overrideCacheKey = ($useBackgroundHost ? 'overrideBackgroundServer' : 'overrideMainServer');
633
-			$forceBackupHost = ($this->configuration->ldapOverrideMainServer || $this->getFromCache($overrideCacheKey));
634
-			$bindStatus = false;
635
-			if (!$forceBackupHost) {
636
-				try {
637
-					$host = $this->configuration->ldapHost ?? '';
638
-					$port = $this->configuration->ldapPort ?? '';
639
-					if ($useBackgroundHost) {
640
-						$host = $this->configuration->ldapBackgroundHost ?? '';
641
-						$port = $this->configuration->ldapBackgroundPort ?? '';
642
-					}
643
-					$this->doConnect($host, $port);
644
-					return $this->bind();
645
-				} catch (ServerNotAvailableException $e) {
646
-					if (!$hasBackupHost) {
647
-						throw $e;
648
-					}
649
-				}
650
-				$this->logger->warning(
651
-					'Main LDAP not reachable, connecting to backup: {msg}',
652
-					[
653
-						'app' => 'user_ldap',
654
-						'msg' => $e->getMessage(),
655
-						'exception' => $e,
656
-					]
657
-				);
658
-			}
659
-
660
-			// if LDAP server is not reachable, try the Backup (Replica!) Server
661
-			$this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? '');
662
-			$this->bindResult = [];
663
-			$bindStatus = $this->bind();
664
-			$error = $this->ldap->isResource($this->ldapConnectionRes) ?
665
-				$this->ldap->errno($this->ldapConnectionRes) : -1;
666
-			if ($bindStatus && $error === 0 && !$forceBackupHost) {
667
-				//when bind to backup server succeeded and failed to main server,
668
-				//skip contacting it for 15min
669
-				$this->writeToCache($overrideCacheKey, true, 60 * 15);
670
-			}
671
-
672
-			return $bindStatus;
673
-		}
674
-		return null;
675
-	}
676
-
677
-	/**
678
-	 * @param string $host
679
-	 * @param string $port
680
-	 * @throws \OC\ServerNotAvailableException
681
-	 */
682
-	private function doConnect($host, $port): bool {
683
-		if ($host === '') {
684
-			return false;
685
-		}
686
-
687
-		$this->ldapConnectionRes = $this->ldap->connect($host, $port) ?: null;
688
-
689
-		if ($this->ldapConnectionRes === null) {
690
-			throw new ServerNotAvailableException('Connection failed');
691
-		}
692
-
693
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
694
-			throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
695
-		}
696
-
697
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
698
-			throw new ServerNotAvailableException('Could not disable LDAP referrals.');
699
-		}
700
-
701
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, $this->configuration->ldapConnectionTimeout)) {
702
-			throw new ServerNotAvailableException('Could not set network timeout');
703
-		}
704
-
705
-		if ($this->configuration->ldapTLS) {
706
-			if ($this->configuration->turnOffCertCheck) {
707
-				if ($this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) {
708
-					$this->logger->debug(
709
-						'Turned off SSL certificate validation successfully.',
710
-						['app' => 'user_ldap']
711
-					);
712
-				} else {
713
-					$this->logger->warning(
714
-						'Could not turn off SSL certificate validation.',
715
-						['app' => 'user_ldap']
716
-					);
717
-				}
718
-			}
719
-
720
-			if (!$this->ldap->startTls($this->ldapConnectionRes)) {
721
-				throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
722
-			}
723
-		}
724
-
725
-		return true;
726
-	}
727
-
728
-	/**
729
-	 * Binds to LDAP
730
-	 */
731
-	public function bind() {
732
-		if (!$this->configuration->ldapConfigurationActive) {
733
-			return false;
734
-		}
735
-		$cr = $this->ldapConnectionRes;
736
-		if (!$this->ldap->isResource($cr)) {
737
-			$cr = $this->getConnectionResource();
738
-		}
739
-
740
-		if (
741
-			count($this->bindResult) !== 0
742
-			&& $this->bindResult['sum'] === md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword)
743
-		) {
744
-			// don't attempt to bind again with the same data as before
745
-			// bind might have been invoked via getConnectionResource(),
746
-			// but we need results specifically for e.g. user login
747
-			return $this->bindResult['result'];
748
-		}
749
-
750
-		$ldapLogin = @$this->ldap->bind($cr,
751
-			$this->configuration->ldapAgentName,
752
-			$this->configuration->ldapAgentPassword);
753
-
754
-		$this->bindResult = [
755
-			'sum' => md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword),
756
-			'result' => $ldapLogin,
757
-		];
758
-
759
-		if (!$ldapLogin) {
760
-			$errno = $this->ldap->errno($cr);
761
-
762
-			$this->logger->warning(
763
-				'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
764
-				['app' => 'user_ldap']
765
-			);
766
-
767
-			// Set to failure mode, if LDAP error code is not one of
768
-			// - LDAP_SUCCESS (0)
769
-			// - LDAP_INVALID_CREDENTIALS (49)
770
-			// - LDAP_INSUFFICIENT_ACCESS (50, spotted Apple Open Directory)
771
-			// - LDAP_UNWILLING_TO_PERFORM (53, spotted eDirectory)
772
-			if (!in_array($errno, [0, 49, 50, 53], true)) {
773
-				$this->ldapConnectionRes = null;
774
-			}
775
-
776
-			return false;
777
-		}
778
-		return true;
779
-	}
97
+    private ?\LDAP\Connection $ldapConnectionRes = null;
98
+    private bool $configured = false;
99
+
100
+    /**
101
+     * @var bool whether connection should be kept on __destruct
102
+     */
103
+    private bool $dontDestruct = false;
104
+
105
+    /**
106
+     * @var bool runtime flag that indicates whether supported primary groups are available
107
+     */
108
+    public $hasPrimaryGroups = true;
109
+
110
+    /**
111
+     * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
112
+     */
113
+    public $hasGidNumber = true;
114
+
115
+    /**
116
+     * @var ICache|null
117
+     */
118
+    protected $cache = null;
119
+
120
+    /** @var Configuration settings handler * */
121
+    protected $configuration;
122
+
123
+    /**
124
+     * @var bool
125
+     */
126
+    protected $doNotValidate = false;
127
+
128
+    /**
129
+     * @var bool
130
+     */
131
+    protected $ignoreValidation = false;
132
+
133
+    /**
134
+     * @var array{sum?: string, result?: bool}
135
+     */
136
+    protected $bindResult = [];
137
+
138
+    protected LoggerInterface $logger;
139
+    private IL10N $l10n;
140
+
141
+    /**
142
+     * Constructor
143
+     * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
144
+     * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
145
+     */
146
+    public function __construct(
147
+        ILDAPWrapper $ldap,
148
+        private string $configPrefix = '',
149
+        private ?string $configID = 'user_ldap',
150
+    ) {
151
+        parent::__construct($ldap);
152
+        $this->configuration = new Configuration($this->configPrefix, !is_null($this->configID));
153
+        $memcache = Server::get(ICacheFactory::class);
154
+        if ($memcache->isAvailable()) {
155
+            $this->cache = $memcache->createDistributed();
156
+        }
157
+        $helper = Server::get(Helper::class);
158
+        $this->doNotValidate = !in_array($this->configPrefix,
159
+            $helper->getServerConfigurationPrefixes());
160
+        $this->logger = Server::get(LoggerInterface::class);
161
+        $this->l10n = Util::getL10N('user_ldap');
162
+    }
163
+
164
+    public function __destruct() {
165
+        if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
166
+            @$this->ldap->unbind($this->ldapConnectionRes);
167
+            $this->bindResult = [];
168
+        }
169
+    }
170
+
171
+    /**
172
+     * defines behaviour when the instance is cloned
173
+     */
174
+    public function __clone() {
175
+        $this->configuration = new Configuration($this->configPrefix,
176
+            !is_null($this->configID));
177
+        if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
178
+            $this->bindResult = [];
179
+        }
180
+        $this->ldapConnectionRes = null;
181
+        $this->dontDestruct = true;
182
+    }
183
+
184
+    public function __get(string $name) {
185
+        if (!$this->configured) {
186
+            $this->readConfiguration();
187
+        }
188
+
189
+        return $this->configuration->$name;
190
+    }
191
+
192
+    /**
193
+     * @param string $name
194
+     * @param mixed $value
195
+     */
196
+    public function __set($name, $value) {
197
+        $this->doNotValidate = false;
198
+        $before = $this->configuration->$name;
199
+        $this->configuration->$name = $value;
200
+        $after = $this->configuration->$name;
201
+        if ($before !== $after) {
202
+            if ($this->configID !== '' && $this->configID !== null) {
203
+                $this->configuration->saveConfiguration();
204
+            }
205
+            $this->validateConfiguration();
206
+        }
207
+    }
208
+
209
+    /**
210
+     * @param string $rule
211
+     * @return array
212
+     * @throws \RuntimeException
213
+     */
214
+    public function resolveRule($rule) {
215
+        return $this->configuration->resolveRule($rule);
216
+    }
217
+
218
+    /**
219
+     * sets whether the result of the configuration validation shall
220
+     * be ignored when establishing the connection. Used by the Wizard
221
+     * in early configuration state.
222
+     * @param bool $state
223
+     */
224
+    public function setIgnoreValidation($state) {
225
+        $this->ignoreValidation = (bool)$state;
226
+    }
227
+
228
+    /**
229
+     * initializes the LDAP backend
230
+     * @param bool $force read the config settings no matter what
231
+     */
232
+    public function init($force = false) {
233
+        $this->readConfiguration($force);
234
+        $this->establishConnection();
235
+    }
236
+
237
+    /**
238
+     * @return \LDAP\Connection The LDAP resource
239
+     */
240
+    public function getConnectionResource(): \LDAP\Connection {
241
+        if (!$this->ldapConnectionRes) {
242
+            $this->init();
243
+        }
244
+        if (is_null($this->ldapConnectionRes)) {
245
+            $this->logger->error(
246
+                'No LDAP Connection to server ' . $this->configuration->ldapHost,
247
+                ['app' => 'user_ldap']
248
+            );
249
+            throw new ServerNotAvailableException('Connection to LDAP server could not be established');
250
+        }
251
+        return $this->ldapConnectionRes;
252
+    }
253
+
254
+    /**
255
+     * resets the connection resource
256
+     */
257
+    public function resetConnectionResource(): void {
258
+        if (!is_null($this->ldapConnectionRes)) {
259
+            @$this->ldap->unbind($this->ldapConnectionRes);
260
+            $this->ldapConnectionRes = null;
261
+            $this->bindResult = [];
262
+        }
263
+    }
264
+
265
+    /**
266
+     * @param string|null $key
267
+     */
268
+    private function getCacheKey($key): string {
269
+        $prefix = 'LDAP-' . $this->configID . '-' . $this->configPrefix . '-';
270
+        if (is_null($key)) {
271
+            return $prefix;
272
+        }
273
+        return $prefix . hash('sha256', $key);
274
+    }
275
+
276
+    /**
277
+     * @param string $key
278
+     * @return mixed|null
279
+     */
280
+    public function getFromCache($key) {
281
+        if (!$this->configured) {
282
+            $this->readConfiguration();
283
+        }
284
+        if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
285
+            return null;
286
+        }
287
+        $key = $this->getCacheKey($key);
288
+
289
+        return json_decode(base64_decode($this->cache->get($key) ?? ''), true);
290
+    }
291
+
292
+    public function getConfigPrefix(): string {
293
+        return $this->configPrefix;
294
+    }
295
+
296
+    /**
297
+     * @param string $key
298
+     * @param mixed $value
299
+     */
300
+    public function writeToCache($key, $value, ?int $ttlOverride = null): void {
301
+        if (!$this->configured) {
302
+            $this->readConfiguration();
303
+        }
304
+        if (is_null($this->cache)
305
+            || !$this->configuration->ldapCacheTTL
306
+            || !$this->configuration->ldapConfigurationActive) {
307
+            return;
308
+        }
309
+        $key = $this->getCacheKey($key);
310
+        $value = base64_encode(json_encode($value));
311
+        $ttl = $ttlOverride ?? $this->configuration->ldapCacheTTL;
312
+        $this->cache->set($key, $value, $ttl);
313
+    }
314
+
315
+    public function clearCache() {
316
+        if (!is_null($this->cache)) {
317
+            $this->cache->clear($this->getCacheKey(null));
318
+        }
319
+    }
320
+
321
+    /**
322
+     * Caches the general LDAP configuration.
323
+     * @param bool $force optional. true, if the re-read should be forced. defaults
324
+     *                    to false.
325
+     */
326
+    private function readConfiguration(bool $force = false): void {
327
+        if ((!$this->configured || $force) && !is_null($this->configID)) {
328
+            $this->configuration->readConfiguration();
329
+            $this->configured = $this->validateConfiguration();
330
+        }
331
+    }
332
+
333
+    /**
334
+     * set LDAP configuration with values delivered by an array, not read from configuration
335
+     * @param array $config array that holds the config parameters in an associated array
336
+     * @param array &$setParameters optional; array where the set fields will be given to
337
+     * @param bool $throw if true, throw ConfigurationIssueException with details instead of returning false
338
+     * @return bool true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
339
+     */
340
+    public function setConfiguration(array $config, ?array &$setParameters = null, bool $throw = false): bool {
341
+        if (is_null($setParameters)) {
342
+            $setParameters = [];
343
+        }
344
+        $this->doNotValidate = false;
345
+        $this->configuration->setConfiguration($config, $setParameters);
346
+        if (count($setParameters) > 0) {
347
+            $this->configured = $this->validateConfiguration($throw);
348
+        }
349
+
350
+
351
+        return $this->configured;
352
+    }
353
+
354
+    /**
355
+     * saves the current Configuration in the database and empties the
356
+     * cache
357
+     * @return null
358
+     */
359
+    public function saveConfiguration() {
360
+        $this->configuration->saveConfiguration();
361
+        $this->clearCache();
362
+    }
363
+
364
+    /**
365
+     * get the current LDAP configuration
366
+     * @return array
367
+     */
368
+    public function getConfiguration() {
369
+        $this->readConfiguration();
370
+        $config = $this->configuration->getConfiguration();
371
+        $cta = $this->configuration->getConfigTranslationArray();
372
+        $result = [];
373
+        foreach ($cta as $dbkey => $configkey) {
374
+            switch ($configkey) {
375
+                case 'homeFolderNamingRule':
376
+                    if (str_starts_with($config[$configkey], 'attr:')) {
377
+                        $result[$dbkey] = substr($config[$configkey], 5);
378
+                    } else {
379
+                        $result[$dbkey] = '';
380
+                    }
381
+                    break;
382
+                case 'ldapBase':
383
+                case 'ldapBaseUsers':
384
+                case 'ldapBaseGroups':
385
+                case 'ldapAttributesForUserSearch':
386
+                case 'ldapAttributesForGroupSearch':
387
+                    if (is_array($config[$configkey])) {
388
+                        $result[$dbkey] = implode("\n", $config[$configkey]);
389
+                        break;
390
+                    } //else follows default
391
+                    // no break
392
+                default:
393
+                    $result[$dbkey] = $config[$configkey];
394
+            }
395
+        }
396
+        return $result;
397
+    }
398
+
399
+    private function doSoftValidation(): void {
400
+        //if User or Group Base are not set, take over Base DN setting
401
+        foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
402
+            $val = $this->configuration->$keyBase;
403
+            if (empty($val)) {
404
+                $this->configuration->$keyBase = $this->configuration->ldapBase;
405
+            }
406
+        }
407
+
408
+        foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
409
+            'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute'] as $expertSetting => $effectiveSetting) {
410
+            $uuidOverride = $this->configuration->$expertSetting;
411
+            if (!empty($uuidOverride)) {
412
+                $this->configuration->$effectiveSetting = $uuidOverride;
413
+            } else {
414
+                $uuidAttributes = Access::UUID_ATTRIBUTES;
415
+                array_unshift($uuidAttributes, 'auto');
416
+                if (!in_array($this->configuration->$effectiveSetting, $uuidAttributes)
417
+                    && !is_null($this->configID)) {
418
+                    $this->configuration->$effectiveSetting = 'auto';
419
+                    $this->configuration->saveConfiguration();
420
+                    $this->logger->info(
421
+                        'Illegal value for the ' . $effectiveSetting . ', reset to autodetect.',
422
+                        ['app' => 'user_ldap']
423
+                    );
424
+                }
425
+            }
426
+        }
427
+
428
+        $backupPort = (int)$this->configuration->ldapBackupPort;
429
+        if ($backupPort <= 0) {
430
+            $this->configuration->ldapBackupPort = $this->configuration->ldapPort;
431
+        }
432
+
433
+        //make sure empty search attributes are saved as simple, empty array
434
+        $saKeys = ['ldapAttributesForUserSearch',
435
+            'ldapAttributesForGroupSearch'];
436
+        foreach ($saKeys as $key) {
437
+            $val = $this->configuration->$key;
438
+            if (is_array($val) && count($val) === 1 && empty($val[0])) {
439
+                $this->configuration->$key = [];
440
+            }
441
+        }
442
+
443
+        if ((stripos((string)$this->configuration->ldapHost, 'ldaps://') === 0)
444
+            && $this->configuration->ldapTLS) {
445
+            $this->configuration->ldapTLS = (string)false;
446
+            $this->logger->info(
447
+                'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
448
+                ['app' => 'user_ldap']
449
+            );
450
+        }
451
+    }
452
+
453
+    /**
454
+     * @throws ConfigurationIssueException
455
+     */
456
+    private function doCriticalValidation(): void {
457
+        //options that shall not be empty
458
+        $options = ['ldapHost', 'ldapUserDisplayName',
459
+            'ldapGroupDisplayName', 'ldapLoginFilter'];
460
+
461
+        //ldapPort should not be empty either unless ldapHost is pointing to a socket
462
+        if (!$this->configuration->usesLdapi()) {
463
+            $options[] = 'ldapPort';
464
+        }
465
+
466
+        foreach ($options as $key) {
467
+            $val = $this->configuration->$key;
468
+            if (empty($val)) {
469
+                switch ($key) {
470
+                    case 'ldapHost':
471
+                        $subj = 'LDAP Host';
472
+                        break;
473
+                    case 'ldapPort':
474
+                        $subj = 'LDAP Port';
475
+                        break;
476
+                    case 'ldapUserDisplayName':
477
+                        $subj = 'LDAP User Display Name';
478
+                        break;
479
+                    case 'ldapGroupDisplayName':
480
+                        $subj = 'LDAP Group Display Name';
481
+                        break;
482
+                    case 'ldapLoginFilter':
483
+                        $subj = 'LDAP Login Filter';
484
+                        break;
485
+                    default:
486
+                        $subj = $key;
487
+                        break;
488
+                }
489
+                throw new ConfigurationIssueException(
490
+                    'No ' . $subj . ' given!',
491
+                    $this->l10n->t('Mandatory field "%s" left empty', $subj),
492
+                );
493
+            }
494
+        }
495
+
496
+        //combinations
497
+        $agent = $this->configuration->ldapAgentName;
498
+        $pwd = $this->configuration->ldapAgentPassword;
499
+        if ($agent === '' && $pwd !== '') {
500
+            throw new ConfigurationIssueException(
501
+                'A password is given, but not an LDAP agent',
502
+                $this->l10n->t('A password is given, but not an LDAP agent'),
503
+            );
504
+        }
505
+        if ($agent !== '' && $pwd === '') {
506
+            throw new ConfigurationIssueException(
507
+                'No password is given for the user agent',
508
+                $this->l10n->t('No password is given for the user agent'),
509
+            );
510
+        }
511
+
512
+        $base = $this->configuration->ldapBase;
513
+        $baseUsers = $this->configuration->ldapBaseUsers;
514
+        $baseGroups = $this->configuration->ldapBaseGroups;
515
+
516
+        if (empty($base)) {
517
+            throw new ConfigurationIssueException(
518
+                'Not a single Base DN given',
519
+                $this->l10n->t('No LDAP base DN was given'),
520
+            );
521
+        }
522
+
523
+        if (!empty($baseUsers) && !$this->checkBasesAreValid($baseUsers, $base)) {
524
+            throw new ConfigurationIssueException(
525
+                'User base is not in root base',
526
+                $this->l10n->t('User base DN is not a subnode of global base DN'),
527
+            );
528
+        }
529
+
530
+        if (!empty($baseGroups) && !$this->checkBasesAreValid($baseGroups, $base)) {
531
+            throw new ConfigurationIssueException(
532
+                'Group base is not in root base',
533
+                $this->l10n->t('Group base DN is not a subnode of global base DN'),
534
+            );
535
+        }
536
+
537
+        if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
538
+            throw new ConfigurationIssueException(
539
+                'Login filter does not contain %uid placeholder.',
540
+                $this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']),
541
+            );
542
+        }
543
+    }
544
+
545
+    /**
546
+     * Checks that all bases are subnodes of one of the root bases
547
+     */
548
+    private function checkBasesAreValid(array $bases, array $rootBases): bool {
549
+        foreach ($bases as $base) {
550
+            $ok = false;
551
+            foreach ($rootBases as $rootBase) {
552
+                if (str_ends_with($base, $rootBase)) {
553
+                    $ok = true;
554
+                    break;
555
+                }
556
+            }
557
+            if (!$ok) {
558
+                return false;
559
+            }
560
+        }
561
+        return true;
562
+    }
563
+
564
+    /**
565
+     * Validates the user specified configuration
566
+     * @return bool true if configuration seems OK, false otherwise
567
+     */
568
+    private function validateConfiguration(bool $throw = false): bool {
569
+        if ($this->doNotValidate) {
570
+            //don't do a validation if it is a new configuration with pure
571
+            //default values. Will be allowed on changes via __set or
572
+            //setConfiguration
573
+            return false;
574
+        }
575
+
576
+        // first step: "soft" checks: settings that are not really
577
+        // necessary, but advisable. If left empty, give an info message
578
+        $this->doSoftValidation();
579
+
580
+        //second step: critical checks. If left empty or filled wrong, mark as
581
+        //not configured and give a warning.
582
+        try {
583
+            $this->doCriticalValidation();
584
+            return true;
585
+        } catch (ConfigurationIssueException $e) {
586
+            if ($throw) {
587
+                throw $e;
588
+            }
589
+            $this->logger->warning(
590
+                'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(),
591
+                ['exception' => $e]
592
+            );
593
+            return false;
594
+        }
595
+    }
596
+
597
+
598
+    /**
599
+     * Connects and Binds to LDAP
600
+     *
601
+     * @throws ServerNotAvailableException
602
+     */
603
+    private function establishConnection(): ?bool {
604
+        if (!$this->configuration->ldapConfigurationActive) {
605
+            return null;
606
+        }
607
+        static $phpLDAPinstalled = true;
608
+        if (!$phpLDAPinstalled) {
609
+            return false;
610
+        }
611
+        if (!$this->ignoreValidation && !$this->configured) {
612
+            $this->logger->warning(
613
+                'Configuration is invalid, cannot connect',
614
+                ['app' => 'user_ldap']
615
+            );
616
+            return false;
617
+        }
618
+        if (!$this->ldapConnectionRes) {
619
+            if (!$this->ldap->areLDAPFunctionsAvailable()) {
620
+                $phpLDAPinstalled = false;
621
+                $this->logger->error(
622
+                    'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
623
+                    ['app' => 'user_ldap']
624
+                );
625
+
626
+                return false;
627
+            }
628
+
629
+            $hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== '');
630
+            $hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== '');
631
+            $useBackgroundHost = (\OC::$CLI && $hasBackgroundHost);
632
+            $overrideCacheKey = ($useBackgroundHost ? 'overrideBackgroundServer' : 'overrideMainServer');
633
+            $forceBackupHost = ($this->configuration->ldapOverrideMainServer || $this->getFromCache($overrideCacheKey));
634
+            $bindStatus = false;
635
+            if (!$forceBackupHost) {
636
+                try {
637
+                    $host = $this->configuration->ldapHost ?? '';
638
+                    $port = $this->configuration->ldapPort ?? '';
639
+                    if ($useBackgroundHost) {
640
+                        $host = $this->configuration->ldapBackgroundHost ?? '';
641
+                        $port = $this->configuration->ldapBackgroundPort ?? '';
642
+                    }
643
+                    $this->doConnect($host, $port);
644
+                    return $this->bind();
645
+                } catch (ServerNotAvailableException $e) {
646
+                    if (!$hasBackupHost) {
647
+                        throw $e;
648
+                    }
649
+                }
650
+                $this->logger->warning(
651
+                    'Main LDAP not reachable, connecting to backup: {msg}',
652
+                    [
653
+                        'app' => 'user_ldap',
654
+                        'msg' => $e->getMessage(),
655
+                        'exception' => $e,
656
+                    ]
657
+                );
658
+            }
659
+
660
+            // if LDAP server is not reachable, try the Backup (Replica!) Server
661
+            $this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? '');
662
+            $this->bindResult = [];
663
+            $bindStatus = $this->bind();
664
+            $error = $this->ldap->isResource($this->ldapConnectionRes) ?
665
+                $this->ldap->errno($this->ldapConnectionRes) : -1;
666
+            if ($bindStatus && $error === 0 && !$forceBackupHost) {
667
+                //when bind to backup server succeeded and failed to main server,
668
+                //skip contacting it for 15min
669
+                $this->writeToCache($overrideCacheKey, true, 60 * 15);
670
+            }
671
+
672
+            return $bindStatus;
673
+        }
674
+        return null;
675
+    }
676
+
677
+    /**
678
+     * @param string $host
679
+     * @param string $port
680
+     * @throws \OC\ServerNotAvailableException
681
+     */
682
+    private function doConnect($host, $port): bool {
683
+        if ($host === '') {
684
+            return false;
685
+        }
686
+
687
+        $this->ldapConnectionRes = $this->ldap->connect($host, $port) ?: null;
688
+
689
+        if ($this->ldapConnectionRes === null) {
690
+            throw new ServerNotAvailableException('Connection failed');
691
+        }
692
+
693
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
694
+            throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
695
+        }
696
+
697
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
698
+            throw new ServerNotAvailableException('Could not disable LDAP referrals.');
699
+        }
700
+
701
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, $this->configuration->ldapConnectionTimeout)) {
702
+            throw new ServerNotAvailableException('Could not set network timeout');
703
+        }
704
+
705
+        if ($this->configuration->ldapTLS) {
706
+            if ($this->configuration->turnOffCertCheck) {
707
+                if ($this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) {
708
+                    $this->logger->debug(
709
+                        'Turned off SSL certificate validation successfully.',
710
+                        ['app' => 'user_ldap']
711
+                    );
712
+                } else {
713
+                    $this->logger->warning(
714
+                        'Could not turn off SSL certificate validation.',
715
+                        ['app' => 'user_ldap']
716
+                    );
717
+                }
718
+            }
719
+
720
+            if (!$this->ldap->startTls($this->ldapConnectionRes)) {
721
+                throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
722
+            }
723
+        }
724
+
725
+        return true;
726
+    }
727
+
728
+    /**
729
+     * Binds to LDAP
730
+     */
731
+    public function bind() {
732
+        if (!$this->configuration->ldapConfigurationActive) {
733
+            return false;
734
+        }
735
+        $cr = $this->ldapConnectionRes;
736
+        if (!$this->ldap->isResource($cr)) {
737
+            $cr = $this->getConnectionResource();
738
+        }
739
+
740
+        if (
741
+            count($this->bindResult) !== 0
742
+            && $this->bindResult['sum'] === md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword)
743
+        ) {
744
+            // don't attempt to bind again with the same data as before
745
+            // bind might have been invoked via getConnectionResource(),
746
+            // but we need results specifically for e.g. user login
747
+            return $this->bindResult['result'];
748
+        }
749
+
750
+        $ldapLogin = @$this->ldap->bind($cr,
751
+            $this->configuration->ldapAgentName,
752
+            $this->configuration->ldapAgentPassword);
753
+
754
+        $this->bindResult = [
755
+            'sum' => md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword),
756
+            'result' => $ldapLogin,
757
+        ];
758
+
759
+        if (!$ldapLogin) {
760
+            $errno = $this->ldap->errno($cr);
761
+
762
+            $this->logger->warning(
763
+                'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
764
+                ['app' => 'user_ldap']
765
+            );
766
+
767
+            // Set to failure mode, if LDAP error code is not one of
768
+            // - LDAP_SUCCESS (0)
769
+            // - LDAP_INVALID_CREDENTIALS (49)
770
+            // - LDAP_INSUFFICIENT_ACCESS (50, spotted Apple Open Directory)
771
+            // - LDAP_UNWILLING_TO_PERFORM (53, spotted eDirectory)
772
+            if (!in_array($errno, [0, 49, 50, 53], true)) {
773
+                $this->ldapConnectionRes = null;
774
+            }
775
+
776
+            return false;
777
+        }
778
+        return true;
779
+    }
780 780
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Jobs/CleanUp.php 1 patch
Indentation   +168 added lines, -168 removed lines patch added patch discarded remove patch
@@ -25,172 +25,172 @@
 block discarded – undo
25 25
  * @package OCA\User_LDAP\Jobs;
26 26
  */
27 27
 class CleanUp extends TimedJob {
28
-	/** @var ?int $limit amount of users that should be checked per run */
29
-	protected $limit;
30
-
31
-	/** @var int $defaultIntervalMin default interval in minutes */
32
-	protected $defaultIntervalMin = 60;
33
-
34
-	/** @var IConfig $ocConfig */
35
-	protected $ocConfig;
36
-
37
-	/** @var IDBConnection $db */
38
-	protected $db;
39
-
40
-	/** @var Helper $ldapHelper */
41
-	protected $ldapHelper;
42
-
43
-	/** @var UserMapping */
44
-	protected $mapping;
45
-
46
-	public function __construct(
47
-		ITimeFactory $timeFactory,
48
-		protected User_Proxy $userBackend,
49
-		protected DeletedUsersIndex $dui,
50
-	) {
51
-		parent::__construct($timeFactory);
52
-		$minutes = Server::get(IConfig::class)->getSystemValue(
53
-			'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
54
-		$this->setInterval((int)$minutes * 60);
55
-	}
56
-
57
-	/**
58
-	 * assigns the instances passed to run() to the class properties
59
-	 * @param array $arguments
60
-	 */
61
-	public function setArguments($arguments): void {
62
-		//Dependency Injection is not possible, because the constructor will
63
-		//only get values that are serialized to JSON. I.e. whatever we would
64
-		//pass in app.php we do add here, except something else is passed e.g.
65
-		//in tests.
66
-
67
-		if (isset($arguments['helper'])) {
68
-			$this->ldapHelper = $arguments['helper'];
69
-		} else {
70
-			$this->ldapHelper = Server::get(Helper::class);
71
-		}
72
-
73
-		if (isset($arguments['ocConfig'])) {
74
-			$this->ocConfig = $arguments['ocConfig'];
75
-		} else {
76
-			$this->ocConfig = Server::get(IConfig::class);
77
-		}
78
-
79
-		if (isset($arguments['userBackend'])) {
80
-			$this->userBackend = $arguments['userBackend'];
81
-		}
82
-
83
-		if (isset($arguments['db'])) {
84
-			$this->db = $arguments['db'];
85
-		} else {
86
-			$this->db = Server::get(IDBConnection::class);
87
-		}
88
-
89
-		if (isset($arguments['mapping'])) {
90
-			$this->mapping = $arguments['mapping'];
91
-		} else {
92
-			$this->mapping = Server::get(UserMapping::class);
93
-		}
94
-
95
-		if (isset($arguments['deletedUsersIndex'])) {
96
-			$this->dui = $arguments['deletedUsersIndex'];
97
-		}
98
-	}
99
-
100
-	/**
101
-	 * makes the background job do its work
102
-	 * @param array $argument
103
-	 */
104
-	public function run($argument): void {
105
-		$this->setArguments($argument);
106
-
107
-		if (!$this->isCleanUpAllowed()) {
108
-			return;
109
-		}
110
-		$users = $this->mapping->getList($this->getOffset(), $this->getChunkSize());
111
-		$resetOffset = $this->isOffsetResetNecessary(count($users));
112
-		$this->checkUsers($users);
113
-		$this->setOffset($resetOffset);
114
-	}
115
-
116
-	/**
117
-	 * checks whether next run should start at 0 again
118
-	 */
119
-	public function isOffsetResetNecessary(int $resultCount): bool {
120
-		return $resultCount < $this->getChunkSize();
121
-	}
122
-
123
-	/**
124
-	 * checks whether cleaning up LDAP users is allowed
125
-	 */
126
-	public function isCleanUpAllowed(): bool {
127
-		try {
128
-			if ($this->ldapHelper->haveDisabledConfigurations()) {
129
-				return false;
130
-			}
131
-		} catch (\Exception $e) {
132
-			return false;
133
-		}
134
-
135
-		return $this->isCleanUpEnabled();
136
-	}
137
-
138
-	/**
139
-	 * checks whether clean up is enabled by configuration
140
-	 */
141
-	private function isCleanUpEnabled(): bool {
142
-		return (bool)$this->ocConfig->getSystemValue(
143
-			'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
144
-	}
145
-
146
-	/**
147
-	 * checks users whether they are still existing
148
-	 * @param array $users result from getMappedUsers()
149
-	 */
150
-	private function checkUsers(array $users): void {
151
-		foreach ($users as $user) {
152
-			$this->checkUser($user);
153
-		}
154
-	}
155
-
156
-	/**
157
-	 * checks whether a user is still existing in LDAP
158
-	 * @param string[] $user
159
-	 */
160
-	private function checkUser(array $user): void {
161
-		if ($this->userBackend->userExistsOnLDAP($user['name'])) {
162
-			//still available, all good
163
-
164
-			return;
165
-		}
166
-
167
-		$this->dui->markUser($user['name']);
168
-	}
169
-
170
-	/**
171
-	 * gets the offset to fetch users from the mappings table
172
-	 */
173
-	private function getOffset(): int {
174
-		return (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobOffset', '0');
175
-	}
176
-
177
-	/**
178
-	 * sets the new offset for the next run
179
-	 * @param bool $reset whether the offset should be set to 0
180
-	 */
181
-	public function setOffset(bool $reset = false): void {
182
-		$newOffset = $reset ? 0 :
183
-			$this->getOffset() + $this->getChunkSize();
184
-		$this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', (string)$newOffset);
185
-	}
186
-
187
-	/**
188
-	 * returns the chunk size (limit in DB speak)
189
-	 */
190
-	public function getChunkSize(): int {
191
-		if ($this->limit === null) {
192
-			$this->limit = (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobChunkSize', '50');
193
-		}
194
-		return $this->limit;
195
-	}
28
+    /** @var ?int $limit amount of users that should be checked per run */
29
+    protected $limit;
30
+
31
+    /** @var int $defaultIntervalMin default interval in minutes */
32
+    protected $defaultIntervalMin = 60;
33
+
34
+    /** @var IConfig $ocConfig */
35
+    protected $ocConfig;
36
+
37
+    /** @var IDBConnection $db */
38
+    protected $db;
39
+
40
+    /** @var Helper $ldapHelper */
41
+    protected $ldapHelper;
42
+
43
+    /** @var UserMapping */
44
+    protected $mapping;
45
+
46
+    public function __construct(
47
+        ITimeFactory $timeFactory,
48
+        protected User_Proxy $userBackend,
49
+        protected DeletedUsersIndex $dui,
50
+    ) {
51
+        parent::__construct($timeFactory);
52
+        $minutes = Server::get(IConfig::class)->getSystemValue(
53
+            'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
54
+        $this->setInterval((int)$minutes * 60);
55
+    }
56
+
57
+    /**
58
+     * assigns the instances passed to run() to the class properties
59
+     * @param array $arguments
60
+     */
61
+    public function setArguments($arguments): void {
62
+        //Dependency Injection is not possible, because the constructor will
63
+        //only get values that are serialized to JSON. I.e. whatever we would
64
+        //pass in app.php we do add here, except something else is passed e.g.
65
+        //in tests.
66
+
67
+        if (isset($arguments['helper'])) {
68
+            $this->ldapHelper = $arguments['helper'];
69
+        } else {
70
+            $this->ldapHelper = Server::get(Helper::class);
71
+        }
72
+
73
+        if (isset($arguments['ocConfig'])) {
74
+            $this->ocConfig = $arguments['ocConfig'];
75
+        } else {
76
+            $this->ocConfig = Server::get(IConfig::class);
77
+        }
78
+
79
+        if (isset($arguments['userBackend'])) {
80
+            $this->userBackend = $arguments['userBackend'];
81
+        }
82
+
83
+        if (isset($arguments['db'])) {
84
+            $this->db = $arguments['db'];
85
+        } else {
86
+            $this->db = Server::get(IDBConnection::class);
87
+        }
88
+
89
+        if (isset($arguments['mapping'])) {
90
+            $this->mapping = $arguments['mapping'];
91
+        } else {
92
+            $this->mapping = Server::get(UserMapping::class);
93
+        }
94
+
95
+        if (isset($arguments['deletedUsersIndex'])) {
96
+            $this->dui = $arguments['deletedUsersIndex'];
97
+        }
98
+    }
99
+
100
+    /**
101
+     * makes the background job do its work
102
+     * @param array $argument
103
+     */
104
+    public function run($argument): void {
105
+        $this->setArguments($argument);
106
+
107
+        if (!$this->isCleanUpAllowed()) {
108
+            return;
109
+        }
110
+        $users = $this->mapping->getList($this->getOffset(), $this->getChunkSize());
111
+        $resetOffset = $this->isOffsetResetNecessary(count($users));
112
+        $this->checkUsers($users);
113
+        $this->setOffset($resetOffset);
114
+    }
115
+
116
+    /**
117
+     * checks whether next run should start at 0 again
118
+     */
119
+    public function isOffsetResetNecessary(int $resultCount): bool {
120
+        return $resultCount < $this->getChunkSize();
121
+    }
122
+
123
+    /**
124
+     * checks whether cleaning up LDAP users is allowed
125
+     */
126
+    public function isCleanUpAllowed(): bool {
127
+        try {
128
+            if ($this->ldapHelper->haveDisabledConfigurations()) {
129
+                return false;
130
+            }
131
+        } catch (\Exception $e) {
132
+            return false;
133
+        }
134
+
135
+        return $this->isCleanUpEnabled();
136
+    }
137
+
138
+    /**
139
+     * checks whether clean up is enabled by configuration
140
+     */
141
+    private function isCleanUpEnabled(): bool {
142
+        return (bool)$this->ocConfig->getSystemValue(
143
+            'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
144
+    }
145
+
146
+    /**
147
+     * checks users whether they are still existing
148
+     * @param array $users result from getMappedUsers()
149
+     */
150
+    private function checkUsers(array $users): void {
151
+        foreach ($users as $user) {
152
+            $this->checkUser($user);
153
+        }
154
+    }
155
+
156
+    /**
157
+     * checks whether a user is still existing in LDAP
158
+     * @param string[] $user
159
+     */
160
+    private function checkUser(array $user): void {
161
+        if ($this->userBackend->userExistsOnLDAP($user['name'])) {
162
+            //still available, all good
163
+
164
+            return;
165
+        }
166
+
167
+        $this->dui->markUser($user['name']);
168
+    }
169
+
170
+    /**
171
+     * gets the offset to fetch users from the mappings table
172
+     */
173
+    private function getOffset(): int {
174
+        return (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobOffset', '0');
175
+    }
176
+
177
+    /**
178
+     * sets the new offset for the next run
179
+     * @param bool $reset whether the offset should be set to 0
180
+     */
181
+    public function setOffset(bool $reset = false): void {
182
+        $newOffset = $reset ? 0 :
183
+            $this->getOffset() + $this->getChunkSize();
184
+        $this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', (string)$newOffset);
185
+    }
186
+
187
+    /**
188
+     * returns the chunk size (limit in DB speak)
189
+     */
190
+    public function getChunkSize(): int {
191
+        if ($this->limit === null) {
192
+            $this->limit = (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobChunkSize', '50');
193
+        }
194
+        return $this->limit;
195
+    }
196 196
 }
Please login to merge, or discard this patch.
apps/user_ldap/ajax/deleteConfiguration.php 2 patches
Indentation   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -17,8 +17,8 @@
 block discarded – undo
17 17
 $prefix = (string)$_POST['ldap_serverconfig_chooser'];
18 18
 $helper = Server::get(Helper::class);
19 19
 if ($helper->deleteServerConfiguration($prefix)) {
20
-	\OC_JSON::success();
20
+    \OC_JSON::success();
21 21
 } else {
22
-	$l = Util::getL10N('user_ldap');
23
-	\OC_JSON::error(['message' => $l->t('Failed to delete the server configuration')]);
22
+    $l = Util::getL10N('user_ldap');
23
+    \OC_JSON::error(['message' => $l->t('Failed to delete the server configuration')]);
24 24
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -14,7 +14,7 @@
 block discarded – undo
14 14
 \OC_JSON::checkAppEnabled('user_ldap');
15 15
 \OC_JSON::callCheck();
16 16
 
17
-$prefix = (string)$_POST['ldap_serverconfig_chooser'];
17
+$prefix = (string) $_POST['ldap_serverconfig_chooser'];
18 18
 $helper = Server::get(Helper::class);
19 19
 if ($helper->deleteServerConfiguration($prefix)) {
20 20
 	\OC_JSON::success();
Please login to merge, or discard this patch.
apps/user_ldap/tests/Integration/AbstractIntegrationTest.php 1 patch
Indentation   +142 added lines, -142 removed lines patch added patch discarded remove patch
@@ -23,146 +23,146 @@
 block discarded – undo
23 23
 use Psr\Log\LoggerInterface;
24 24
 
25 25
 abstract class AbstractIntegrationTest {
26
-	/** @var LDAP */
27
-	protected $ldap;
28
-
29
-	/** @var Connection */
30
-	protected $connection;
31
-
32
-	/** @var Access */
33
-	protected $access;
34
-
35
-	/** @var Manager */
36
-	protected $userManager;
37
-
38
-	/** @var Helper */
39
-	protected $helper;
40
-
41
-	/** @var string[] */
42
-	protected $server;
43
-
44
-	/**
45
-	 * @param string $base
46
-	 */
47
-	public function __construct(
48
-		$host,
49
-		$port,
50
-		$bind,
51
-		$pwd,
52
-		protected $base,
53
-	) {
54
-		$this->server = [
55
-			'host' => $host,
56
-			'port' => $port,
57
-			'dn' => $bind,
58
-			'pwd' => $pwd
59
-		];
60
-	}
61
-
62
-	/**
63
-	 * prepares the LDAP environment and sets up a test configuration for
64
-	 * the LDAP backend.
65
-	 */
66
-	public function init() {
67
-		\OC::$server->registerService(UserPluginManager::class, function () {
68
-			return new UserPluginManager();
69
-		});
70
-		\OC::$server->registerService(GroupPluginManager::class, function () {
71
-			return new GroupPluginManager();
72
-		});
73
-
74
-		$this->initLDAPWrapper();
75
-		$this->initConnection();
76
-		$this->initUserManager();
77
-		$this->initHelper();
78
-		$this->initAccess();
79
-	}
80
-
81
-	/**
82
-	 * initializes the test LDAP wrapper
83
-	 */
84
-	protected function initLDAPWrapper() {
85
-		$this->ldap = new LDAP();
86
-	}
87
-
88
-	/**
89
-	 * sets up the LDAP configuration to be used for the test
90
-	 */
91
-	protected function initConnection() {
92
-		$this->connection = new Connection($this->ldap, '', null);
93
-		$this->connection->setConfiguration([
94
-			'ldapHost' => $this->server['host'],
95
-			'ldapPort' => $this->server['port'],
96
-			'ldapBase' => $this->base,
97
-			'ldapAgentName' => $this->server['dn'],
98
-			'ldapAgentPassword' => $this->server['pwd'],
99
-			'ldapUserFilter' => 'objectclass=inetOrgPerson',
100
-			'ldapUserDisplayName' => 'cn',
101
-			'ldapGroupDisplayName' => 'cn',
102
-			'ldapLoginFilter' => '(|(uid=%uid)(samaccountname=%uid))',
103
-			'ldapCacheTTL' => 0,
104
-			'ldapConfigurationActive' => 1,
105
-		]);
106
-	}
107
-
108
-	/**
109
-	 * initializes an LDAP user manager instance
110
-	 */
111
-	protected function initUserManager() {
112
-		$this->userManager = new Manager(
113
-			Server::get(IConfig::class),
114
-			Server::get(LoggerInterface::class),
115
-			Server::get(IAvatarManager::class),
116
-			new Image(),
117
-			Server::get(IUserManager::class),
118
-			Server::get(\OCP\Notification\IManager::class),
119
-			Server::get(IManager::class)
120
-		);
121
-	}
122
-
123
-	/**
124
-	 * initializes the test Helper
125
-	 */
126
-	protected function initHelper() {
127
-		$this->helper = Server::get(Helper::class);
128
-	}
129
-
130
-	/**
131
-	 * initializes the Access test instance
132
-	 */
133
-	protected function initAccess() {
134
-		$this->access = new Access($this->connection, $this->ldap, $this->userManager, $this->helper, Server::get(IConfig::class), Server::get(LoggerInterface::class));
135
-	}
136
-
137
-	/**
138
-	 * runs the test cases while outputting progress and result information
139
-	 *
140
-	 * If a test failed, the script is exited with return code 1.
141
-	 */
142
-	public function run() {
143
-		$methods = get_class_methods($this);
144
-		$atLeastOneCaseRan = false;
145
-		foreach ($methods as $method) {
146
-			if (str_starts_with($method, 'case')) {
147
-				print("running $method " . PHP_EOL);
148
-				try {
149
-					if (!$this->$method()) {
150
-						print(PHP_EOL . '>>> !!! Test ' . $method . ' FAILED !!! <<<' . PHP_EOL . PHP_EOL);
151
-						exit(1);
152
-					}
153
-					$atLeastOneCaseRan = true;
154
-				} catch (\Exception $e) {
155
-					print(PHP_EOL . '>>> !!! Test ' . $method . ' RAISED AN EXCEPTION !!! <<<' . PHP_EOL);
156
-					print($e->getMessage() . PHP_EOL . PHP_EOL);
157
-					exit(1);
158
-				}
159
-			}
160
-		}
161
-		if ($atLeastOneCaseRan) {
162
-			print('Tests succeeded' . PHP_EOL);
163
-		} else {
164
-			print('No Test was available.' . PHP_EOL);
165
-			exit(1);
166
-		}
167
-	}
26
+    /** @var LDAP */
27
+    protected $ldap;
28
+
29
+    /** @var Connection */
30
+    protected $connection;
31
+
32
+    /** @var Access */
33
+    protected $access;
34
+
35
+    /** @var Manager */
36
+    protected $userManager;
37
+
38
+    /** @var Helper */
39
+    protected $helper;
40
+
41
+    /** @var string[] */
42
+    protected $server;
43
+
44
+    /**
45
+     * @param string $base
46
+     */
47
+    public function __construct(
48
+        $host,
49
+        $port,
50
+        $bind,
51
+        $pwd,
52
+        protected $base,
53
+    ) {
54
+        $this->server = [
55
+            'host' => $host,
56
+            'port' => $port,
57
+            'dn' => $bind,
58
+            'pwd' => $pwd
59
+        ];
60
+    }
61
+
62
+    /**
63
+     * prepares the LDAP environment and sets up a test configuration for
64
+     * the LDAP backend.
65
+     */
66
+    public function init() {
67
+        \OC::$server->registerService(UserPluginManager::class, function () {
68
+            return new UserPluginManager();
69
+        });
70
+        \OC::$server->registerService(GroupPluginManager::class, function () {
71
+            return new GroupPluginManager();
72
+        });
73
+
74
+        $this->initLDAPWrapper();
75
+        $this->initConnection();
76
+        $this->initUserManager();
77
+        $this->initHelper();
78
+        $this->initAccess();
79
+    }
80
+
81
+    /**
82
+     * initializes the test LDAP wrapper
83
+     */
84
+    protected function initLDAPWrapper() {
85
+        $this->ldap = new LDAP();
86
+    }
87
+
88
+    /**
89
+     * sets up the LDAP configuration to be used for the test
90
+     */
91
+    protected function initConnection() {
92
+        $this->connection = new Connection($this->ldap, '', null);
93
+        $this->connection->setConfiguration([
94
+            'ldapHost' => $this->server['host'],
95
+            'ldapPort' => $this->server['port'],
96
+            'ldapBase' => $this->base,
97
+            'ldapAgentName' => $this->server['dn'],
98
+            'ldapAgentPassword' => $this->server['pwd'],
99
+            'ldapUserFilter' => 'objectclass=inetOrgPerson',
100
+            'ldapUserDisplayName' => 'cn',
101
+            'ldapGroupDisplayName' => 'cn',
102
+            'ldapLoginFilter' => '(|(uid=%uid)(samaccountname=%uid))',
103
+            'ldapCacheTTL' => 0,
104
+            'ldapConfigurationActive' => 1,
105
+        ]);
106
+    }
107
+
108
+    /**
109
+     * initializes an LDAP user manager instance
110
+     */
111
+    protected function initUserManager() {
112
+        $this->userManager = new Manager(
113
+            Server::get(IConfig::class),
114
+            Server::get(LoggerInterface::class),
115
+            Server::get(IAvatarManager::class),
116
+            new Image(),
117
+            Server::get(IUserManager::class),
118
+            Server::get(\OCP\Notification\IManager::class),
119
+            Server::get(IManager::class)
120
+        );
121
+    }
122
+
123
+    /**
124
+     * initializes the test Helper
125
+     */
126
+    protected function initHelper() {
127
+        $this->helper = Server::get(Helper::class);
128
+    }
129
+
130
+    /**
131
+     * initializes the Access test instance
132
+     */
133
+    protected function initAccess() {
134
+        $this->access = new Access($this->connection, $this->ldap, $this->userManager, $this->helper, Server::get(IConfig::class), Server::get(LoggerInterface::class));
135
+    }
136
+
137
+    /**
138
+     * runs the test cases while outputting progress and result information
139
+     *
140
+     * If a test failed, the script is exited with return code 1.
141
+     */
142
+    public function run() {
143
+        $methods = get_class_methods($this);
144
+        $atLeastOneCaseRan = false;
145
+        foreach ($methods as $method) {
146
+            if (str_starts_with($method, 'case')) {
147
+                print("running $method " . PHP_EOL);
148
+                try {
149
+                    if (!$this->$method()) {
150
+                        print(PHP_EOL . '>>> !!! Test ' . $method . ' FAILED !!! <<<' . PHP_EOL . PHP_EOL);
151
+                        exit(1);
152
+                    }
153
+                    $atLeastOneCaseRan = true;
154
+                } catch (\Exception $e) {
155
+                    print(PHP_EOL . '>>> !!! Test ' . $method . ' RAISED AN EXCEPTION !!! <<<' . PHP_EOL);
156
+                    print($e->getMessage() . PHP_EOL . PHP_EOL);
157
+                    exit(1);
158
+                }
159
+            }
160
+        }
161
+        if ($atLeastOneCaseRan) {
162
+            print('Tests succeeded' . PHP_EOL);
163
+        } else {
164
+            print('No Test was available.' . PHP_EOL);
165
+            exit(1);
166
+        }
167
+    }
168 168
 }
Please login to merge, or discard this patch.
apps/user_ldap/tests/AccessTest.php 1 patch
Indentation   +710 added lines, -710 removed lines patch added patch discarded remove patch
@@ -42,714 +42,714 @@
 block discarded – undo
42 42
  * @package OCA\User_LDAP\Tests
43 43
  */
44 44
 class AccessTest extends TestCase {
45
-	protected UserMapping&MockObject $userMapper;
46
-	protected IManager&MockObject $shareManager;
47
-	protected GroupMapping&MockObject $groupMapper;
48
-	private Connection&MockObject $connection;
49
-	private LDAP&MockObject $ldap;
50
-	private Manager&MockObject $userManager;
51
-	private Helper&MockObject $helper;
52
-	private IConfig&MockObject $config;
53
-	private IUserManager&MockObject $ncUserManager;
54
-	private LoggerInterface&MockObject $logger;
55
-	private IAppConfig&MockObject $appConfig;
56
-	private IEventDispatcher&MockObject $dispatcher;
57
-	private Access $access;
58
-
59
-	protected function setUp(): void {
60
-		$this->ldap = $this->createMock(LDAP::class);
61
-		$this->connection = $this->getMockBuilder(Connection::class)
62
-			->setConstructorArgs([$this->ldap])
63
-			->getMock();
64
-		$this->userManager = $this->createMock(Manager::class);
65
-		$this->helper = $this->createMock(Helper::class);
66
-		$this->config = $this->createMock(IConfig::class);
67
-		$this->userMapper = $this->createMock(UserMapping::class);
68
-		$this->groupMapper = $this->createMock(GroupMapping::class);
69
-		$this->ncUserManager = $this->createMock(IUserManager::class);
70
-		$this->shareManager = $this->createMock(IManager::class);
71
-		$this->logger = $this->createMock(LoggerInterface::class);
72
-		$this->appConfig = $this->createMock(IAppConfig::class);
73
-		$this->dispatcher = $this->createMock(IEventDispatcher::class);
74
-
75
-		$this->access = new Access(
76
-			$this->ldap,
77
-			$this->connection,
78
-			$this->userManager,
79
-			$this->helper,
80
-			$this->config,
81
-			$this->ncUserManager,
82
-			$this->logger,
83
-			$this->appConfig,
84
-			$this->dispatcher,
85
-		);
86
-		$this->dispatcher->expects($this->any())->method('dispatchTyped');
87
-		$this->access->setUserMapper($this->userMapper);
88
-		$this->access->setGroupMapper($this->groupMapper);
89
-	}
90
-
91
-	private function getConnectorAndLdapMock() {
92
-		/** @var ILDAPWrapper&MockObject */
93
-		$lw = $this->createMock(ILDAPWrapper::class);
94
-		/** @var Connection&MockObject */
95
-		$connector = $this->getMockBuilder(Connection::class)
96
-			->setConstructorArgs([$lw, '', null])
97
-			->getMock();
98
-		$connector->expects($this->any())
99
-			->method('getConnectionResource')
100
-			->willReturn(ldap_connect('ldap://example.com'));
101
-		/** @var Manager&MockObject */
102
-		$um = $this->getMockBuilder(Manager::class)
103
-			->setConstructorArgs([
104
-				$this->createMock(IConfig::class),
105
-				$this->createMock(LoggerInterface::class),
106
-				$this->createMock(IAvatarManager::class),
107
-				$this->createMock(Image::class),
108
-				$this->createMock(IUserManager::class),
109
-				$this->createMock(INotificationManager::class),
110
-				$this->shareManager])
111
-			->getMock();
112
-		$helper = Server::get(Helper::class);
113
-
114
-		return [$lw, $connector, $um, $helper];
115
-	}
116
-
117
-	public function testEscapeFilterPartValidChars(): void {
118
-		$input = 'okay';
119
-		$this->assertSame($input, $this->access->escapeFilterPart($input));
120
-	}
121
-
122
-	public function testEscapeFilterPartEscapeWildcard(): void {
123
-		$input = '*';
124
-		$expected = '\\2a';
125
-		$this->assertSame($expected, $this->access->escapeFilterPart($input));
126
-	}
127
-
128
-	public function testEscapeFilterPartEscapeWildcard2(): void {
129
-		$input = 'foo*bar';
130
-		$expected = 'foo\\2abar';
131
-		$this->assertSame($expected, $this->access->escapeFilterPart($input));
132
-	}
133
-
134
-	/**
135
-	 * @dataProvider convertSID2StrSuccessData
136
-	 * @param array $sidArray
137
-	 * @param $sidExpected
138
-	 */
139
-	public function testConvertSID2StrSuccess(array $sidArray, $sidExpected): void {
140
-		$sidBinary = implode('', $sidArray);
141
-		$this->assertSame($sidExpected, $this->access->convertSID2Str($sidBinary));
142
-	}
143
-
144
-	public static function convertSID2StrSuccessData(): array {
145
-		return [
146
-			[
147
-				[
148
-					"\x01",
149
-					"\x04",
150
-					"\x00\x00\x00\x00\x00\x05",
151
-					"\x15\x00\x00\x00",
152
-					"\xa6\x81\xe5\x0e",
153
-					"\x4d\x6c\x6c\x2b",
154
-					"\xca\x32\x05\x5f",
155
-				],
156
-				'S-1-5-21-249921958-728525901-1594176202',
157
-			],
158
-			[
159
-				[
160
-					"\x01",
161
-					"\x02",
162
-					"\xFF\xFF\xFF\xFF\xFF\xFF",
163
-					"\xFF\xFF\xFF\xFF",
164
-					"\xFF\xFF\xFF\xFF",
165
-				],
166
-				'S-1-281474976710655-4294967295-4294967295',
167
-			],
168
-		];
169
-	}
170
-
171
-	public function testConvertSID2StrInputError(): void {
172
-		$sidIllegal = 'foobar';
173
-		$sidExpected = '';
174
-
175
-		$this->assertSame($sidExpected, $this->access->convertSID2Str($sidIllegal));
176
-	}
177
-
178
-	public function testGetDomainDNFromDNSuccess(): void {
179
-		$inputDN = 'uid=zaphod,cn=foobar,dc=my,dc=server,dc=com';
180
-		$domainDN = 'dc=my,dc=server,dc=com';
181
-
182
-		$this->ldap->expects($this->once())
183
-			->method('explodeDN')
184
-			->with($inputDN, 0)
185
-			->willReturn(explode(',', $inputDN));
186
-
187
-		$this->assertSame($domainDN, $this->access->getDomainDNFromDN($inputDN));
188
-	}
189
-
190
-	public function testGetDomainDNFromDNError(): void {
191
-		$inputDN = 'foobar';
192
-		$expected = '';
193
-
194
-		$this->ldap->expects($this->once())
195
-			->method('explodeDN')
196
-			->with($inputDN, 0)
197
-			->willReturn(false);
198
-
199
-		$this->assertSame($expected, $this->access->getDomainDNFromDN($inputDN));
200
-	}
201
-
202
-	public static function dnInputDataProvider(): array {
203
-		return [
204
-			[
205
-				'foo=bar,bar=foo,dc=foobar',
206
-				[
207
-					'count' => 3,
208
-					0 => 'foo=bar',
209
-					1 => 'bar=foo',
210
-					2 => 'dc=foobar'
211
-				],
212
-				true
213
-			],
214
-			[
215
-				'foobarbarfoodcfoobar',
216
-				false,
217
-				false
218
-			]
219
-		];
220
-	}
221
-
222
-	/**
223
-	 * @dataProvider dnInputDataProvider
224
-	 */
225
-	public function testStringResemblesDN(string $input, array|bool $interResult, bool $expectedResult): void {
226
-		[$lw, $con, $um, $helper] = $this->getConnectorAndLdapMock();
227
-		/** @var IConfig&MockObject $config */
228
-		$config = $this->createMock(IConfig::class);
229
-		$access = new Access($lw, $con, $um, $helper, $config, $this->ncUserManager, $this->logger, $this->appConfig, $this->dispatcher);
230
-
231
-		$lw->expects($this->exactly(1))
232
-			->method('explodeDN')
233
-			->willReturnCallback(function ($dn) use ($input, $interResult) {
234
-				if ($dn === $input) {
235
-					return $interResult;
236
-				}
237
-				return null;
238
-			});
239
-
240
-		$this->assertSame($expectedResult, $access->stringResemblesDN($input));
241
-	}
242
-
243
-	/**
244
-	 * @dataProvider dnInputDataProvider
245
-	 */
246
-	public function testStringResemblesDNLDAPmod(string $input, array|bool $interResult, bool $expectedResult): void {
247
-		[, $con, $um, $helper] = $this->getConnectorAndLdapMock();
248
-		/** @var IConfig&MockObject $config */
249
-		$config = $this->createMock(IConfig::class);
250
-		$lw = new LDAP();
251
-		$access = new Access($lw, $con, $um, $helper, $config, $this->ncUserManager, $this->logger, $this->appConfig, $this->dispatcher);
252
-
253
-		if (!function_exists('ldap_explode_dn')) {
254
-			$this->markTestSkipped('LDAP Module not available');
255
-		}
256
-
257
-		$this->assertSame($expectedResult, $access->stringResemblesDN($input));
258
-	}
259
-
260
-	public function testCacheUserHome(): void {
261
-		$this->connection->expects($this->once())
262
-			->method('writeToCache');
263
-
264
-		$this->access->cacheUserHome('foobar', '/foobars/path');
265
-	}
266
-
267
-	public function testBatchApplyUserAttributes(): void {
268
-		$this->ldap->expects($this->any())
269
-			->method('isResource')
270
-			->willReturn(true);
271
-
272
-		$this->connection
273
-			->expects($this->any())
274
-			->method('getConnectionResource')
275
-			->willReturn(ldap_connect('ldap://example.com'));
276
-
277
-		$this->ldap->expects($this->any())
278
-			->method('getAttributes')
279
-			->willReturn(['displayname' => ['bar', 'count' => 1]]);
280
-
281
-		/** @var UserMapping&MockObject $mapperMock */
282
-		$mapperMock = $this->createMock(UserMapping::class);
283
-		$mapperMock->expects($this->any())
284
-			->method('getNameByDN')
285
-			->willReturn(false);
286
-		$mapperMock->expects($this->any())
287
-			->method('map')
288
-			->willReturn(true);
289
-
290
-		$userMock = $this->createMock(User::class);
291
-
292
-		// also returns for userUuidAttribute
293
-		$this->access->connection->expects($this->any())
294
-			->method('__get')
295
-			->willReturn('displayName');
296
-
297
-		$this->access->setUserMapper($mapperMock);
298
-
299
-		$displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName);
300
-		$data = [
301
-			[
302
-				'dn' => ['foobar'],
303
-				$displayNameAttribute => 'barfoo'
304
-			],
305
-			[
306
-				'dn' => ['foo'],
307
-				$displayNameAttribute => 'bar'
308
-			],
309
-			[
310
-				'dn' => ['raboof'],
311
-				$displayNameAttribute => 'oofrab'
312
-			]
313
-		];
314
-
315
-		$userMock->expects($this->exactly(count($data)))
316
-			->method('processAttributes');
317
-
318
-		$this->userManager->expects($this->exactly(count($data) * 2))
319
-			->method('get')
320
-			->willReturn($userMock);
321
-
322
-		$this->access->batchApplyUserAttributes($data);
323
-	}
324
-
325
-	public function testBatchApplyUserAttributesSkipped(): void {
326
-		/** @var UserMapping&MockObject $mapperMock */
327
-		$mapperMock = $this->createMock(UserMapping::class);
328
-		$mapperMock->expects($this->any())
329
-			->method('getNameByDN')
330
-			->willReturn('a_username');
331
-
332
-		$userMock = $this->createMock(User::class);
333
-
334
-		$this->access->connection->expects($this->any())
335
-			->method('__get')
336
-			->willReturn('displayName');
337
-
338
-		$this->access->setUserMapper($mapperMock);
339
-
340
-		$displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName);
341
-		$data = [
342
-			[
343
-				'dn' => ['foobar'],
344
-				$displayNameAttribute => 'barfoo'
345
-			],
346
-			[
347
-				'dn' => ['foo'],
348
-				$displayNameAttribute => 'bar'
349
-			],
350
-			[
351
-				'dn' => ['raboof'],
352
-				$displayNameAttribute => 'oofrab'
353
-			]
354
-		];
355
-
356
-		$userMock->expects($this->never())
357
-			->method('processAttributes');
358
-
359
-		$this->userManager->expects($this->any())
360
-			->method('get')
361
-			->willReturn($this->createMock(User::class));
362
-
363
-		$this->access->batchApplyUserAttributes($data);
364
-	}
365
-
366
-	public function testBatchApplyUserAttributesDontSkip(): void {
367
-		/** @var UserMapping&MockObject $mapperMock */
368
-		$mapperMock = $this->createMock(UserMapping::class);
369
-		$mapperMock->expects($this->any())
370
-			->method('getNameByDN')
371
-			->willReturn('a_username');
372
-
373
-		$userMock = $this->createMock(User::class);
374
-
375
-		$this->access->connection->expects($this->any())
376
-			->method('__get')
377
-			->willReturn('displayName');
378
-
379
-		$this->access->setUserMapper($mapperMock);
380
-
381
-		$displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName);
382
-		$data = [
383
-			[
384
-				'dn' => ['foobar'],
385
-				$displayNameAttribute => 'barfoo'
386
-			],
387
-			[
388
-				'dn' => ['foo'],
389
-				$displayNameAttribute => 'bar'
390
-			],
391
-			[
392
-				'dn' => ['raboof'],
393
-				$displayNameAttribute => 'oofrab'
394
-			]
395
-		];
396
-
397
-		$userMock->expects($this->exactly(count($data)))
398
-			->method('processAttributes');
399
-
400
-		$this->userManager->expects($this->exactly(count($data) * 2))
401
-			->method('get')
402
-			->willReturn($userMock);
403
-
404
-		$this->access->batchApplyUserAttributes($data);
405
-	}
406
-
407
-	public static function dNAttributeProvider(): array {
408
-		// corresponds to Access::resemblesDN()
409
-		return [
410
-			'dn' => ['dn'],
411
-			'uniqueMember' => ['uniquemember'],
412
-			'member' => ['member'],
413
-			'memberOf' => ['memberof']
414
-		];
415
-	}
416
-
417
-	/**
418
-	 * @dataProvider dNAttributeProvider
419
-	 */
420
-	public function testSanitizeDN(string $attribute): void {
421
-		[$lw, $con, $um, $helper] = $this->getConnectorAndLdapMock();
422
-		/** @var IConfig&MockObject $config */
423
-		$config = $this->createMock(IConfig::class);
424
-
425
-		$dnFromServer = 'cn=Mixed Cases,ou=Are Sufficient To,ou=Test,dc=example,dc=org';
426
-
427
-		$lw->expects($this->any())
428
-			->method('isResource')
429
-			->willReturn(true);
430
-		$lw->expects($this->any())
431
-			->method('getAttributes')
432
-			->willReturn([
433
-				$attribute => ['count' => 1, $dnFromServer]
434
-			]);
435
-
436
-		$access = new Access($lw, $con, $um, $helper, $config, $this->ncUserManager, $this->logger, $this->appConfig, $this->dispatcher);
437
-		$values = $access->readAttribute('uid=whoever,dc=example,dc=org', $attribute);
438
-		$this->assertSame($values[0], strtolower($dnFromServer));
439
-	}
440
-
441
-
442
-	public function testSetPasswordWithDisabledChanges(): void {
443
-		$this->expectException(\Exception::class);
444
-		$this->expectExceptionMessage('LDAP password changes are disabled');
445
-
446
-		$this->connection
447
-			->method('__get')
448
-			->willReturn(false);
449
-
450
-		/** @noinspection PhpUnhandledExceptionInspection */
451
-		$this->access->setPassword('CN=foo', 'MyPassword');
452
-	}
453
-
454
-	public function testSetPasswordWithLdapNotAvailable(): void {
455
-		$this->connection
456
-			->method('__get')
457
-			->willReturn(true);
458
-		$connection = ldap_connect('ldap://example.com');
459
-		$this->connection
460
-			->expects($this->once())
461
-			->method('getConnectionResource')
462
-			->willThrowException(new ServerNotAvailableException('Connection to LDAP server could not be established'));
463
-		$this->ldap
464
-			->expects($this->never())
465
-			->method('isResource');
466
-
467
-		$this->expectException(ServerNotAvailableException::class);
468
-		$this->expectExceptionMessage('Connection to LDAP server could not be established');
469
-		$this->access->setPassword('CN=foo', 'MyPassword');
470
-	}
471
-
472
-
473
-	public function testSetPasswordWithRejectedChange(): void {
474
-		$this->expectException(HintException::class);
475
-		$this->expectExceptionMessage('Password change rejected.');
476
-
477
-		$this->connection
478
-			->method('__get')
479
-			->willReturn(true);
480
-		$connection = ldap_connect('ldap://example.com');
481
-		$this->connection
482
-			->expects($this->any())
483
-			->method('getConnectionResource')
484
-			->willReturn($connection);
485
-		$this->ldap
486
-			->expects($this->once())
487
-			->method('modReplace')
488
-			->with($connection, 'CN=foo', 'MyPassword')
489
-			->willThrowException(new ConstraintViolationException());
490
-
491
-		/** @noinspection PhpUnhandledExceptionInspection */
492
-		$this->access->setPassword('CN=foo', 'MyPassword');
493
-	}
494
-
495
-	public function testSetPassword(): void {
496
-		$this->connection
497
-			->method('__get')
498
-			->willReturn(true);
499
-		$connection = ldap_connect('ldap://example.com');
500
-		$this->connection
501
-			->expects($this->any())
502
-			->method('getConnectionResource')
503
-			->willReturn($connection);
504
-		$this->ldap
505
-			->expects($this->once())
506
-			->method('modReplace')
507
-			->with($connection, 'CN=foo', 'MyPassword')
508
-			->willReturn(true);
509
-
510
-		/** @noinspection PhpUnhandledExceptionInspection */
511
-		$this->assertTrue($this->access->setPassword('CN=foo', 'MyPassword'));
512
-	}
513
-
514
-	protected function prepareMocksForSearchTests(
515
-		$base,
516
-		$fakeConnection,
517
-		$fakeSearchResultResource,
518
-		$fakeLdapEntries,
519
-	) {
520
-		$this->connection
521
-			->expects($this->any())
522
-			->method('getConnectionResource')
523
-			->willReturn($fakeConnection);
524
-		$this->connection->expects($this->any())
525
-			->method('__get')
526
-			->willReturnCallback(function ($key) use ($base) {
527
-				if (stripos($key, 'base') !== false) {
528
-					return [$base];
529
-				}
530
-				return null;
531
-			});
532
-
533
-		$this->ldap
534
-			->expects($this->any())
535
-			->method('isResource')
536
-			->willReturnCallback(function ($resource) {
537
-				return is_object($resource);
538
-			});
539
-		$this->ldap
540
-			->expects($this->any())
541
-			->method('errno')
542
-			->willReturn(0);
543
-		$this->ldap
544
-			->expects($this->once())
545
-			->method('search')
546
-			->willReturn($fakeSearchResultResource);
547
-		$this->ldap
548
-			->expects($this->exactly(1))
549
-			->method('getEntries')
550
-			->willReturn($fakeLdapEntries);
551
-
552
-		$this->helper->expects($this->any())
553
-			->method('sanitizeDN')
554
-			->willReturnArgument(0);
555
-	}
556
-
557
-	public function testSearchNoPagedSearch(): void {
558
-		// scenario: no pages search, 1 search base
559
-		$filter = 'objectClass=nextcloudUser';
560
-		$base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com';
561
-
562
-		$fakeConnection = ldap_connect();
563
-		$fakeSearchResultResource = ldap_connect();
564
-		$fakeLdapEntries = [
565
-			'count' => 2,
566
-			[
567
-				'dn' => 'uid=sgarth,' . $base,
568
-			],
569
-			[
570
-				'dn' => 'uid=wwilson,' . $base,
571
-			]
572
-		];
573
-
574
-		$expected = $fakeLdapEntries;
575
-		unset($expected['count']);
576
-
577
-		$this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries);
578
-
579
-		/** @noinspection PhpUnhandledExceptionInspection */
580
-		$result = $this->access->search($filter, $base);
581
-		$this->assertSame($expected, $result);
582
-	}
583
-
584
-	public function testFetchListOfUsers(): void {
585
-		$filter = 'objectClass=nextcloudUser';
586
-		$base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com';
587
-		$attrs = ['dn', 'uid'];
588
-
589
-		$fakeConnection = ldap_connect();
590
-		$fakeSearchResultResource = ldap_connect();
591
-		$fakeLdapEntries = [
592
-			'count' => 2,
593
-			[
594
-				'dn' => 'uid=sgarth,' . $base,
595
-				'uid' => [ 'sgarth' ],
596
-			],
597
-			[
598
-				'dn' => 'uid=wwilson,' . $base,
599
-				'uid' => [ 'wwilson' ],
600
-			]
601
-		];
602
-		$expected = $fakeLdapEntries;
603
-		unset($expected['count']);
604
-		array_walk($expected, function (&$v): void {
605
-			$v['dn'] = [$v['dn']];	// dn is translated into an array internally for consistency
606
-		});
607
-
608
-		$this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries);
609
-
610
-		// Called twice per user, for userExists and userExistsOnLdap
611
-		$this->connection->expects($this->exactly(2 * $fakeLdapEntries['count']))
612
-			->method('writeToCache')
613
-			->with($this->stringStartsWith('userExists'), true);
614
-
615
-		$this->userMapper->expects($this->exactly($fakeLdapEntries['count']))
616
-			->method('getNameByDN')
617
-			->willReturnCallback(function ($fdn) {
618
-				$parts = ldap_explode_dn($fdn, 0);
619
-				return $parts[0];
620
-			});
621
-
622
-		/** @noinspection PhpUnhandledExceptionInspection */
623
-		$list = $this->access->fetchListOfUsers($filter, $attrs);
624
-		$this->assertSame($expected, $list);
625
-	}
626
-
627
-	public function testFetchListOfGroupsKnown(): void {
628
-		$filter = 'objectClass=nextcloudGroup';
629
-		$attributes = ['cn', 'gidNumber', 'dn'];
630
-		$base = 'ou=SomeGroups,dc=my,dc=directory';
631
-
632
-		$fakeConnection = ldap_connect();
633
-		$fakeSearchResultResource = ldap_connect();
634
-		$fakeLdapEntries = [
635
-			'count' => 2,
636
-			[
637
-				'dn' => 'cn=Good Team,' . $base,
638
-				'cn' => ['Good Team'],
639
-			],
640
-			[
641
-				'dn' => 'cn=Another Good Team,' . $base,
642
-				'cn' => ['Another Good Team'],
643
-			]
644
-		];
645
-
646
-		$this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries);
647
-
648
-		$this->groupMapper->expects($this->any())
649
-			->method('getListOfIdsByDn')
650
-			->willReturn([
651
-				'cn=Good Team,' . $base => 'Good_Team',
652
-				'cn=Another Good Team,' . $base => 'Another_Good_Team',
653
-			]);
654
-		$this->groupMapper->expects($this->never())
655
-			->method('getNameByDN');
656
-
657
-		$this->connection->expects($this->exactly(1))
658
-			->method('writeToCache');
659
-
660
-		$groups = $this->access->fetchListOfGroups($filter, $attributes);
661
-		$this->assertSame(2, count($groups));
662
-		$this->assertSame('Good Team', $groups[0]['cn'][0]);
663
-		$this->assertSame('Another Good Team', $groups[1]['cn'][0]);
664
-	}
665
-
666
-	public static function intUsernameProvider(): array {
667
-		return [
668
-			['alice', 'alice'],
669
-			['b/ob', 'bob'],
670
-			['charly
Please login to merge, or discard this patch.