Completed
Push — master ( 5488f7...d0a5ee )
by
unknown
51:30 queued 23:36
created
apps/user_ldap/lib/LDAP.php 2 patches
Indentation   +389 added lines, -389 removed lines patch added patch discarded remove patch
@@ -17,393 +17,393 @@
 block discarded – undo
17 17
 use Psr\Log\LoggerInterface;
18 18
 
19 19
 class LDAP implements ILDAPWrapper {
20
-	protected array $curArgs = [];
21
-	protected LoggerInterface $logger;
22
-	protected IConfig $config;
23
-
24
-	private ?LdapDataCollector $dataCollector = null;
25
-
26
-	public function __construct(
27
-		protected string $logFile = '',
28
-	) {
29
-		/** @var IProfiler $profiler */
30
-		$profiler = Server::get(IProfiler::class);
31
-		if ($profiler->isEnabled()) {
32
-			$this->dataCollector = new LdapDataCollector();
33
-			$profiler->add($this->dataCollector);
34
-		}
35
-
36
-		$this->logger = Server::get(LoggerInterface::class);
37
-		$this->config = Server::get(IConfig::class);
38
-	}
39
-
40
-	/**
41
-	 * {@inheritDoc}
42
-	 */
43
-	public function bind($link, $dn, $password) {
44
-		return $this->invokeLDAPMethod('bind', $link, $dn, $password);
45
-	}
46
-
47
-	/**
48
-	 * {@inheritDoc}
49
-	 */
50
-	public function connect($host, $port) {
51
-		$pos = strpos($host, '://');
52
-		if ($pos === false) {
53
-			$host = 'ldap://' . $host;
54
-			$pos = 4;
55
-		}
56
-		if (strpos($host, ':', $pos + 1) === false && !empty($port)) {
57
-			//ldap_connect ignores port parameter when URLs are passed
58
-			$host .= ':' . $port;
59
-		}
60
-		return $this->invokeLDAPMethod('connect', $host);
61
-	}
62
-
63
-	/**
64
-	 * {@inheritDoc}
65
-	 */
66
-	public function controlPagedResultResponse($link, $result, &$cookie): bool {
67
-		$errorCode = 0;
68
-		$errorMsg = '';
69
-		$controls = [];
70
-		$matchedDn = null;
71
-		$referrals = [];
72
-
73
-		/** Cannot use invokeLDAPMethod because arguments are passed by reference */
74
-		$this->preFunctionCall('ldap_parse_result', [$link, $result]);
75
-		$success = ldap_parse_result($link, $result,
76
-			$errorCode,
77
-			$matchedDn,
78
-			$errorMsg,
79
-			$referrals,
80
-			$controls);
81
-		if ($errorCode !== 0) {
82
-			$this->processLDAPError($link, 'ldap_parse_result', $errorCode, $errorMsg);
83
-		}
84
-		if ($this->dataCollector !== null) {
85
-			$this->dataCollector->stopLastLdapRequest();
86
-		}
87
-
88
-		$cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? '';
89
-
90
-		return $success;
91
-	}
92
-
93
-	/**
94
-	 * {@inheritDoc}
95
-	 */
96
-	public function countEntries($link, $result) {
97
-		return $this->invokeLDAPMethod('count_entries', $link, $result);
98
-	}
99
-
100
-	/**
101
-	 * {@inheritDoc}
102
-	 */
103
-	public function errno($link) {
104
-		return $this->invokeLDAPMethod('errno', $link);
105
-	}
106
-
107
-	/**
108
-	 * {@inheritDoc}
109
-	 */
110
-	public function error($link) {
111
-		return $this->invokeLDAPMethod('error', $link);
112
-	}
113
-
114
-	/**
115
-	 * Splits DN into its component parts
116
-	 * @param string $dn
117
-	 * @param int $withAttrib
118
-	 * @return array|false
119
-	 * @link https://www.php.net/manual/en/function.ldap-explode-dn.php
120
-	 */
121
-	public function explodeDN($dn, $withAttrib) {
122
-		return $this->invokeLDAPMethod('explode_dn', $dn, $withAttrib);
123
-	}
124
-
125
-	/**
126
-	 * {@inheritDoc}
127
-	 */
128
-	public function firstEntry($link, $result) {
129
-		return $this->invokeLDAPMethod('first_entry', $link, $result);
130
-	}
131
-
132
-	/**
133
-	 * {@inheritDoc}
134
-	 */
135
-	public function getAttributes($link, $result) {
136
-		return $this->invokeLDAPMethod('get_attributes', $link, $result);
137
-	}
138
-
139
-	/**
140
-	 * {@inheritDoc}
141
-	 */
142
-	public function getDN($link, $result) {
143
-		return $this->invokeLDAPMethod('get_dn', $link, $result);
144
-	}
145
-
146
-	/**
147
-	 * {@inheritDoc}
148
-	 */
149
-	public function getEntries($link, $result) {
150
-		return $this->invokeLDAPMethod('get_entries', $link, $result);
151
-	}
152
-
153
-	/**
154
-	 * {@inheritDoc}
155
-	 */
156
-	public function nextEntry($link, $result) {
157
-		return $this->invokeLDAPMethod('next_entry', $link, $result);
158
-	}
159
-
160
-	/**
161
-	 * {@inheritDoc}
162
-	 */
163
-	public function read($link, $baseDN, $filter, $attr) {
164
-		return $this->invokeLDAPMethod('read', $link, $baseDN, $filter, $attr, 0, -1);
165
-	}
166
-
167
-	/**
168
-	 * {@inheritDoc}
169
-	 */
170
-	public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = 0, int $pageSize = 0, string $cookie = '') {
171
-		if ($pageSize > 0 || $cookie !== '') {
172
-			$serverControls = [[
173
-				'oid' => LDAP_CONTROL_PAGEDRESULTS,
174
-				'value' => [
175
-					'size' => $pageSize,
176
-					'cookie' => $cookie,
177
-				],
178
-				'iscritical' => false,
179
-			]];
180
-		} else {
181
-			$serverControls = [];
182
-		}
183
-
184
-		/** @psalm-suppress UndefinedVariable $oldHandler is defined when the closure is called but psalm fails to get that */
185
-		$oldHandler = set_error_handler(function ($no, $message, $file, $line) use (&$oldHandler) {
186
-			if (str_contains($message, 'Partial search results returned: Sizelimit exceeded')) {
187
-				return true;
188
-			}
189
-			$oldHandler($no, $message, $file, $line);
190
-			return true;
191
-		});
192
-		try {
193
-			$result = $this->invokeLDAPMethod('search', $link, $baseDN, $filter, $attr, $attrsOnly, $limit, -1, LDAP_DEREF_NEVER, $serverControls);
194
-
195
-			restore_error_handler();
196
-			return $result;
197
-		} catch (\Exception $e) {
198
-			restore_error_handler();
199
-			throw $e;
200
-		}
201
-	}
202
-
203
-	/**
204
-	 * {@inheritDoc}
205
-	 */
206
-	public function modReplace($link, $userDN, $password) {
207
-		return $this->invokeLDAPMethod('mod_replace', $link, $userDN, ['userPassword' => $password]);
208
-	}
209
-
210
-	/**
211
-	 * {@inheritDoc}
212
-	 */
213
-	public function exopPasswd($link, string $userDN, string $oldPassword, string $password) {
214
-		return $this->invokeLDAPMethod('exop_passwd', $link, $userDN, $oldPassword, $password);
215
-	}
216
-
217
-	/**
218
-	 * {@inheritDoc}
219
-	 */
220
-	public function setOption($link, $option, $value) {
221
-		return $this->invokeLDAPMethod('set_option', $link, $option, $value);
222
-	}
223
-
224
-	/**
225
-	 * {@inheritDoc}
226
-	 */
227
-	public function startTls($link) {
228
-		return $this->invokeLDAPMethod('start_tls', $link);
229
-	}
230
-
231
-	/**
232
-	 * {@inheritDoc}
233
-	 */
234
-	public function unbind($link) {
235
-		return $this->invokeLDAPMethod('unbind', $link);
236
-	}
237
-
238
-	/**
239
-	 * Checks whether the server supports LDAP
240
-	 * @return boolean if it the case, false otherwise
241
-	 * */
242
-	public function areLDAPFunctionsAvailable() {
243
-		return function_exists('ldap_connect');
244
-	}
245
-
246
-	/**
247
-	 * {@inheritDoc}
248
-	 */
249
-	public function isResource($resource) {
250
-		return is_resource($resource) || is_object($resource);
251
-	}
252
-
253
-	/**
254
-	 * Checks whether the return value from LDAP is wrong or not.
255
-	 *
256
-	 * When using ldap_search we provide an array, in case multiple bases are
257
-	 * configured. Thus, we need to check the array elements.
258
-	 *
259
-	 * @param mixed $result
260
-	 */
261
-	protected function isResultFalse(string $functionName, $result): bool {
262
-		if ($result === false) {
263
-			return true;
264
-		}
265
-
266
-		if ($functionName === 'ldap_search' && is_array($result)) {
267
-			foreach ($result as $singleResult) {
268
-				if ($singleResult === false) {
269
-					return true;
270
-				}
271
-			}
272
-		}
273
-
274
-		return false;
275
-	}
276
-
277
-	/**
278
-	 * @param array $arguments
279
-	 * @return mixed
280
-	 */
281
-	protected function invokeLDAPMethod(string $func, ...$arguments) {
282
-		$func = 'ldap_' . $func;
283
-		if (function_exists($func)) {
284
-			$this->preFunctionCall($func, $arguments);
285
-			$result = call_user_func_array($func, $arguments);
286
-			if ($this->isResultFalse($func, $result)) {
287
-				$this->postFunctionCall($func);
288
-			}
289
-			if ($this->dataCollector !== null) {
290
-				$this->dataCollector->stopLastLdapRequest();
291
-			}
292
-			return $result;
293
-		}
294
-		return null;
295
-	}
296
-
297
-	/**
298
-	 * Turn resources into string, and removes potentially problematic cookie string to avoid breaking logfiles
299
-	 */
300
-	private function sanitizeFunctionParameters(array $args): array {
301
-		return array_map(function ($item) {
302
-			if ($this->isResource($item)) {
303
-				return '(resource)';
304
-			}
305
-			if (isset($item[0]['value']['cookie']) && $item[0]['value']['cookie'] !== '') {
306
-				$item[0]['value']['cookie'] = '*opaque cookie*';
307
-			}
308
-			return $item;
309
-		}, $args);
310
-	}
311
-
312
-	private function preFunctionCall(string $functionName, array $args): void {
313
-		$this->curArgs = $args;
314
-		if (strcasecmp($functionName, 'ldap_bind') === 0 || strcasecmp($functionName, 'ldap_exop_passwd') === 0) {
315
-			// The arguments are not key value pairs
316
-			// \OCA\User_LDAP\LDAP::bind passes 3 arguments, the 3rd being the pw
317
-			// Remove it via direct array access for now, although a better solution could be found mebbe?
318
-			// @link https://github.com/nextcloud/server/issues/38461
319
-			$args[2] = IConfig::SENSITIVE_VALUE;
320
-		}
321
-
322
-		if ($this->config->getSystemValue('loglevel') === ILogger::DEBUG) {
323
-			/* Only running this if debug loglevel is on, to avoid processing parameters on production */
324
-			$this->logger->debug('Calling LDAP function {func} with parameters {args}', [
325
-				'app' => 'user_ldap',
326
-				'func' => $functionName,
327
-				'args' => $this->sanitizeFunctionParameters($args),
328
-			]);
329
-		}
330
-
331
-		if ($this->dataCollector !== null) {
332
-			$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
333
-			$this->dataCollector->startLdapRequest($functionName, $this->sanitizeFunctionParameters($args), $backtrace);
334
-		}
335
-
336
-		if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
337
-			file_put_contents(
338
-				$this->logFile,
339
-				$functionName . '::' . json_encode($this->sanitizeFunctionParameters($args)) . "\n",
340
-				FILE_APPEND
341
-			);
342
-		}
343
-	}
344
-
345
-	/**
346
-	 * Analyzes the returned LDAP error and acts accordingly if not 0
347
-	 *
348
-	 * @param \LDAP\Connection $resource the LDAP Connection resource
349
-	 * @throws ConstraintViolationException
350
-	 * @throws ServerNotAvailableException
351
-	 * @throws \Exception
352
-	 */
353
-	private function processLDAPError($resource, string $functionName, int $errorCode, string $errorMsg): void {
354
-		$this->logger->debug('LDAP error {message} ({code}) after calling {func}', [
355
-			'app' => 'user_ldap',
356
-			'message' => $errorMsg,
357
-			'code' => $errorCode,
358
-			'func' => $functionName,
359
-		]);
360
-		if ($functionName === 'ldap_get_entries'
361
-			&& $errorCode === -4) {
362
-		} elseif ($errorCode === 32) {
363
-			//for now
364
-		} elseif ($errorCode === 10) {
365
-			//referrals, we switch them off, but then there is AD :)
366
-		} elseif ($errorCode === -1) {
367
-			throw new ServerNotAvailableException('Lost connection to LDAP server.');
368
-		} elseif ($errorCode === 52) {
369
-			throw new ServerNotAvailableException('LDAP server is shutting down.');
370
-		} elseif ($errorCode === 48) {
371
-			throw new \Exception('LDAP authentication method rejected', $errorCode);
372
-		} elseif ($errorCode === 1) {
373
-			throw new \Exception('LDAP Operations error', $errorCode);
374
-		} elseif ($errorCode === 19) {
375
-			ldap_get_option($resource, LDAP_OPT_ERROR_STRING, $extended_error);
376
-			throw new ConstraintViolationException(!empty($extended_error) ? $extended_error : $errorMsg, $errorCode);
377
-		}
378
-	}
379
-
380
-	/**
381
-	 * Called after an ldap method is run to act on LDAP error if necessary
382
-	 * @throws \Exception
383
-	 */
384
-	private function postFunctionCall(string $functionName): void {
385
-		if ($this->isResource($this->curArgs[0])) {
386
-			$resource = $this->curArgs[0];
387
-		} elseif (
388
-			$functionName === 'ldap_search'
389
-			&& is_array($this->curArgs[0])
390
-			&& $this->isResource($this->curArgs[0][0])
391
-		) {
392
-			// we use always the same LDAP connection resource, is enough to
393
-			// take the first one.
394
-			$resource = $this->curArgs[0][0];
395
-		} else {
396
-			return;
397
-		}
398
-
399
-		$errorCode = ldap_errno($resource);
400
-		if ($errorCode === 0) {
401
-			return;
402
-		}
403
-		$errorMsg = ldap_error($resource);
404
-
405
-		$this->processLDAPError($resource, $functionName, $errorCode, $errorMsg);
406
-
407
-		$this->curArgs = [];
408
-	}
20
+    protected array $curArgs = [];
21
+    protected LoggerInterface $logger;
22
+    protected IConfig $config;
23
+
24
+    private ?LdapDataCollector $dataCollector = null;
25
+
26
+    public function __construct(
27
+        protected string $logFile = '',
28
+    ) {
29
+        /** @var IProfiler $profiler */
30
+        $profiler = Server::get(IProfiler::class);
31
+        if ($profiler->isEnabled()) {
32
+            $this->dataCollector = new LdapDataCollector();
33
+            $profiler->add($this->dataCollector);
34
+        }
35
+
36
+        $this->logger = Server::get(LoggerInterface::class);
37
+        $this->config = Server::get(IConfig::class);
38
+    }
39
+
40
+    /**
41
+     * {@inheritDoc}
42
+     */
43
+    public function bind($link, $dn, $password) {
44
+        return $this->invokeLDAPMethod('bind', $link, $dn, $password);
45
+    }
46
+
47
+    /**
48
+     * {@inheritDoc}
49
+     */
50
+    public function connect($host, $port) {
51
+        $pos = strpos($host, '://');
52
+        if ($pos === false) {
53
+            $host = 'ldap://' . $host;
54
+            $pos = 4;
55
+        }
56
+        if (strpos($host, ':', $pos + 1) === false && !empty($port)) {
57
+            //ldap_connect ignores port parameter when URLs are passed
58
+            $host .= ':' . $port;
59
+        }
60
+        return $this->invokeLDAPMethod('connect', $host);
61
+    }
62
+
63
+    /**
64
+     * {@inheritDoc}
65
+     */
66
+    public function controlPagedResultResponse($link, $result, &$cookie): bool {
67
+        $errorCode = 0;
68
+        $errorMsg = '';
69
+        $controls = [];
70
+        $matchedDn = null;
71
+        $referrals = [];
72
+
73
+        /** Cannot use invokeLDAPMethod because arguments are passed by reference */
74
+        $this->preFunctionCall('ldap_parse_result', [$link, $result]);
75
+        $success = ldap_parse_result($link, $result,
76
+            $errorCode,
77
+            $matchedDn,
78
+            $errorMsg,
79
+            $referrals,
80
+            $controls);
81
+        if ($errorCode !== 0) {
82
+            $this->processLDAPError($link, 'ldap_parse_result', $errorCode, $errorMsg);
83
+        }
84
+        if ($this->dataCollector !== null) {
85
+            $this->dataCollector->stopLastLdapRequest();
86
+        }
87
+
88
+        $cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? '';
89
+
90
+        return $success;
91
+    }
92
+
93
+    /**
94
+     * {@inheritDoc}
95
+     */
96
+    public function countEntries($link, $result) {
97
+        return $this->invokeLDAPMethod('count_entries', $link, $result);
98
+    }
99
+
100
+    /**
101
+     * {@inheritDoc}
102
+     */
103
+    public function errno($link) {
104
+        return $this->invokeLDAPMethod('errno', $link);
105
+    }
106
+
107
+    /**
108
+     * {@inheritDoc}
109
+     */
110
+    public function error($link) {
111
+        return $this->invokeLDAPMethod('error', $link);
112
+    }
113
+
114
+    /**
115
+     * Splits DN into its component parts
116
+     * @param string $dn
117
+     * @param int $withAttrib
118
+     * @return array|false
119
+     * @link https://www.php.net/manual/en/function.ldap-explode-dn.php
120
+     */
121
+    public function explodeDN($dn, $withAttrib) {
122
+        return $this->invokeLDAPMethod('explode_dn', $dn, $withAttrib);
123
+    }
124
+
125
+    /**
126
+     * {@inheritDoc}
127
+     */
128
+    public function firstEntry($link, $result) {
129
+        return $this->invokeLDAPMethod('first_entry', $link, $result);
130
+    }
131
+
132
+    /**
133
+     * {@inheritDoc}
134
+     */
135
+    public function getAttributes($link, $result) {
136
+        return $this->invokeLDAPMethod('get_attributes', $link, $result);
137
+    }
138
+
139
+    /**
140
+     * {@inheritDoc}
141
+     */
142
+    public function getDN($link, $result) {
143
+        return $this->invokeLDAPMethod('get_dn', $link, $result);
144
+    }
145
+
146
+    /**
147
+     * {@inheritDoc}
148
+     */
149
+    public function getEntries($link, $result) {
150
+        return $this->invokeLDAPMethod('get_entries', $link, $result);
151
+    }
152
+
153
+    /**
154
+     * {@inheritDoc}
155
+     */
156
+    public function nextEntry($link, $result) {
157
+        return $this->invokeLDAPMethod('next_entry', $link, $result);
158
+    }
159
+
160
+    /**
161
+     * {@inheritDoc}
162
+     */
163
+    public function read($link, $baseDN, $filter, $attr) {
164
+        return $this->invokeLDAPMethod('read', $link, $baseDN, $filter, $attr, 0, -1);
165
+    }
166
+
167
+    /**
168
+     * {@inheritDoc}
169
+     */
170
+    public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = 0, int $pageSize = 0, string $cookie = '') {
171
+        if ($pageSize > 0 || $cookie !== '') {
172
+            $serverControls = [[
173
+                'oid' => LDAP_CONTROL_PAGEDRESULTS,
174
+                'value' => [
175
+                    'size' => $pageSize,
176
+                    'cookie' => $cookie,
177
+                ],
178
+                'iscritical' => false,
179
+            ]];
180
+        } else {
181
+            $serverControls = [];
182
+        }
183
+
184
+        /** @psalm-suppress UndefinedVariable $oldHandler is defined when the closure is called but psalm fails to get that */
185
+        $oldHandler = set_error_handler(function ($no, $message, $file, $line) use (&$oldHandler) {
186
+            if (str_contains($message, 'Partial search results returned: Sizelimit exceeded')) {
187
+                return true;
188
+            }
189
+            $oldHandler($no, $message, $file, $line);
190
+            return true;
191
+        });
192
+        try {
193
+            $result = $this->invokeLDAPMethod('search', $link, $baseDN, $filter, $attr, $attrsOnly, $limit, -1, LDAP_DEREF_NEVER, $serverControls);
194
+
195
+            restore_error_handler();
196
+            return $result;
197
+        } catch (\Exception $e) {
198
+            restore_error_handler();
199
+            throw $e;
200
+        }
201
+    }
202
+
203
+    /**
204
+     * {@inheritDoc}
205
+     */
206
+    public function modReplace($link, $userDN, $password) {
207
+        return $this->invokeLDAPMethod('mod_replace', $link, $userDN, ['userPassword' => $password]);
208
+    }
209
+
210
+    /**
211
+     * {@inheritDoc}
212
+     */
213
+    public function exopPasswd($link, string $userDN, string $oldPassword, string $password) {
214
+        return $this->invokeLDAPMethod('exop_passwd', $link, $userDN, $oldPassword, $password);
215
+    }
216
+
217
+    /**
218
+     * {@inheritDoc}
219
+     */
220
+    public function setOption($link, $option, $value) {
221
+        return $this->invokeLDAPMethod('set_option', $link, $option, $value);
222
+    }
223
+
224
+    /**
225
+     * {@inheritDoc}
226
+     */
227
+    public function startTls($link) {
228
+        return $this->invokeLDAPMethod('start_tls', $link);
229
+    }
230
+
231
+    /**
232
+     * {@inheritDoc}
233
+     */
234
+    public function unbind($link) {
235
+        return $this->invokeLDAPMethod('unbind', $link);
236
+    }
237
+
238
+    /**
239
+     * Checks whether the server supports LDAP
240
+     * @return boolean if it the case, false otherwise
241
+     * */
242
+    public function areLDAPFunctionsAvailable() {
243
+        return function_exists('ldap_connect');
244
+    }
245
+
246
+    /**
247
+     * {@inheritDoc}
248
+     */
249
+    public function isResource($resource) {
250
+        return is_resource($resource) || is_object($resource);
251
+    }
252
+
253
+    /**
254
+     * Checks whether the return value from LDAP is wrong or not.
255
+     *
256
+     * When using ldap_search we provide an array, in case multiple bases are
257
+     * configured. Thus, we need to check the array elements.
258
+     *
259
+     * @param mixed $result
260
+     */
261
+    protected function isResultFalse(string $functionName, $result): bool {
262
+        if ($result === false) {
263
+            return true;
264
+        }
265
+
266
+        if ($functionName === 'ldap_search' && is_array($result)) {
267
+            foreach ($result as $singleResult) {
268
+                if ($singleResult === false) {
269
+                    return true;
270
+                }
271
+            }
272
+        }
273
+
274
+        return false;
275
+    }
276
+
277
+    /**
278
+     * @param array $arguments
279
+     * @return mixed
280
+     */
281
+    protected function invokeLDAPMethod(string $func, ...$arguments) {
282
+        $func = 'ldap_' . $func;
283
+        if (function_exists($func)) {
284
+            $this->preFunctionCall($func, $arguments);
285
+            $result = call_user_func_array($func, $arguments);
286
+            if ($this->isResultFalse($func, $result)) {
287
+                $this->postFunctionCall($func);
288
+            }
289
+            if ($this->dataCollector !== null) {
290
+                $this->dataCollector->stopLastLdapRequest();
291
+            }
292
+            return $result;
293
+        }
294
+        return null;
295
+    }
296
+
297
+    /**
298
+     * Turn resources into string, and removes potentially problematic cookie string to avoid breaking logfiles
299
+     */
300
+    private function sanitizeFunctionParameters(array $args): array {
301
+        return array_map(function ($item) {
302
+            if ($this->isResource($item)) {
303
+                return '(resource)';
304
+            }
305
+            if (isset($item[0]['value']['cookie']) && $item[0]['value']['cookie'] !== '') {
306
+                $item[0]['value']['cookie'] = '*opaque cookie*';
307
+            }
308
+            return $item;
309
+        }, $args);
310
+    }
311
+
312
+    private function preFunctionCall(string $functionName, array $args): void {
313
+        $this->curArgs = $args;
314
+        if (strcasecmp($functionName, 'ldap_bind') === 0 || strcasecmp($functionName, 'ldap_exop_passwd') === 0) {
315
+            // The arguments are not key value pairs
316
+            // \OCA\User_LDAP\LDAP::bind passes 3 arguments, the 3rd being the pw
317
+            // Remove it via direct array access for now, although a better solution could be found mebbe?
318
+            // @link https://github.com/nextcloud/server/issues/38461
319
+            $args[2] = IConfig::SENSITIVE_VALUE;
320
+        }
321
+
322
+        if ($this->config->getSystemValue('loglevel') === ILogger::DEBUG) {
323
+            /* Only running this if debug loglevel is on, to avoid processing parameters on production */
324
+            $this->logger->debug('Calling LDAP function {func} with parameters {args}', [
325
+                'app' => 'user_ldap',
326
+                'func' => $functionName,
327
+                'args' => $this->sanitizeFunctionParameters($args),
328
+            ]);
329
+        }
330
+
331
+        if ($this->dataCollector !== null) {
332
+            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
333
+            $this->dataCollector->startLdapRequest($functionName, $this->sanitizeFunctionParameters($args), $backtrace);
334
+        }
335
+
336
+        if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
337
+            file_put_contents(
338
+                $this->logFile,
339
+                $functionName . '::' . json_encode($this->sanitizeFunctionParameters($args)) . "\n",
340
+                FILE_APPEND
341
+            );
342
+        }
343
+    }
344
+
345
+    /**
346
+     * Analyzes the returned LDAP error and acts accordingly if not 0
347
+     *
348
+     * @param \LDAP\Connection $resource the LDAP Connection resource
349
+     * @throws ConstraintViolationException
350
+     * @throws ServerNotAvailableException
351
+     * @throws \Exception
352
+     */
353
+    private function processLDAPError($resource, string $functionName, int $errorCode, string $errorMsg): void {
354
+        $this->logger->debug('LDAP error {message} ({code}) after calling {func}', [
355
+            'app' => 'user_ldap',
356
+            'message' => $errorMsg,
357
+            'code' => $errorCode,
358
+            'func' => $functionName,
359
+        ]);
360
+        if ($functionName === 'ldap_get_entries'
361
+            && $errorCode === -4) {
362
+        } elseif ($errorCode === 32) {
363
+            //for now
364
+        } elseif ($errorCode === 10) {
365
+            //referrals, we switch them off, but then there is AD :)
366
+        } elseif ($errorCode === -1) {
367
+            throw new ServerNotAvailableException('Lost connection to LDAP server.');
368
+        } elseif ($errorCode === 52) {
369
+            throw new ServerNotAvailableException('LDAP server is shutting down.');
370
+        } elseif ($errorCode === 48) {
371
+            throw new \Exception('LDAP authentication method rejected', $errorCode);
372
+        } elseif ($errorCode === 1) {
373
+            throw new \Exception('LDAP Operations error', $errorCode);
374
+        } elseif ($errorCode === 19) {
375
+            ldap_get_option($resource, LDAP_OPT_ERROR_STRING, $extended_error);
376
+            throw new ConstraintViolationException(!empty($extended_error) ? $extended_error : $errorMsg, $errorCode);
377
+        }
378
+    }
379
+
380
+    /**
381
+     * Called after an ldap method is run to act on LDAP error if necessary
382
+     * @throws \Exception
383
+     */
384
+    private function postFunctionCall(string $functionName): void {
385
+        if ($this->isResource($this->curArgs[0])) {
386
+            $resource = $this->curArgs[0];
387
+        } elseif (
388
+            $functionName === 'ldap_search'
389
+            && is_array($this->curArgs[0])
390
+            && $this->isResource($this->curArgs[0][0])
391
+        ) {
392
+            // we use always the same LDAP connection resource, is enough to
393
+            // take the first one.
394
+            $resource = $this->curArgs[0][0];
395
+        } else {
396
+            return;
397
+        }
398
+
399
+        $errorCode = ldap_errno($resource);
400
+        if ($errorCode === 0) {
401
+            return;
402
+        }
403
+        $errorMsg = ldap_error($resource);
404
+
405
+        $this->processLDAPError($resource, $functionName, $errorCode, $errorMsg);
406
+
407
+        $this->curArgs = [];
408
+    }
409 409
 }
