Passed
Push — master ( e80e0d...c605ef )
by Blizzz
15:00 queued 15s
created

AbstractMapping::getNamesBySearch()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 3
dl 0
loc 17
rs 9.9332
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Aaron Wood <[email protected]>
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author blizzz <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 *
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program. If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
namespace OCA\User_LDAP\Mapping;
28
29
use Doctrine\DBAL\Exception;
30
use OC\DB\QueryBuilder\QueryBuilder;
31
use OCP\DB\IPreparedStatement;
32
use OCP\DB\QueryBuilder\IQueryBuilder;
33
34
/**
35
 * Class AbstractMapping
36
 *
37
 * @package OCA\User_LDAP\Mapping
38
 */
39
abstract class AbstractMapping {
40
	/**
41
	 * @var \OCP\IDBConnection $dbc
42
	 */
43
	protected $dbc;
44
45
	/**
46
	 * returns the DB table name which holds the mappings
47
	 *
48
	 * @return string
49
	 */
50
	abstract protected function getTableName(bool $includePrefix = true);
51
52
	/**
53
	 * @param \OCP\IDBConnection $dbc
54
	 */
55
	public function __construct(\OCP\IDBConnection $dbc) {
56
		$this->dbc = $dbc;
57
	}
58
59
	/** @var array caches Names (value) by DN (key) */
60
	protected $cache = [];
61
62
	/**
63
	 * checks whether a provided string represents an existing table col
64
	 *
65
	 * @param string $col
66
	 * @return bool
67
	 */
68
	public function isColNameValid($col) {
69
		switch ($col) {
70
			case 'ldap_dn':
71
			case 'ldap_dn_hash':
72
			case 'owncloud_name':
73
			case 'directory_uuid':
74
				return true;
75
			default:
76
				return false;
77
		}
78
	}
79
80
	/**
81
	 * Gets the value of one column based on a provided value of another column
82
	 *
83
	 * @param string $fetchCol
84
	 * @param string $compareCol
85
	 * @param string $search
86
	 * @return string|false
87
	 * @throws \Exception
88
	 */
89
	protected function getXbyY($fetchCol, $compareCol, $search) {
90
		if (!$this->isColNameValid($fetchCol)) {
91
			//this is used internally only, but we don't want to risk
92
			//having SQL injection at all.
93
			throw new \Exception('Invalid Column Name');
94
		}
95
		$query = $this->dbc->prepare('
96
			SELECT `' . $fetchCol . '`
97
			FROM `' . $this->getTableName() . '`
98
			WHERE `' . $compareCol . '` = ?
99
		');
100
101
		try {
102
			$res = $query->execute([$search]);
103
			$data = $res->fetchOne();
104
			$res->closeCursor();
105
			return $data;
106
		} catch (Exception $e) {
107
			return false;
108
		}
109
	}
110
111
	/**
112
	 * Performs a DELETE or UPDATE query to the database.
113
	 *
114
	 * @param IPreparedStatement $statement
115
	 * @param array $parameters
116
	 * @return bool true if at least one row was modified, false otherwise
117
	 */
118
	protected function modify(IPreparedStatement $statement, $parameters) {
119
		try {
120
			$result = $statement->execute($parameters);
121
			$updated = $result->rowCount() > 0;
122
			$result->closeCursor();
123
			return $updated;
124
		} catch (Exception $e) {
125
			return false;
126
		}
127
	}
128
129
	/**
130
	 * Gets the LDAP DN based on the provided name.
131
	 * Replaces Access::ocname2dn
132
	 *
133
	 * @param string $name
134
	 * @return string|false
135
	 */
136
	public function getDNByName($name) {
137
		$dn = array_search($name, $this->cache);
138
		if ($dn === false && ($dn = $this->getXbyY('ldap_dn', 'owncloud_name', $name)) !== false) {
139
			$this->cache[$dn] = $name;
140
		}
141
		return $dn;
142
	}
143
144
	/**
145
	 * Updates the DN based on the given UUID
146
	 *
147
	 * @param string $fdn
148
	 * @param string $uuid
149
	 * @return bool
150
	 */
151
	public function setDNbyUUID($fdn, $uuid) {
152
		$oldDn = $this->getDnByUUID($uuid);
153
		$statement = $this->dbc->prepare('
154
			UPDATE `' . $this->getTableName() . '`
155
			SET `ldap_dn_hash` = ?, `ldap_dn` = ?
156
			WHERE `directory_uuid` = ?
157
		');
158
159
		$r = $this->modify($statement, [$this->getDNHash($fdn), $fdn, $uuid]);
160
161
		if ($r && is_string($oldDn) && isset($this->cache[$oldDn])) {
162
			$this->cache[$fdn] = $this->cache[$oldDn];
163
			unset($this->cache[$oldDn]);
164
		}
165
166
		return $r;
167
	}
168
169
	/**
170
	 * Updates the UUID based on the given DN
171
	 *
172
	 * required by Migration/UUIDFix
173
	 *
174
	 * @param $uuid
175
	 * @param $fdn
176
	 * @return bool
177
	 */
178
	public function setUUIDbyDN($uuid, $fdn): bool {
179
		$statement = $this->dbc->prepare('
180
			UPDATE `' . $this->getTableName() . '`
181
			SET `directory_uuid` = ?
182
			WHERE `ldap_dn_hash` = ?
183
		');
184
185
		unset($this->cache[$fdn]);
186
187
		return $this->modify($statement, [$uuid, $this->getDNHash($fdn)]);
188
	}
189
190
	/**
191
	 * Get the hash to store in database column ldap_dn_hash for a given dn
192
	 */
193
	protected function getDNHash(string $fdn): string {
194
		$hash = hash('sha256', $fdn, false);
195
		if (is_string($hash)) {
0 ignored issues
show
introduced by
The condition is_string($hash) is always true.
Loading history...
196
			return $hash;
197
		} else {
198
			throw new \RuntimeException('hash function did not return a string');
199
		}
200
	}
201
202
	/**
203
	 * Gets the name based on the provided LDAP DN.
204
	 *
205
	 * @param string $fdn
206
	 * @return string|false
207
	 */
208
	public function getNameByDN($fdn) {
209
		if (!isset($this->cache[$fdn])) {
210
			$this->cache[$fdn] = $this->getXbyY('owncloud_name', 'ldap_dn_hash', $this->getDNHash($fdn));
211
		}
212
		return $this->cache[$fdn];
213
	}
214
215
	/**
216
	 * @param array<string> $hashList
217
	 */
218
	protected function prepareListOfIdsQuery(array $hashList): IQueryBuilder {
219
		$qb = $this->dbc->getQueryBuilder();
220
		$qb->select('owncloud_name', 'ldap_dn_hash', 'ldap_dn')
221
			->from($this->getTableName(false))
222
			->where($qb->expr()->in('ldap_dn_hash', $qb->createNamedParameter($hashList, QueryBuilder::PARAM_STR_ARRAY)));
223
		return $qb;
224
	}
225
226
	protected function collectResultsFromListOfIdsQuery(IQueryBuilder $qb, array &$results): void {
227
		$stmt = $qb->execute();
228
		while ($entry = $stmt->fetch(\Doctrine\DBAL\FetchMode::ASSOCIATIVE)) {
229
			$results[$entry['ldap_dn']] = $entry['owncloud_name'];
230
			$this->cache[$entry['ldap_dn']] = $entry['owncloud_name'];
231
		}
232
		$stmt->closeCursor();
233
	}
234
235
	/**
236
	 * @param array<string> $fdns
237
	 * @return array<string,string>
238
	 */
239
	public function getListOfIdsByDn(array $fdns): array {
240
		$totalDBParamLimit = 65000;
241
		$sliceSize = 1000;
242
		$maxSlices = $totalDBParamLimit / $sliceSize;
243
		$results = [];
244
245
		$slice = 1;
246
		$fdns = array_map([$this, 'getDNHash'], $fdns);
247
		$fdnsSlice = count($fdns) > $sliceSize ? array_slice($fdns, 0, $sliceSize) : $fdns;
248
		$qb = $this->prepareListOfIdsQuery($fdnsSlice);
249
250
		while (isset($fdnsSlice[999])) {
251
			// Oracle does not allow more than 1000 values in the IN list,
252
			// but allows slicing
253
			$slice++;
254
			$fdnsSlice = array_slice($fdns, $sliceSize * ($slice - 1), $sliceSize);
255
256
			/** @see https://github.com/vimeo/psalm/issues/4995 */
257
			/** @psalm-suppress TypeDoesNotContainType */
258
			if (!isset($qb)) {
259
				$qb = $this->prepareListOfIdsQuery($fdnsSlice);
260
				continue;
261
			}
262
263
			if (!empty($fdnsSlice)) {
264
				$qb->orWhere($qb->expr()->in('ldap_dn_hash', $qb->createNamedParameter($fdnsSlice, QueryBuilder::PARAM_STR_ARRAY)));
265
			}
266
267
			if ($slice % $maxSlices === 0) {
268
				$this->collectResultsFromListOfIdsQuery($qb, $results);
269
				unset($qb);
270
			}
271
		}
272
273
		if (isset($qb)) {
274
			$this->collectResultsFromListOfIdsQuery($qb, $results);
275
		}
276
277
		return $results;
278
	}
279
280
	/**
281
	 * Searches mapped names by the giving string in the name column
282
	 *
283
	 * @param string $search
284
	 * @param string $prefixMatch
285
	 * @param string $postfixMatch
286
	 * @return string[]
287
	 */
288
	public function getNamesBySearch($search, $prefixMatch = "", $postfixMatch = "") {
289
		$statement = $this->dbc->prepare('
290
			SELECT `owncloud_name`
291
			FROM `' . $this->getTableName() . '`
292
			WHERE `owncloud_name` LIKE ?
293
		');
294
295
		try {
296
			$res = $statement->execute([$prefixMatch . $this->dbc->escapeLikeParameter($search) . $postfixMatch]);
297
		} catch (Exception $e) {
298
			return [];
299
		}
300
		$names = [];
301
		while ($row = $res->fetch()) {
302
			$names[] = $row['owncloud_name'];
303
		}
304
		return $names;
305
	}
306
307
	/**
308
	 * Gets the name based on the provided LDAP UUID.
309
	 *
310
	 * @param string $uuid
311
	 * @return string|false
312
	 */
313
	public function getNameByUUID($uuid) {
314
		return $this->getXbyY('owncloud_name', 'directory_uuid', $uuid);
315
	}
316
317
	public function getDnByUUID($uuid) {
318
		return $this->getXbyY('ldap_dn', 'directory_uuid', $uuid);
319
	}
320
321
	/**
322
	 * Gets the UUID based on the provided LDAP DN
323
	 *
324
	 * @param string $dn
325
	 * @return false|string
326
	 * @throws \Exception
327
	 */
328
	public function getUUIDByDN($dn) {
329
		return $this->getXbyY('directory_uuid', 'ldap_dn_hash', $this->getDNHash($dn));
330
	}
331
332
	public function getList(int $offset = 0, int $limit = null, bool $invalidatedOnly = false): array {
333
		$select = $this->dbc->getQueryBuilder();
334
		$select->selectAlias('ldap_dn', 'dn')
335
			->selectAlias('owncloud_name', 'name')
336
			->selectAlias('directory_uuid', 'uuid')
337
			->from($this->getTableName())
338
			->setMaxResults($limit)
339
			->setFirstResult($offset);
340
341
		if ($invalidatedOnly) {
342
			$select->where($select->expr()->like('directory_uuid', $select->createNamedParameter('invalidated_%')));
343
		}
344
345
		$result = $select->executeQuery();
346
		$entries = $result->fetchAll();
347
		$result->closeCursor();
348
349
		return $entries;
350
	}
351
352
	/**
353
	 * attempts to map the given entry
354
	 *
355
	 * @param string $fdn fully distinguished name (from LDAP)
356
	 * @param string $name
357
	 * @param string $uuid a unique identifier as used in LDAP
358
	 * @return bool
359
	 */
360
	public function map($fdn, $name, $uuid) {
361
		if (mb_strlen($fdn) > 4096) {
362
			\OC::$server->getLogger()->error(
363
				'Cannot map, because the DN exceeds 4096 characters: {dn}',
364
				[
365
					'app' => 'user_ldap',
366
					'dn' => $fdn,
367
				]
368
			);
369
			return false;
370
		}
371
372
		$row = [
373
			'ldap_dn_hash' => $this->getDNHash($fdn),
374
			'ldap_dn' => $fdn,
375
			'owncloud_name' => $name,
376
			'directory_uuid' => $uuid
377
		];
378
379
		try {
380
			$result = $this->dbc->insertIfNotExist($this->getTableName(), $row);
381
			if ((bool)$result === true) {
382
				$this->cache[$fdn] = $name;
383
			}
384
			// insertIfNotExist returns values as int
385
			return (bool)$result;
386
		} catch (\Exception $e) {
387
			return false;
388
		}
389
	}
390
391
	/**
392
	 * removes a mapping based on the owncloud_name of the entry
393
	 *
394
	 * @param string $name
395
	 * @return bool
396
	 */
397
	public function unmap($name) {
398
		$statement = $this->dbc->prepare('
399
			DELETE FROM `' . $this->getTableName() . '`
400
			WHERE `owncloud_name` = ?');
401
402
		$dn = array_search($name, $this->cache);
403
		if ($dn !== false) {
404
			unset($this->cache[$dn]);
405
		}
406
407
		return $this->modify($statement, [$name]);
408
	}
409
410
	/**
411
	 * Truncates the mapping table
412
	 *
413
	 * @return bool
414
	 */
415
	public function clear() {
416
		$sql = $this->dbc
417
			->getDatabasePlatform()
418
			->getTruncateTableSQL('`' . $this->getTableName() . '`');
419
		try {
420
			$this->dbc->executeQuery($sql);
421
422
			return true;
423
		} catch (Exception $e) {
424
			return false;
425
		}
426
	}
427
428
	/**
429
	 * clears the mapping table one by one and executing a callback with
430
	 * each row's id (=owncloud_name col)
431
	 *
432
	 * @param callable $preCallback
433
	 * @param callable $postCallback
434
	 * @return bool true on success, false when at least one row was not
435
	 * deleted
436
	 */
437
	public function clearCb(callable $preCallback, callable $postCallback): bool {
438
		$picker = $this->dbc->getQueryBuilder();
439
		$picker->select('owncloud_name')
440
			->from($this->getTableName());
441
		$cursor = $picker->execute();
442
		$result = true;
443
		while ($id = $cursor->fetchOne()) {
444
			$preCallback($id);
445
			if ($isUnmapped = $this->unmap($id)) {
446
				$postCallback($id);
447
			}
448
			$result &= $isUnmapped;
449
		}
450
		$cursor->closeCursor();
451
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
452
	}
453
454
	/**
455
	 * returns the number of entries in the mappings table
456
	 *
457
	 * @return int
458
	 */
459
	public function count(): int {
460
		$query = $this->dbc->getQueryBuilder();
461
		$query->select($query->func()->count('ldap_dn_hash'))
462
			->from($this->getTableName());
463
		$res = $query->execute();
464
		$count = $res->fetchOne();
465
		$res->closeCursor();
466
		return (int)$count;
467
	}
468
469
	public function countInvalidated(): int {
470
		$query = $this->dbc->getQueryBuilder();
471
		$query->select($query->func()->count('ldap_dn_hash'))
472
			->from($this->getTableName())
473
			->where($query->expr()->like('directory_uuid', $query->createNamedParameter('invalidated_%')));
474
		$res = $query->execute();
475
		$count = $res->fetchOne();
476
		$res->closeCursor();
477
		return (int)$count;
478
	}
479
}
480