Passed
Push — master ( 3f0b7f...9de6ef )
by Julius
15:21 queued 10s
created

LDAP::invokeLDAPMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 0
dl 0
loc 12
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Alexander Bergolth <[email protected]>
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author J0WI <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Jörn Friedrich Dreyer <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Peter Kubica <[email protected]>
14
 * @author Robin McCorkell <[email protected]>
15
 * @author Roeland Jago Douma <[email protected]>
16
 * @author Roger Szabo <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program. If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OCA\User_LDAP;
35
36
use OC\ServerNotAvailableException;
37
use OCA\User_LDAP\Exceptions\ConstraintViolationException;
38
use OCA\User_LDAP\PagedResults\IAdapter;
39
use OCA\User_LDAP\PagedResults\Php73;
40
41
class LDAP implements ILDAPWrapper {
42
	protected $curFunc = '';
43
	protected $curArgs = [];
44
45
	/** @var IAdapter */
46
	protected $pagedResultsAdapter;
47
48
	public function __construct() {
49
		$this->pagedResultsAdapter = new Php73();
50
	}
51
52
	/**
53
	 * @param resource $link
54
	 * @param string $dn
55
	 * @param string $password
56
	 * @return bool|mixed
57
	 */
58
	public function bind($link, $dn, $password) {
59
		return $this->invokeLDAPMethod('bind', $link, $dn, $password);
60
	}
61
62
	/**
63
	 * @param string $host
64
	 * @param string $port
65
	 * @return mixed
66
	 */
67
	public function connect($host, $port) {
68
		if (strpos($host, '://') === false) {
69
			$host = 'ldap://' . $host;
70
		}
71
		if (strpos($host, ':', strpos($host, '://') + 1) === false) {
72
			//ldap_connect ignores port parameter when URLs are passed
73
			$host .= ':' . $port;
74
		}
75
		return $this->invokeLDAPMethod('connect', $host);
76
	}
77
78
	public function controlPagedResultResponse($link, $result, &$cookie): bool {
79
		$this->preFunctionCall(
80
			$this->pagedResultsAdapter->getResponseCallFunc(),
81
			$this->pagedResultsAdapter->getResponseCallArgs([$link, $result, &$cookie])
82
		);
83
84
		$result = $this->pagedResultsAdapter->responseCall($link);
85
		$cookie = $this->pagedResultsAdapter->getCookie($link);
86
87
		if ($this->isResultFalse($result)) {
88
			$this->postFunctionCall();
89
		}
90
91
		return $result;
92
	}
93
94
	/**
95
	 * @param LDAP $link
96
	 * @param int $pageSize
97
	 * @param bool $isCritical
98
	 * @return mixed|true
99
	 */
100
	public function controlPagedResult($link, $pageSize, $isCritical) {
101
		$fn = $this->pagedResultsAdapter->getRequestCallFunc();
102
		$this->pagedResultsAdapter->setRequestParameters($link, $pageSize, $isCritical);
103
		if ($fn === null) {
104
			return true;
105
		}
106
107
		$this->preFunctionCall($fn, $this->pagedResultsAdapter->getRequestCallArgs($link));
108
		$result = $this->pagedResultsAdapter->requestCall($link);
109
110
		if ($this->isResultFalse($result)) {
111
			$this->postFunctionCall();
112
		}
113
114
		return $result;
115
	}
116
117
	/**
118
	 * @param LDAP $link
119
	 * @param LDAP $result
120
	 * @return mixed
121
	 */
122
	public function countEntries($link, $result) {
123
		return $this->invokeLDAPMethod('count_entries', $link, $result);
124
	}
125
126
	/**
127
	 * @param LDAP $link
128
	 * @return integer
129
	 */
130
	public function errno($link) {
131
		return $this->invokeLDAPMethod('errno', $link);
132
	}
133
134
	/**
135
	 * @param LDAP $link
136
	 * @return string
137
	 */
138
	public function error($link) {
139
		return $this->invokeLDAPMethod('error', $link);
140
	}
141
142
	/**
143
	 * Splits DN into its component parts
144
	 * @param string $dn
145
	 * @param int @withAttrib
0 ignored issues
show
Documentation Bug introduced by
The doc comment @withAttrib at position 0 could not be parsed: Unknown type name '@withAttrib' at position 0 in @withAttrib.
Loading history...
146
	 * @return array|false
147
	 * @link https://www.php.net/manual/en/function.ldap-explode-dn.php
148
	 */
149
	public function explodeDN($dn, $withAttrib) {
150
		return $this->invokeLDAPMethod('explode_dn', $dn, $withAttrib);
151
	}
152
153
	/**
154
	 * @param LDAP $link
155
	 * @param LDAP $result
156
	 * @return mixed
157
	 */
158
	public function firstEntry($link, $result) {
159
		return $this->invokeLDAPMethod('first_entry', $link, $result);
160
	}
161
162
	/**
163
	 * @param LDAP $link
164
	 * @param LDAP $result
165
	 * @return array|mixed
166
	 */
167
	public function getAttributes($link, $result) {
168
		return $this->invokeLDAPMethod('get_attributes', $link, $result);
169
	}
170
171
	/**
172
	 * @param LDAP $link
173
	 * @param LDAP $result
174
	 * @return mixed|string
175
	 */
176
	public function getDN($link, $result) {
177
		return $this->invokeLDAPMethod('get_dn', $link, $result);
178
	}
179
180
	/**
181
	 * @param LDAP $link
182
	 * @param LDAP $result
183
	 * @return array|mixed
184
	 */
185
	public function getEntries($link, $result) {
186
		return $this->invokeLDAPMethod('get_entries', $link, $result);
187
	}
188
189
	/**
190
	 * @param LDAP $link
191
	 * @param resource $result
192
	 * @return mixed
193
	 */
194
	public function nextEntry($link, $result) {
195
		return $this->invokeLDAPMethod('next_entry', $link, $result);
196
	}
197
198
	/**
199
	 * @param LDAP $link
200
	 * @param string $baseDN
201
	 * @param string $filter
202
	 * @param array $attr
203
	 * @return mixed
204
	 */
205
	public function read($link, $baseDN, $filter, $attr) {
206
		$this->pagedResultsAdapter->setReadArgs($link, $baseDN, $filter, $attr);
207
		return $this->invokeLDAPMethod('read', ...$this->pagedResultsAdapter->getReadArgs($link));
208
	}
209
210
	/**
211
	 * @param LDAP $link
212
	 * @param string[] $baseDN
213
	 * @param string $filter
214
	 * @param array $attr
215
	 * @param int $attrsOnly
216
	 * @param int $limit
217
	 * @return mixed
218
	 * @throws \Exception
219
	 */
220
	public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = 0) {
221
		$oldHandler = set_error_handler(function ($no, $message, $file, $line) use (&$oldHandler) {
0 ignored issues
show
Unused Code introduced by
The assignment to $oldHandler is dead and can be removed.
Loading history...
222
			if (strpos($message, 'Partial search results returned: Sizelimit exceeded') !== false) {
223
				return true;
224
			}
225
			$oldHandler($no, $message, $file, $line);
226
			return true;
227
		});
228
		try {
229
			$this->pagedResultsAdapter->setSearchArgs($link, $baseDN, $filter, $attr, $attrsOnly, $limit);
0 ignored issues
show
Bug introduced by
$baseDN of type string[] is incompatible with the type string expected by parameter $baseDN of OCA\User_LDAP\PagedResul...dapter::setSearchArgs(). ( Ignorable by Annotation )

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

229
			$this->pagedResultsAdapter->setSearchArgs($link, /** @scrutinizer ignore-type */ $baseDN, $filter, $attr, $attrsOnly, $limit);
Loading history...
230
			$result = $this->invokeLDAPMethod('search', ...$this->pagedResultsAdapter->getSearchArgs($link));
231
232
			restore_error_handler();
233
			return $result;
234
		} catch (\Exception $e) {
235
			restore_error_handler();
236
			throw $e;
237
		}
238
	}