Please login to merge, or discard this patch.
Spacing   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -50,12 +50,12 @@  discard block
 block discarded – undo
50 50
 	public function connect($host, $port) {
51 51
 		$pos = strpos($host, '://');
52 52
 		if ($pos === false) {
53
-			$host = 'ldap://' . $host;
53
+			$host = 'ldap://'.$host;
54 54
 			$pos = 4;
55 55
 		}
56 56
 		if (strpos($host, ':', $pos + 1) === false && !empty($port)) {
57 57
 			//ldap_connect ignores port parameter when URLs are passed
58
-			$host .= ':' . $port;
58
+			$host .= ':'.$port;
59 59
 		}
60 60
 		return $this->invokeLDAPMethod('connect', $host);
61 61
 	}
@@ -182,7 +182,7 @@  discard block
 block discarded – undo
182 182
 		}
183 183
 
184 184
 		/** @psalm-suppress UndefinedVariable $oldHandler is defined when the closure is called but psalm fails to get that */
185
-		$oldHandler = set_error_handler(function ($no, $message, $file, $line) use (&$oldHandler) {
185
+		$oldHandler = set_error_handler(function($no, $message, $file, $line) use (&$oldHandler) {
186 186
 			if (str_contains($message, 'Partial search results returned: Sizelimit exceeded')) {
187 187
 				return true;
188 188
 			}
@@ -279,7 +279,7 @@  discard block
 block discarded – undo
279 279
 	 * @return mixed
280 280
 	 */
281 281
 	protected function invokeLDAPMethod(string $func, ...$arguments) {
282
-		$func = 'ldap_' . $func;
282
+		$func = 'ldap_'.$func;
283 283
 		if (function_exists($func)) {
284 284
 			$this->preFunctionCall($func, $arguments);
285 285
 			$result = call_user_func_array($func, $arguments);
@@ -298,7 +298,7 @@  discard block
 block discarded – undo
298 298
 	 * Turn resources into string, and removes potentially problematic cookie string to avoid breaking logfiles
299 299
 	 */
300 300
 	private function sanitizeFunctionParameters(array $args): array {
301
-		return array_map(function ($item) {
301
+		return array_map(function($item) {
302 302
 			if ($this->isResource($item)) {
303 303
 				return '(resource)';
304 304
 			}
@@ -336,7 +336,7 @@  discard block
 block discarded – undo
336 336
 		if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
337 337
 			file_put_contents(
338 338
 				$this->logFile,
339
-				$functionName . '::' . json_encode($this->sanitizeFunctionParameters($args)) . "\n",
339
+				$functionName.'::'.json_encode($this->sanitizeFunctionParameters($args))."\n",
340 340
 				FILE_APPEND
341 341
 			);
342 342
 		}
Please login to merge, or discard this patch.