239
240
	/**
241
	 * @param LDAP $link
242
	 * @param string $userDN
243
	 * @param string $password
244
	 * @return bool
245
	 */
246
	public function modReplace($link, $userDN, $password) {
247
		return $this->invokeLDAPMethod('mod_replace', $link, $userDN, ['userPassword' => $password]);
248
	}
249
250
	/**
251
	 * @param LDAP $link
252
	 * @param string $userDN
253
	 * @param string $oldPassword
254
	 * @param string $password
255
	 * @return bool
256
	 */
257
	public function exopPasswd($link, $userDN, $oldPassword, $password) {
258
		return $this->invokeLDAPMethod('exop_passwd', $link, $userDN, $oldPassword, $password);
259
	}
260
261
	/**
262
	 * @param LDAP $link
263
	 * @param string $option
264
	 * @param int $value
265
	 * @return bool|mixed
266
	 */
267
	public function setOption($link, $option, $value) {
268
		return $this->invokeLDAPMethod('set_option', $link, $option, $value);
269
	}
270
271
	/**
272
	 * @param LDAP $link
273
	 * @return mixed|true
274
	 */
275
	public function startTls($link) {
276
		return $this->invokeLDAPMethod('start_tls', $link);
277
	}
278
279
	/**
280
	 * @param resource $link
281
	 * @return bool|mixed
282
	 */
283
	public function unbind($link) {
284
		return $this->invokeLDAPMethod('unbind', $link);
285
	}
286
287
	/**
288
	 * Checks whether the server supports LDAP
289
	 * @return boolean if it the case, false otherwise
290
	 * */
291
	public function areLDAPFunctionsAvailable() {
292
		return function_exists('ldap_connect');
293
	}
294
295
	/**
296
	 * Checks whether the submitted parameter is a resource
297
	 * @param Resource $resource the resource variable to check
298
	 * @return bool true if it is a resource, false otherwise
299
	 */
300
	public function isResource($resource) {
301
		return is_resource($resource);
302
	}
303
304
	/**
305
	 * Checks whether the return value from LDAP is wrong or not.
306
	 *
307
	 * When using ldap_search we provide an array, in case multiple bases are
308
	 * configured. Thus, we need to check the array elements.
309
	 *
310
	 * @param $result
311
	 * @return bool
312
	 */
313
	protected function isResultFalse($result) {
314
		if ($result === false) {
315
			return true;
316
		}
317
318
		if ($this->curFunc === 'ldap_search' && is_array($result)) {
319
			foreach ($result as $singleResult) {
320
				if ($singleResult === false) {
321
					return true;
322
				}
323
			}
324
		}
325
326
		return false;
327
	}
328
329
	/**
330
	 * @return mixed
331
	 */
332
	protected function invokeLDAPMethod() {
333
		$arguments = func_get_args();
334
		$func = 'ldap_' . array_shift($arguments);
335
		if (function_exists($func)) {
336
			$this->preFunctionCall($func, $arguments);
337
			$result = call_user_func_array($func, $arguments);
338
			if ($this->isResultFalse($result)) {
339
				$this->postFunctionCall();
340
			}
341
			return $result;
342
		}
343
		return null;
344
	}
345
346
	/**
347
	 * @param string $functionName
348
	 * @param array $args
349
	 */
350
	private function preFunctionCall($functionName, $args) {
351
		$this->curFunc = $functionName;
352
		$this->curArgs = $args;
353
	}
354
355
	/**
356
	 * Analyzes the returned LDAP error and acts accordingly if not 0
357
	 *
358
	 * @param resource $resource the LDAP Connection resource
359
	 * @throws ConstraintViolationException
360
	 * @throws ServerNotAvailableException
361
	 * @throws \Exception
362
	 */
363
	private function processLDAPError($resource) {
364
		$errorCode = ldap_errno($resource);
365
		if ($errorCode === 0) {
366
			return;
367
		}
368
		$errorMsg = ldap_error($resource);
369
370
		if ($this->curFunc === 'ldap_get_entries'
371
			&& $errorCode === -4) {
372
		} elseif ($errorCode === 32) {
373
			//for now
374
		} elseif ($errorCode === 10) {
375
			//referrals, we switch them off, but then there is AD :)
376
		} elseif ($errorCode === -1) {
377
			throw new ServerNotAvailableException('Lost connection to LDAP server.');
378
		} elseif ($errorCode === 52) {
379
			throw new ServerNotAvailableException('LDAP server is shutting down.');
380
		} elseif ($errorCode === 48) {
381
			throw new \Exception('LDAP authentication method rejected', $errorCode);
382
		} elseif ($errorCode === 1) {
383
			throw new \Exception('LDAP Operations error', $errorCode);
384
		} elseif ($errorCode === 19) {
385
			ldap_get_option($this->curArgs[0], LDAP_OPT_ERROR_STRING, $extended_error);
386
			throw new ConstraintViolationException(!empty($extended_error)?$extended_error:$errorMsg, $errorCode);
387
		} else {
388
			\OC::$server->getLogger()->debug('LDAP error {message} ({code}) after calling {func}', [
389
				'app' => 'user_ldap',
390
				'message' => $errorMsg,
391
				'code' => $errorCode,
392
				'func' => $this->curFunc,
393
			]);
394
		}
395
	}
396
397
	/**
398
	 * Called after an ldap method is run to act on LDAP error if necessary
399
	 * @throw \Exception
400
	 */
401
	private function postFunctionCall() {
402
		if ($this->isResource($this->curArgs[0])) {
403
			$resource = $this->curArgs[0];
404
		} elseif (
405
			   $this->curFunc === 'ldap_search'
406
			&& is_array($this->curArgs[0])
407
			&& $this->isResource($this->curArgs[0][0])
408
		) {
409
			// we use always the same LDAP connection resource, is enough to
410
			// take the first one.
411
			$resource = $this->curArgs[0][0];
412
		} else {
413
			return;
414
		}
415
416
		$this->processLDAPError($resource);
417
418
		$this->curFunc = '';
419
		$this->curArgs = [];
420
	}
421
}
422