Completed
Push — master ( ff1b34...d1fb93 )
by Joas
16:03
created
lib/private/Files/Storage/Wrapper/Encryption.php 1 patch
Indentation   +975 added lines, -975 removed lines patch added patch discarded remove patch
@@ -48,980 +48,980 @@
 block discarded – undo
48 48
 
49 49
 class Encryption extends Wrapper {
50 50
 
51
-	use LocalTempFileTrait;
52
-
53
-	/** @var string */
54
-	private $mountPoint;
55
-
56
-	/** @var \OC\Encryption\Util */
57
-	private $util;
58
-
59
-	/** @var \OCP\Encryption\IManager */
60
-	private $encryptionManager;
61
-
62
-	/** @var \OCP\ILogger */
63
-	private $logger;
64
-
65
-	/** @var string */
66
-	private $uid;
67
-
68
-	/** @var array */
69
-	protected $unencryptedSize;
70
-
71
-	/** @var \OCP\Encryption\IFile */
72
-	private $fileHelper;
73
-
74
-	/** @var IMountPoint */
75
-	private $mount;
76
-
77
-	/** @var IStorage */
78
-	private $keyStorage;
79
-
80
-	/** @var Update */
81
-	private $update;
82
-
83
-	/** @var Manager */
84
-	private $mountManager;
85
-
86
-	/** @var array remember for which path we execute the repair step to avoid recursions */
87
-	private $fixUnencryptedSizeOf = array();
88
-
89
-	/** @var  ArrayCache */
90
-	private $arrayCache;
91
-
92
-	/**
93
-	 * @param array $parameters
94
-	 * @param IManager $encryptionManager
95
-	 * @param Util $util
96
-	 * @param ILogger $logger
97
-	 * @param IFile $fileHelper
98
-	 * @param string $uid
99
-	 * @param IStorage $keyStorage
100
-	 * @param Update $update
101
-	 * @param Manager $mountManager
102
-	 * @param ArrayCache $arrayCache
103
-	 */
104
-	public function __construct(
105
-			$parameters,
106
-			IManager $encryptionManager = null,
107
-			Util $util = null,
108
-			ILogger $logger = null,
109
-			IFile $fileHelper = null,
110
-			$uid = null,
111
-			IStorage $keyStorage = null,
112
-			Update $update = null,
113
-			Manager $mountManager = null,
114
-			ArrayCache $arrayCache = null
115
-		) {
116
-
117
-		$this->mountPoint = $parameters['mountPoint'];
118
-		$this->mount = $parameters['mount'];
119
-		$this->encryptionManager = $encryptionManager;
120
-		$this->util = $util;
121
-		$this->logger = $logger;
122
-		$this->uid = $uid;
123
-		$this->fileHelper = $fileHelper;
124
-		$this->keyStorage = $keyStorage;
125
-		$this->unencryptedSize = array();
126
-		$this->update = $update;
127
-		$this->mountManager = $mountManager;
128
-		$this->arrayCache = $arrayCache;
129
-		parent::__construct($parameters);
130
-	}
131
-
132
-	/**
133
-	 * see http://php.net/manual/en/function.filesize.php
134
-	 * The result for filesize when called on a folder is required to be 0
135
-	 *
136
-	 * @param string $path
137
-	 * @return int
138
-	 */
139
-	public function filesize($path) {
140
-		$fullPath = $this->getFullPath($path);
141
-
142
-		/** @var CacheEntry $info */
143
-		$info = $this->getCache()->get($path);
144
-		if (isset($this->unencryptedSize[$fullPath])) {
145
-			$size = $this->unencryptedSize[$fullPath];
146
-			// update file cache
147
-			if ($info instanceof ICacheEntry) {
148
-				$info = $info->getData();
149
-				$info['encrypted'] = $info['encryptedVersion'];
150
-			} else {
151
-				if (!is_array($info)) {
152
-					$info = [];
153
-				}
154
-				$info['encrypted'] = true;
155
-			}
156
-
157
-			$info['size'] = $size;
158
-			$this->getCache()->put($path, $info);
159
-
160
-			return $size;
161
-		}
162
-
163
-		if (isset($info['fileid']) && $info['encrypted']) {
164
-			return $this->verifyUnencryptedSize($path, $info['size']);
165
-		}
166
-
167
-		return $this->storage->filesize($path);
168
-	}
169
-
170
-	/**
171
-	 * @param string $path
172
-	 * @return array
173
-	 */
174
-	public function getMetaData($path) {
175
-		$data = $this->storage->getMetaData($path);
176
-		if (is_null($data)) {
177
-			return null;
178
-		}
179
-		$fullPath = $this->getFullPath($path);
180
-		$info = $this->getCache()->get($path);
181
-
182
-		if (isset($this->unencryptedSize[$fullPath])) {
183
-			$data['encrypted'] = true;
184
-			$data['size'] = $this->unencryptedSize[$fullPath];
185
-		} else {
186
-			if (isset($info['fileid']) && $info['encrypted']) {
187
-				$data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
188
-				$data['encrypted'] = true;
189
-			}
190
-		}
191
-
192
-		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
193
-			$data['encryptedVersion'] = $info['encryptedVersion'];
194
-		}
195
-
196
-		return $data;
197
-	}
198
-
199
-	/**
200
-	 * see http://php.net/manual/en/function.file_get_contents.php
201
-	 *
202
-	 * @param string $path
203
-	 * @return string
204
-	 */
205
-	public function file_get_contents($path) {
206
-
207
-		$encryptionModule = $this->getEncryptionModule($path);
208
-
209
-		if ($encryptionModule) {
210
-			$handle = $this->fopen($path, "r");
211
-			if (!$handle) {
212
-				return false;
213
-			}
214
-			$data = stream_get_contents($handle);
215
-			fclose($handle);
216
-			return $data;
217
-		}
218
-		return $this->storage->file_get_contents($path);
219
-	}
220
-
221
-	/**
222
-	 * see http://php.net/manual/en/function.file_put_contents.php
223
-	 *
224
-	 * @param string $path
225
-	 * @param string $data
226
-	 * @return bool
227
-	 */
228
-	public function file_put_contents($path, $data) {
229
-		// file put content will always be translated to a stream write
230
-		$handle = $this->fopen($path, 'w');
231
-		if (is_resource($handle)) {
232
-			$written = fwrite($handle, $data);
233
-			fclose($handle);
234
-			return $written;
235
-		}
236
-
237
-		return false;
238
-	}
239
-
240
-	/**
241
-	 * see http://php.net/manual/en/function.unlink.php
242
-	 *
243
-	 * @param string $path
244
-	 * @return bool
245
-	 */
246
-	public function unlink($path) {
247
-		$fullPath = $this->getFullPath($path);
248
-		if ($this->util->isExcluded($fullPath)) {
249
-			return $this->storage->unlink($path);
250
-		}
251
-
252
-		$encryptionModule = $this->getEncryptionModule($path);
253
-		if ($encryptionModule) {
254
-			$this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
255
-		}
256
-
257
-		return $this->storage->unlink($path);
258
-	}
259
-
260
-	/**
261
-	 * see http://php.net/manual/en/function.rename.php
262
-	 *
263
-	 * @param string $path1
264
-	 * @param string $path2
265
-	 * @return bool
266
-	 */
267
-	public function rename($path1, $path2) {
268
-
269
-		$result = $this->storage->rename($path1, $path2);
270
-
271
-		if ($result &&
272
-			// versions always use the keys from the original file, so we can skip
273
-			// this step for versions
274
-			$this->isVersion($path2) === false &&
275
-			$this->encryptionManager->isEnabled()) {
276
-			$source = $this->getFullPath($path1);
277
-			if (!$this->util->isExcluded($source)) {
278
-				$target = $this->getFullPath($path2);
279
-				if (isset($this->unencryptedSize[$source])) {
280
-					$this->unencryptedSize[$target] = $this->unencryptedSize[$source];
281
-				}
282
-				$this->keyStorage->renameKeys($source, $target);
283
-				$module = $this->getEncryptionModule($path2);
284
-				if ($module) {
285
-					$module->update($target, $this->uid, []);
286
-				}
287
-			}
288
-		}
289
-
290
-		return $result;
291
-	}
292
-
293
-	/**
294
-	 * see http://php.net/manual/en/function.rmdir.php
295
-	 *
296
-	 * @param string $path
297
-	 * @return bool
298
-	 */
299
-	public function rmdir($path) {
300
-		$result = $this->storage->rmdir($path);
301
-		$fullPath = $this->getFullPath($path);
302
-		if ($result &&
303
-			$this->util->isExcluded($fullPath) === false &&
304
-			$this->encryptionManager->isEnabled()
305
-		) {
306
-			$this->keyStorage->deleteAllFileKeys($fullPath);
307
-		}
308
-
309
-		return $result;
310
-	}
311
-
312
-	/**
313
-	 * check if a file can be read
314
-	 *
315
-	 * @param string $path
316
-	 * @return bool
317
-	 */
318
-	public function isReadable($path) {
319
-
320
-		$isReadable = true;
321
-
322
-		$metaData = $this->getMetaData($path);
323
-		if (
324
-			!$this->is_dir($path) &&
325
-			isset($metaData['encrypted']) &&
326
-			$metaData['encrypted'] === true
327
-		) {
328
-			$fullPath = $this->getFullPath($path);
329
-			$module = $this->getEncryptionModule($path);
330
-			$isReadable = $module->isReadable($fullPath, $this->uid);
331
-		}
332
-
333
-		return $this->storage->isReadable($path) && $isReadable;
334
-	}
335
-
336
-	/**
337
-	 * see http://php.net/manual/en/function.copy.php
338
-	 *
339
-	 * @param string $path1
340
-	 * @param string $path2
341
-	 * @return bool
342
-	 */
343
-	public function copy($path1, $path2) {
344
-
345
-		$source = $this->getFullPath($path1);
346
-
347
-		if ($this->util->isExcluded($source)) {
348
-			return $this->storage->copy($path1, $path2);
349
-		}
350
-
351
-		// need to stream copy file by file in case we copy between a encrypted
352
-		// and a unencrypted storage
353
-		$this->unlink($path2);
354
-		return $this->copyFromStorage($this, $path1, $path2);
355
-	}
356
-
357
-	/**
358
-	 * see http://php.net/manual/en/function.fopen.php
359
-	 *
360
-	 * @param string $path
361
-	 * @param string $mode
362
-	 * @return resource|bool
363
-	 * @throws GenericEncryptionException
364
-	 * @throws ModuleDoesNotExistsException
365
-	 */
366
-	public function fopen($path, $mode) {
367
-
368
-		// check if the file is stored in the array cache, this means that we
369
-		// copy a file over to the versions folder, in this case we don't want to
370
-		// decrypt it
371
-		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
372
-			$this->arrayCache->remove('encryption_copy_version_' . $path);
373
-			return $this->storage->fopen($path, $mode);
374
-		}
375
-
376
-		$encryptionEnabled = $this->encryptionManager->isEnabled();
377
-		$shouldEncrypt = false;
378
-		$encryptionModule = null;
379
-		$header = $this->getHeader($path);
380
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
381
-		$fullPath = $this->getFullPath($path);
382
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
383
-
384
-		if ($this->util->isExcluded($fullPath) === false) {
385
-
386
-			$size = $unencryptedSize = 0;
387
-			$realFile = $this->util->stripPartialFileExtension($path);
388
-			$targetExists = $this->file_exists($realFile) || $this->file_exists($path);
389
-			$targetIsEncrypted = false;
390
-			if ($targetExists) {
391
-				// in case the file exists we require the explicit module as
392
-				// specified in the file header - otherwise we need to fail hard to
393
-				// prevent data loss on client side
394
-				if (!empty($encryptionModuleId)) {
395
-					$targetIsEncrypted = true;
396
-					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
397
-				}
398
-
399
-				if ($this->file_exists($path)) {
400
-					$size = $this->storage->filesize($path);
401
-					$unencryptedSize = $this->filesize($path);
402
-				} else {
403
-					$size = $unencryptedSize = 0;
404
-				}
405
-			}
406
-
407
-			try {
408
-
409
-				if (
410
-					$mode === 'w'
411
-					|| $mode === 'w+'
412
-					|| $mode === 'wb'
413
-					|| $mode === 'wb+'
414
-				) {
415
-					// don't overwrite encrypted files if encryption is not enabled
416
-					if ($targetIsEncrypted && $encryptionEnabled === false) {
417
-						throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled');
418
-					}
419
-					if ($encryptionEnabled) {
420
-						// if $encryptionModuleId is empty, the default module will be used
421
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
422
-						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
423
-						$signed = true;
424
-					}
425
-				} else {
426
-					$info = $this->getCache()->get($path);
427
-					// only get encryption module if we found one in the header
428
-					// or if file should be encrypted according to the file cache
429
-					if (!empty($encryptionModuleId)) {
430
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
431
-						$shouldEncrypt = true;
432
-					} else if (empty($encryptionModuleId) && $info['encrypted'] === true) {
433
-						// we come from a old installation. No header and/or no module defined
434
-						// but the file is encrypted. In this case we need to use the
435
-						// OC_DEFAULT_MODULE to read the file
436
-						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
437
-						$shouldEncrypt = true;
438
-						$targetIsEncrypted = true;
439
-					}
440
-				}
441
-			} catch (ModuleDoesNotExistsException $e) {
442
-				$this->logger->logException($e, [
443
-					'message' => 'Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted',
444
-					'level' => \OCP\Util::WARN,
445
-					'app' => 'core',
446
-				]);
447
-			}
448
-
449
-			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
450
-			if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
451
-				if (!$targetExists || !$targetIsEncrypted) {
452
-					$shouldEncrypt = false;
453
-				}
454
-			}
455
-
456
-			if ($shouldEncrypt === true && $encryptionModule !== null) {
457
-				$headerSize = $this->getHeaderSize($path);
458
-				$source = $this->storage->fopen($path, $mode);
459
-				if (!is_resource($source)) {
460
-					return false;
461
-				}
462
-				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
463
-					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
464
-					$size, $unencryptedSize, $headerSize, $signed);
465
-				return $handle;
466
-			}
467
-
468
-		}
469
-
470
-		return $this->storage->fopen($path, $mode);
471
-	}
472
-
473
-
474
-	/**
475
-	 * perform some plausibility checks if the the unencrypted size is correct.
476
-	 * If not, we calculate the correct unencrypted size and return it
477
-	 *
478
-	 * @param string $path internal path relative to the storage root
479
-	 * @param int $unencryptedSize size of the unencrypted file
480
-	 *
481
-	 * @return int unencrypted size
482
-	 */
483
-	protected function verifyUnencryptedSize($path, $unencryptedSize) {
484
-
485
-		$size = $this->storage->filesize($path);
486
-		$result = $unencryptedSize;
487
-
488
-		if ($unencryptedSize < 0 ||
489
-			($size > 0 && $unencryptedSize === $size)
490
-		) {
491
-			// check if we already calculate the unencrypted size for the
492
-			// given path to avoid recursions
493
-			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
494
-				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
495
-				try {
496
-					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
497
-				} catch (\Exception $e) {
498
-					$this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
499
-					$this->logger->logException($e);
500
-				}
501
-				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
502
-			}
503
-		}
504
-
505
-		return $result;
506
-	}
507
-
508
-	/**
509
-	 * calculate the unencrypted size
510
-	 *
511
-	 * @param string $path internal path relative to the storage root
512
-	 * @param int $size size of the physical file
513
-	 * @param int $unencryptedSize size of the unencrypted file
514
-	 *
515
-	 * @return int calculated unencrypted size
516
-	 */
517
-	protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
518
-
519
-		$headerSize = $this->getHeaderSize($path);
520
-		$header = $this->getHeader($path);
521
-		$encryptionModule = $this->getEncryptionModule($path);
522
-
523
-		$stream = $this->storage->fopen($path, 'r');
524
-
525
-		// if we couldn't open the file we return the old unencrypted size
526
-		if (!is_resource($stream)) {
527
-			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
528
-			return $unencryptedSize;
529
-		}
530
-
531
-		$newUnencryptedSize = 0;
532
-		$size -= $headerSize;
533
-		$blockSize = $this->util->getBlockSize();
534
-
535
-		// if a header exists we skip it
536
-		if ($headerSize > 0) {
537
-			fread($stream, $headerSize);
538
-		}
539
-
540
-		// fast path, else the calculation for $lastChunkNr is bogus
541
-		if ($size === 0) {
542
-			return 0;
543
-		}
544
-
545
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
546
-		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
547
-
548
-		// calculate last chunk nr
549
-		// next highest is end of chunks, one subtracted is last one
550
-		// we have to read the last chunk, we can't just calculate it (because of padding etc)
551
-
552
-		$lastChunkNr = ceil($size/ $blockSize)-1;
553
-		// calculate last chunk position
554
-		$lastChunkPos = ($lastChunkNr * $blockSize);
555
-		// try to fseek to the last chunk, if it fails we have to read the whole file
556
-		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
557
-			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
558
-		}
559
-
560
-		$lastChunkContentEncrypted='';
561
-		$count = $blockSize;
562
-
563
-		while ($count > 0) {
564
-			$data=fread($stream, $blockSize);
565
-			$count=strlen($data);
566
-			$lastChunkContentEncrypted .= $data;
567
-			if(strlen($lastChunkContentEncrypted) > $blockSize) {
568
-				$newUnencryptedSize += $unencryptedBlockSize;
569
-				$lastChunkContentEncrypted=substr($lastChunkContentEncrypted, $blockSize);
570
-			}
571
-		}
572
-
573
-		fclose($stream);
574
-
575
-		// we have to decrypt the last chunk to get it actual size
576
-		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
577
-		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
578
-		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
579
-
580
-		// calc the real file size with the size of the last chunk
581
-		$newUnencryptedSize += strlen($decryptedLastChunk);
582
-
583
-		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
584
-
585
-		// write to cache if applicable
586
-		$cache = $this->storage->getCache();
587
-		if ($cache) {
588
-			$entry = $cache->get($path);
589
-			$cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
590
-		}
591
-
592
-		return $newUnencryptedSize;
593
-	}
594
-
595
-	/**
596
-	 * @param Storage\IStorage $sourceStorage
597
-	 * @param string $sourceInternalPath
598
-	 * @param string $targetInternalPath
599
-	 * @param bool $preserveMtime
600
-	 * @return bool
601
-	 */
602
-	public function moveFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
603
-		if ($sourceStorage === $this) {
604
-			return $this->rename($sourceInternalPath, $targetInternalPath);
605
-		}
606
-
607
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
608
-		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
609
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
610
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
611
-		// - remove $this->copyBetweenStorage
612
-
613
-		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
614
-			return false;
615
-		}
616
-
617
-		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
618
-		if ($result) {
619
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
620
-				$result &= $sourceStorage->rmdir($sourceInternalPath);
621
-			} else {
622
-				$result &= $sourceStorage->unlink($sourceInternalPath);
623
-			}
624
-		}
625
-		return $result;
626
-	}
627
-
628
-
629
-	/**
630
-	 * @param Storage\IStorage $sourceStorage
631
-	 * @param string $sourceInternalPath
632
-	 * @param string $targetInternalPath
633
-	 * @param bool $preserveMtime
634
-	 * @param bool $isRename
635
-	 * @return bool
636
-	 */
637
-	public function copyFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) {
638
-
639
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
640
-		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
641
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
642
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
643
-		// - remove $this->copyBetweenStorage
644
-
645
-		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
646
-	}
647
-
648
-	/**
649
-	 * Update the encrypted cache version in the database
650
-	 *
651
-	 * @param Storage\IStorage $sourceStorage
652
-	 * @param string $sourceInternalPath
653
-	 * @param string $targetInternalPath
654
-	 * @param bool $isRename
655
-	 */
656
-	private function updateEncryptedVersion(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename) {
657
-		$isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath) ? 1 : 0;
658
-		$cacheInformation = [
659
-			'encrypted' => (bool)$isEncrypted,
660
-		];
661
-		if($isEncrypted === 1) {
662
-			$encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
663
-
664
-			// In case of a move operation from an unencrypted to an encrypted
665
-			// storage the old encrypted version would stay with "0" while the
666
-			// correct value would be "1". Thus we manually set the value to "1"
667
-			// for those cases.
668
-			// See also https://github.com/owncloud/core/issues/23078
669
-			if($encryptedVersion === 0) {
670
-				$encryptedVersion = 1;
671
-			}
672
-
673
-			$cacheInformation['encryptedVersion'] = $encryptedVersion;
674
-		}
675
-
676
-		// in case of a rename we need to manipulate the source cache because
677
-		// this information will be kept for the new target
678
-		if ($isRename) {
679
-			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
680
-		} else {
681
-			$this->getCache()->put($targetInternalPath, $cacheInformation);
682
-		}
683
-	}
684
-
685
-	/**
686
-	 * copy file between two storages
687
-	 *
688
-	 * @param Storage\IStorage $sourceStorage
689
-	 * @param string $sourceInternalPath
690
-	 * @param string $targetInternalPath
691
-	 * @param bool $preserveMtime
692
-	 * @param bool $isRename
693
-	 * @return bool
694
-	 * @throws \Exception
695
-	 */
696
-	private function copyBetweenStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
697
-
698
-		// for versions we have nothing to do, because versions should always use the
699
-		// key from the original file. Just create a 1:1 copy and done
700
-		if ($this->isVersion($targetInternalPath) ||
701
-			$this->isVersion($sourceInternalPath)) {
702
-			// remember that we try to create a version so that we can detect it during
703
-			// fopen($sourceInternalPath) and by-pass the encryption in order to
704
-			// create a 1:1 copy of the file
705
-			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
706
-			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
707
-			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
708
-			if ($result) {
709
-				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
710
-				// make sure that we update the unencrypted size for the version
711
-				if (isset($info['encrypted']) && $info['encrypted'] === true) {
712
-					$this->updateUnencryptedSize(
713
-						$this->getFullPath($targetInternalPath),
714
-						$info['size']
715
-					);
716
-				}
717
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
718
-			}
719
-			return $result;
720
-		}
721
-
722
-		// first copy the keys that we reuse the existing file key on the target location
723
-		// and don't create a new one which would break versions for example.
724
-		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
725
-		if (count($mount) === 1) {
726
-			$mountPoint = $mount[0]->getMountPoint();
727
-			$source = $mountPoint . '/' . $sourceInternalPath;
728
-			$target = $this->getFullPath($targetInternalPath);
729
-			$this->copyKeys($source, $target);
730
-		} else {
731
-			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
732
-		}
733
-
734
-		if ($sourceStorage->is_dir($sourceInternalPath)) {
735
-			$dh = $sourceStorage->opendir($sourceInternalPath);
736
-			$result = $this->mkdir($targetInternalPath);
737
-			if (is_resource($dh)) {
738
-				while ($result and ($file = readdir($dh)) !== false) {
739
-					if (!Filesystem::isIgnoredDir($file)) {
740
-						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
741
-					}
742
-				}
743
-			}
744
-		} else {
745
-			try {
746
-				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
747
-				$target = $this->fopen($targetInternalPath, 'w');
748
-				list(, $result) = \OC_Helper::streamCopy($source, $target);
749
-				fclose($source);
750
-				fclose($target);
751
-			} catch (\Exception $e) {
752
-				fclose($source);
753
-				fclose($target);
754
-				throw $e;
755
-			}
756
-			if($result) {
757
-				if ($preserveMtime) {
758
-					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
759
-				}
760
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
761
-			} else {
762
-				// delete partially written target file
763
-				$this->unlink($targetInternalPath);
764
-				// delete cache entry that was created by fopen
765
-				$this->getCache()->remove($targetInternalPath);
766
-			}
767
-		}
768
-		return (bool)$result;
769
-
770
-	}
771
-
772
-	/**
773
-	 * get the path to a local version of the file.
774
-	 * The local version of the file can be temporary and doesn't have to be persistent across requests
775
-	 *
776
-	 * @param string $path
777
-	 * @return string
778
-	 */
779
-	public function getLocalFile($path) {
780
-		if ($this->encryptionManager->isEnabled()) {
781
-			$cachedFile = $this->getCachedFile($path);
782
-			if (is_string($cachedFile)) {
783
-				return $cachedFile;
784
-			}
785
-		}
786
-		return $this->storage->getLocalFile($path);
787
-	}
788
-
789
-	/**
790
-	 * Returns the wrapped storage's value for isLocal()
791
-	 *
792
-	 * @return bool wrapped storage's isLocal() value
793
-	 */
794
-	public function isLocal() {
795
-		if ($this->encryptionManager->isEnabled()) {
796
-			return false;
797
-		}
798
-		return $this->storage->isLocal();
799
-	}
800
-
801
-	/**
802
-	 * see http://php.net/manual/en/function.stat.php
803
-	 * only the following keys are required in the result: size and mtime
804
-	 *
805
-	 * @param string $path
806
-	 * @return array
807
-	 */
808
-	public function stat($path) {
809
-		$stat = $this->storage->stat($path);
810
-		$fileSize = $this->filesize($path);
811
-		$stat['size'] = $fileSize;
812
-		$stat[7] = $fileSize;
813
-		return $stat;
814
-	}
815
-
816
-	/**
817
-	 * see http://php.net/manual/en/function.hash.php
818
-	 *
819
-	 * @param string $type
820
-	 * @param string $path
821
-	 * @param bool $raw
822
-	 * @return string
823
-	 */
824
-	public function hash($type, $path, $raw = false) {
825
-		$fh = $this->fopen($path, 'rb');
826
-		$ctx = hash_init($type);
827
-		hash_update_stream($ctx, $fh);
828
-		fclose($fh);
829
-		return hash_final($ctx, $raw);
830
-	}
831
-
832
-	/**
833
-	 * return full path, including mount point
834
-	 *
835
-	 * @param string $path relative to mount point
836
-	 * @return string full path including mount point
837
-	 */
838
-	protected function getFullPath($path) {
839
-		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
840
-	}
841
-
842
-	/**
843
-	 * read first block of encrypted file, typically this will contain the
844
-	 * encryption header
845
-	 *
846
-	 * @param string $path
847
-	 * @return string
848
-	 */
849
-	protected function readFirstBlock($path) {
850
-		$firstBlock = '';
851
-		if ($this->storage->file_exists($path)) {
852
-			$handle = $this->storage->fopen($path, 'r');
853
-			$firstBlock = fread($handle, $this->util->getHeaderSize());
854
-			fclose($handle);
855
-		}
856
-		return $firstBlock;
857
-	}
858
-
859
-	/**
860
-	 * return header size of given file
861
-	 *
862
-	 * @param string $path
863
-	 * @return int
864
-	 */
865
-	protected function getHeaderSize($path) {
866
-		$headerSize = 0;
867
-		$realFile = $this->util->stripPartialFileExtension($path);
868
-		if ($this->storage->file_exists($realFile)) {
869
-			$path = $realFile;
870
-		}
871
-		$firstBlock = $this->readFirstBlock($path);
872
-
873
-		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
874
-			$headerSize = $this->util->getHeaderSize();
875
-		}
876
-
877
-		return $headerSize;
878
-	}
879
-
880
-	/**
881
-	 * parse raw header to array
882
-	 *
883
-	 * @param string $rawHeader
884
-	 * @return array
885
-	 */
886
-	protected function parseRawHeader($rawHeader) {
887
-		$result = array();
888
-		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
889
-			$header = $rawHeader;
890
-			$endAt = strpos($header, Util::HEADER_END);
891
-			if ($endAt !== false) {
892
-				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
893
-
894
-				// +1 to not start with an ':' which would result in empty element at the beginning
895
-				$exploded = explode(':', substr($header, strlen(Util::HEADER_START)+1));
896
-
897
-				$element = array_shift($exploded);
898
-				while ($element !== Util::HEADER_END) {
899
-					$result[$element] = array_shift($exploded);
900
-					$element = array_shift($exploded);
901
-				}
902
-			}
903
-		}
904
-
905
-		return $result;
906
-	}
907
-
908
-	/**
909
-	 * read header from file
910
-	 *
911
-	 * @param string $path
912
-	 * @return array
913
-	 */
914
-	protected function getHeader($path) {
915
-		$realFile = $this->util->stripPartialFileExtension($path);
916
-		$exists = $this->storage->file_exists($realFile);
917
-		if ($exists) {
918
-			$path = $realFile;
919
-		}
920
-
921
-		$firstBlock = $this->readFirstBlock($path);
922
-		$result = $this->parseRawHeader($firstBlock);
923
-
924
-		// if the header doesn't contain a encryption module we check if it is a
925
-		// legacy file. If true, we add the default encryption module
926
-		if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
927
-			if (!empty($result)) {
928
-				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
929
-			} else if ($exists) {
930
-				// if the header was empty we have to check first if it is a encrypted file at all
931
-				// We would do query to filecache only if we know that entry in filecache exists
932
-				$info = $this->getCache()->get($path);
933
-				if (isset($info['encrypted']) && $info['encrypted'] === true) {
934
-					$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
935
-				}
936
-			}
937
-		}
938
-
939
-		return $result;
940
-	}
941
-
942
-	/**
943
-	 * read encryption module needed to read/write the file located at $path
944
-	 *
945
-	 * @param string $path
946
-	 * @return null|\OCP\Encryption\IEncryptionModule
947
-	 * @throws ModuleDoesNotExistsException
948
-	 * @throws \Exception
949
-	 */
950
-	protected function getEncryptionModule($path) {
951
-		$encryptionModule = null;
952
-		$header = $this->getHeader($path);
953
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
954
-		if (!empty($encryptionModuleId)) {
955
-			try {
956
-				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
957
-			} catch (ModuleDoesNotExistsException $e) {
958
-				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
959
-				throw $e;
960
-			}
961
-		}
962
-
963
-		return $encryptionModule;
964
-	}
965
-
966
-	/**
967
-	 * @param string $path
968
-	 * @param int $unencryptedSize
969
-	 */
970
-	public function updateUnencryptedSize($path, $unencryptedSize) {
971
-		$this->unencryptedSize[$path] = $unencryptedSize;
972
-	}
973
-
974
-	/**
975
-	 * copy keys to new location
976
-	 *
977
-	 * @param string $source path relative to data/
978
-	 * @param string $target path relative to data/
979
-	 * @return bool
980
-	 */
981
-	protected function copyKeys($source, $target) {
982
-		if (!$this->util->isExcluded($source)) {
983
-			return $this->keyStorage->copyKeys($source, $target);
984
-		}
985
-
986
-		return false;
987
-	}
988
-
989
-	/**
990
-	 * check if path points to a files version
991
-	 *
992
-	 * @param $path
993
-	 * @return bool
994
-	 */
995
-	protected function isVersion($path) {
996
-		$normalized = Filesystem::normalizePath($path);
997
-		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
998
-	}
999
-
1000
-	/**
1001
-	 * check if the given storage should be encrypted or not
1002
-	 *
1003
-	 * @param $path
1004
-	 * @return bool
1005
-	 */
1006
-	protected function shouldEncrypt($path) {
1007
-		$fullPath = $this->getFullPath($path);
1008
-		$mountPointConfig = $this->mount->getOption('encrypt', true);
1009
-		if ($mountPointConfig === false) {
1010
-			return false;
1011
-		}
1012
-
1013
-		try {
1014
-			$encryptionModule = $this->getEncryptionModule($fullPath);
1015
-		} catch (ModuleDoesNotExistsException $e) {
1016
-			return false;
1017
-		}
1018
-
1019
-		if ($encryptionModule === null) {
1020
-			$encryptionModule = $this->encryptionManager->getEncryptionModule();
1021
-		}
1022
-
1023
-		return $encryptionModule->shouldEncrypt($fullPath);
1024
-
1025
-	}
51
+    use LocalTempFileTrait;
52
+
53
+    /** @var string */
54
+    private $mountPoint;
55
+
56
+    /** @var \OC\Encryption\Util */
57
+    private $util;
58
+
59
+    /** @var \OCP\Encryption\IManager */
60
+    private $encryptionManager;
61
+
62
+    /** @var \OCP\ILogger */
63
+    private $logger;
64
+
65
+    /** @var string */
66
+    private $uid;
67
+
68
+    /** @var array */
69
+    protected $unencryptedSize;
70
+
71
+    /** @var \OCP\Encryption\IFile */
72
+    private $fileHelper;
73
+
74
+    /** @var IMountPoint */
75
+    private $mount;
76
+
77
+    /** @var IStorage */
78
+    private $keyStorage;
79
+
80
+    /** @var Update */
81
+    private $update;
82
+
83
+    /** @var Manager */
84
+    private $mountManager;
85
+
86
+    /** @var array remember for which path we execute the repair step to avoid recursions */
87
+    private $fixUnencryptedSizeOf = array();
88
+
89
+    /** @var  ArrayCache */
90
+    private $arrayCache;
91
+
92
+    /**
93
+     * @param array $parameters
94
+     * @param IManager $encryptionManager
95
+     * @param Util $util
96
+     * @param ILogger $logger
97
+     * @param IFile $fileHelper
98
+     * @param string $uid
99
+     * @param IStorage $keyStorage
100
+     * @param Update $update
101
+     * @param Manager $mountManager
102
+     * @param ArrayCache $arrayCache
103
+     */
104
+    public function __construct(
105
+            $parameters,
106
+            IManager $encryptionManager = null,
107
+            Util $util = null,
108
+            ILogger $logger = null,
109
+            IFile $fileHelper = null,
110
+            $uid = null,
111
+            IStorage $keyStorage = null,
112
+            Update $update = null,
113
+            Manager $mountManager = null,
114
+            ArrayCache $arrayCache = null
115
+        ) {
116
+
117
+        $this->mountPoint = $parameters['mountPoint'];
118
+        $this->mount = $parameters['mount'];
119
+        $this->encryptionManager = $encryptionManager;
120
+        $this->util = $util;
121
+        $this->logger = $logger;
122
+        $this->uid = $uid;
123
+        $this->fileHelper = $fileHelper;
124
+        $this->keyStorage = $keyStorage;
125
+        $this->unencryptedSize = array();
126
+        $this->update = $update;
127
+        $this->mountManager = $mountManager;
128
+        $this->arrayCache = $arrayCache;
129
+        parent::__construct($parameters);
130
+    }
131
+
132
+    /**
133
+     * see http://php.net/manual/en/function.filesize.php
134
+     * The result for filesize when called on a folder is required to be 0
135
+     *
136
+     * @param string $path
137
+     * @return int
138
+     */
139
+    public function filesize($path) {
140
+        $fullPath = $this->getFullPath($path);
141
+
142
+        /** @var CacheEntry $info */
143
+        $info = $this->getCache()->get($path);
144
+        if (isset($this->unencryptedSize[$fullPath])) {
145
+            $size = $this->unencryptedSize[$fullPath];
146
+            // update file cache
147
+            if ($info instanceof ICacheEntry) {
148
+                $info = $info->getData();
149
+                $info['encrypted'] = $info['encryptedVersion'];
150
+            } else {
151
+                if (!is_array($info)) {
152
+                    $info = [];
153
+                }
154
+                $info['encrypted'] = true;
155
+            }
156
+
157
+            $info['size'] = $size;
158
+            $this->getCache()->put($path, $info);
159
+
160
+            return $size;
161
+        }
162
+
163
+        if (isset($info['fileid']) && $info['encrypted']) {
164
+            return $this->verifyUnencryptedSize($path, $info['size']);
165
+        }
166
+
167
+        return $this->storage->filesize($path);
168
+    }
169
+
170
+    /**
171
+     * @param string $path
172
+     * @return array
173
+     */
174
+    public function getMetaData($path) {
175
+        $data = $this->storage->getMetaData($path);
176
+        if (is_null($data)) {
177
+            return null;
178
+        }
179
+        $fullPath = $this->getFullPath($path);
180
+        $info = $this->getCache()->get($path);
181
+
182
+        if (isset($this->unencryptedSize[$fullPath])) {
183
+            $data['encrypted'] = true;
184
+            $data['size'] = $this->unencryptedSize[$fullPath];
185
+        } else {
186
+            if (isset($info['fileid']) && $info['encrypted']) {
187
+                $data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
188
+                $data['encrypted'] = true;
189
+            }
190
+        }
191
+
192
+        if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
193
+            $data['encryptedVersion'] = $info['encryptedVersion'];
194
+        }
195
+
196
+        return $data;
197
+    }
198
+
199
+    /**
200
+     * see http://php.net/manual/en/function.file_get_contents.php
201
+     *
202
+     * @param string $path
203
+     * @return string
204
+     */
205
+    public function file_get_contents($path) {
206
+
207
+        $encryptionModule = $this->getEncryptionModule($path);
208
+
209
+        if ($encryptionModule) {
210
+            $handle = $this->fopen($path, "r");
211
+            if (!$handle) {
212
+                return false;
213
+            }
214
+            $data = stream_get_contents($handle);
215
+            fclose($handle);
216
+            return $data;
217
+        }
218
+        return $this->storage->file_get_contents($path);
219
+    }
220
+
221
+    /**
222
+     * see http://php.net/manual/en/function.file_put_contents.php
223
+     *
224
+     * @param string $path
225
+     * @param string $data
226
+     * @return bool
227
+     */
228
+    public function file_put_contents($path, $data) {
229
+        // file put content will always be translated to a stream write
230
+        $handle = $this->fopen($path, 'w');
231
+        if (is_resource($handle)) {
232
+            $written = fwrite($handle, $data);
233
+            fclose($handle);
234
+            return $written;
235
+        }
236
+
237
+        return false;
238
+    }
239
+
240
+    /**
241
+     * see http://php.net/manual/en/function.unlink.php
242
+     *
243
+     * @param string $path
244
+     * @return bool
245
+     */
246
+    public function unlink($path) {
247
+        $fullPath = $this->getFullPath($path);
248
+        if ($this->util->isExcluded($fullPath)) {
249
+            return $this->storage->unlink($path);
250
+        }
251
+
252
+        $encryptionModule = $this->getEncryptionModule($path);
253
+        if ($encryptionModule) {
254
+            $this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
255
+        }
256
+
257
+        return $this->storage->unlink($path);
258
+    }
259
+
260
+    /**
261
+     * see http://php.net/manual/en/function.rename.php
262
+     *
263
+     * @param string $path1
264
+     * @param string $path2
265
+     * @return bool
266
+     */
267
+    public function rename($path1, $path2) {
268
+
269
+        $result = $this->storage->rename($path1, $path2);
270
+
271
+        if ($result &&
272
+            // versions always use the keys from the original file, so we can skip
273
+            // this step for versions
274
+            $this->isVersion($path2) === false &&
275
+            $this->encryptionManager->isEnabled()) {
276
+            $source = $this->getFullPath($path1);
277
+            if (!$this->util->isExcluded($source)) {
278
+                $target = $this->getFullPath($path2);
279
+                if (isset($this->unencryptedSize[$source])) {
280
+                    $this->unencryptedSize[$target] = $this->unencryptedSize[$source];
281
+                }
282
+                $this->keyStorage->renameKeys($source, $target);
283
+                $module = $this->getEncryptionModule($path2);
284
+                if ($module) {
285
+                    $module->update($target, $this->uid, []);
286
+                }
287
+            }
288
+        }
289
+
290
+        return $result;
291
+    }
292
+
293
+    /**
294
+     * see http://php.net/manual/en/function.rmdir.php
295
+     *
296
+     * @param string $path
297
+     * @return bool
298
+     */
299
+    public function rmdir($path) {
300
+        $result = $this->storage->rmdir($path);
301
+        $fullPath = $this->getFullPath($path);
302
+        if ($result &&
303
+            $this->util->isExcluded($fullPath) === false &&
304
+            $this->encryptionManager->isEnabled()
305
+        ) {
306
+            $this->keyStorage->deleteAllFileKeys($fullPath);
307
+        }
308
+
309
+        return $result;
310
+    }
311
+
312
+    /**
313
+     * check if a file can be read
314
+     *
315
+     * @param string $path
316
+     * @return bool
317
+     */
318
+    public function isReadable($path) {
319
+
320
+        $isReadable = true;
321
+
322
+        $metaData = $this->getMetaData($path);
323
+        if (
324
+            !$this->is_dir($path) &&
325
+            isset($metaData['encrypted']) &&
326
+            $metaData['encrypted'] === true
327
+        ) {
328
+            $fullPath = $this->getFullPath($path);
329
+            $module = $this->getEncryptionModule($path);
330
+            $isReadable = $module->isReadable($fullPath, $this->uid);
331
+        }
332
+
333
+        return $this->storage->isReadable($path) && $isReadable;
334
+    }
335
+
336
+    /**
337
+     * see http://php.net/manual/en/function.copy.php
338
+     *
339
+     * @param string $path1
340
+     * @param string $path2
341
+     * @return bool
342
+     */
343
+    public function copy($path1, $path2) {
344
+
345
+        $source = $this->getFullPath($path1);
346
+
347
+        if ($this->util->isExcluded($source)) {
348
+            return $this->storage->copy($path1, $path2);
349
+        }
350
+
351
+        // need to stream copy file by file in case we copy between a encrypted
352
+        // and a unencrypted storage
353
+        $this->unlink($path2);
354
+        return $this->copyFromStorage($this, $path1, $path2);
355
+    }
356
+
357
+    /**
358
+     * see http://php.net/manual/en/function.fopen.php
359
+     *
360
+     * @param string $path
361
+     * @param string $mode
362
+     * @return resource|bool
363
+     * @throws GenericEncryptionException
364
+     * @throws ModuleDoesNotExistsException
365
+     */
366
+    public function fopen($path, $mode) {
367
+
368
+        // check if the file is stored in the array cache, this means that we
369
+        // copy a file over to the versions folder, in this case we don't want to
370
+        // decrypt it
371
+        if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
372
+            $this->arrayCache->remove('encryption_copy_version_' . $path);
373
+            return $this->storage->fopen($path, $mode);
374
+        }
375
+
376
+        $encryptionEnabled = $this->encryptionManager->isEnabled();
377
+        $shouldEncrypt = false;
378
+        $encryptionModule = null;
379
+        $header = $this->getHeader($path);
380
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
381
+        $fullPath = $this->getFullPath($path);
382
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
383
+
384
+        if ($this->util->isExcluded($fullPath) === false) {
385
+
386
+            $size = $unencryptedSize = 0;
387
+            $realFile = $this->util->stripPartialFileExtension($path);
388
+            $targetExists = $this->file_exists($realFile) || $this->file_exists($path);
389
+            $targetIsEncrypted = false;
390
+            if ($targetExists) {
391
+                // in case the file exists we require the explicit module as
392
+                // specified in the file header - otherwise we need to fail hard to
393
+                // prevent data loss on client side
394
+                if (!empty($encryptionModuleId)) {
395
+                    $targetIsEncrypted = true;
396
+                    $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
397
+                }
398
+
399
+                if ($this->file_exists($path)) {
400
+                    $size = $this->storage->filesize($path);
401
+                    $unencryptedSize = $this->filesize($path);
402
+                } else {
403
+                    $size = $unencryptedSize = 0;
404
+                }
405
+            }
406
+
407
+            try {
408
+
409
+                if (
410
+                    $mode === 'w'
411
+                    || $mode === 'w+'
412
+                    || $mode === 'wb'
413
+                    || $mode === 'wb+'
414
+                ) {
415
+                    // don't overwrite encrypted files if encryption is not enabled
416
+                    if ($targetIsEncrypted && $encryptionEnabled === false) {
417
+                        throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled');
418
+                    }
419
+                    if ($encryptionEnabled) {
420
+                        // if $encryptionModuleId is empty, the default module will be used
421
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
422
+                        $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
423
+                        $signed = true;
424
+                    }
425
+                } else {
426
+                    $info = $this->getCache()->get($path);
427
+                    // only get encryption module if we found one in the header
428
+                    // or if file should be encrypted according to the file cache
429
+                    if (!empty($encryptionModuleId)) {
430
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
431
+                        $shouldEncrypt = true;
432
+                    } else if (empty($encryptionModuleId) && $info['encrypted'] === true) {
433
+                        // we come from a old installation. No header and/or no module defined
434
+                        // but the file is encrypted. In this case we need to use the
435
+                        // OC_DEFAULT_MODULE to read the file
436
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
437
+                        $shouldEncrypt = true;
438
+                        $targetIsEncrypted = true;
439
+                    }
440
+                }
441
+            } catch (ModuleDoesNotExistsException $e) {
442
+                $this->logger->logException($e, [
443
+                    'message' => 'Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted',
444
+                    'level' => \OCP\Util::WARN,
445
+                    'app' => 'core',
446
+                ]);
447
+            }
448
+
449
+            // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
450
+            if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
451
+                if (!$targetExists || !$targetIsEncrypted) {
452
+                    $shouldEncrypt = false;
453
+                }
454
+            }
455
+
456
+            if ($shouldEncrypt === true && $encryptionModule !== null) {
457
+                $headerSize = $this->getHeaderSize($path);
458
+                $source = $this->storage->fopen($path, $mode);
459
+                if (!is_resource($source)) {
460
+                    return false;
461
+                }
462
+                $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
463
+                    $this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
464
+                    $size, $unencryptedSize, $headerSize, $signed);
465
+                return $handle;
466
+            }
467
+
468
+        }
469
+
470
+        return $this->storage->fopen($path, $mode);
471
+    }
472
+
473
+
474
+    /**
475
+     * perform some plausibility checks if the the unencrypted size is correct.
476
+     * If not, we calculate the correct unencrypted size and return it
477
+     *
478
+     * @param string $path internal path relative to the storage root
479
+     * @param int $unencryptedSize size of the unencrypted file
480
+     *
481
+     * @return int unencrypted size
482
+     */
483
+    protected function verifyUnencryptedSize($path, $unencryptedSize) {
484
+
485
+        $size = $this->storage->filesize($path);
486
+        $result = $unencryptedSize;
487
+
488
+        if ($unencryptedSize < 0 ||
489
+            ($size > 0 && $unencryptedSize === $size)
490
+        ) {
491
+            // check if we already calculate the unencrypted size for the
492
+            // given path to avoid recursions
493
+            if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
494
+                $this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
495
+                try {
496
+                    $result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
497
+                } catch (\Exception $e) {
498
+                    $this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
499
+                    $this->logger->logException($e);
500
+                }
501
+                unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
502
+            }
503
+        }
504
+
505
+        return $result;
506
+    }
507
+
508
+    /**
509
+     * calculate the unencrypted size
510
+     *
511
+     * @param string $path internal path relative to the storage root
512
+     * @param int $size size of the physical file
513
+     * @param int $unencryptedSize size of the unencrypted file
514
+     *
515
+     * @return int calculated unencrypted size
516
+     */
517
+    protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
518
+
519
+        $headerSize = $this->getHeaderSize($path);
520
+        $header = $this->getHeader($path);
521
+        $encryptionModule = $this->getEncryptionModule($path);
522
+
523
+        $stream = $this->storage->fopen($path, 'r');
524
+
525
+        // if we couldn't open the file we return the old unencrypted size
526
+        if (!is_resource($stream)) {
527
+            $this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
528
+            return $unencryptedSize;
529
+        }
530
+
531
+        $newUnencryptedSize = 0;
532
+        $size -= $headerSize;
533
+        $blockSize = $this->util->getBlockSize();
534
+
535
+        // if a header exists we skip it
536
+        if ($headerSize > 0) {
537
+            fread($stream, $headerSize);
538
+        }
539
+
540
+        // fast path, else the calculation for $lastChunkNr is bogus
541
+        if ($size === 0) {
542
+            return 0;
543
+        }
544
+
545
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
546
+        $unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
547
+
548
+        // calculate last chunk nr
549
+        // next highest is end of chunks, one subtracted is last one
550
+        // we have to read the last chunk, we can't just calculate it (because of padding etc)
551
+
552
+        $lastChunkNr = ceil($size/ $blockSize)-1;
553
+        // calculate last chunk position
554
+        $lastChunkPos = ($lastChunkNr * $blockSize);
555
+        // try to fseek to the last chunk, if it fails we have to read the whole file
556
+        if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
557
+            $newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
558
+        }
559
+
560
+        $lastChunkContentEncrypted='';
561
+        $count = $blockSize;
562
+
563
+        while ($count > 0) {
564
+            $data=fread($stream, $blockSize);
565
+            $count=strlen($data);
566
+            $lastChunkContentEncrypted .= $data;
567
+            if(strlen($lastChunkContentEncrypted) > $blockSize) {
568
+                $newUnencryptedSize += $unencryptedBlockSize;
569
+                $lastChunkContentEncrypted=substr($lastChunkContentEncrypted, $blockSize);
570
+            }
571
+        }
572
+
573
+        fclose($stream);
574
+
575
+        // we have to decrypt the last chunk to get it actual size
576
+        $encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
577
+        $decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
578
+        $decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
579
+
580
+        // calc the real file size with the size of the last chunk
581
+        $newUnencryptedSize += strlen($decryptedLastChunk);
582
+
583
+        $this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
584
+
585
+        // write to cache if applicable
586
+        $cache = $this->storage->getCache();
587
+        if ($cache) {
588
+            $entry = $cache->get($path);
589
+            $cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
590
+        }
591
+
592
+        return $newUnencryptedSize;
593
+    }
594
+
595
+    /**
596
+     * @param Storage\IStorage $sourceStorage
597
+     * @param string $sourceInternalPath
598
+     * @param string $targetInternalPath
599
+     * @param bool $preserveMtime
600
+     * @return bool
601
+     */
602
+    public function moveFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
603
+        if ($sourceStorage === $this) {
604
+            return $this->rename($sourceInternalPath, $targetInternalPath);
605
+        }
606
+
607
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
608
+        // - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
609
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
610
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
611
+        // - remove $this->copyBetweenStorage
612
+
613
+        if (!$sourceStorage->isDeletable($sourceInternalPath)) {
614
+            return false;
615
+        }
616
+
617
+        $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
618
+        if ($result) {
619
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
620
+                $result &= $sourceStorage->rmdir($sourceInternalPath);
621
+            } else {
622
+                $result &= $sourceStorage->unlink($sourceInternalPath);
623
+            }
624
+        }
625
+        return $result;
626
+    }
627
+
628
+
629
+    /**
630
+     * @param Storage\IStorage $sourceStorage
631
+     * @param string $sourceInternalPath
632
+     * @param string $targetInternalPath
633
+     * @param bool $preserveMtime
634
+     * @param bool $isRename
635
+     * @return bool
636
+     */
637
+    public function copyFromStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) {
638
+
639
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
640
+        // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
641
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
642
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
643
+        // - remove $this->copyBetweenStorage
644
+
645
+        return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
646
+    }
647
+
648
+    /**
649
+     * Update the encrypted cache version in the database
650
+     *
651
+     * @param Storage\IStorage $sourceStorage
652
+     * @param string $sourceInternalPath
653
+     * @param string $targetInternalPath
654
+     * @param bool $isRename
655
+     */
656
+    private function updateEncryptedVersion(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename) {
657
+        $isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath) ? 1 : 0;
658
+        $cacheInformation = [
659
+            'encrypted' => (bool)$isEncrypted,
660
+        ];
661
+        if($isEncrypted === 1) {
662
+            $encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
663
+
664
+            // In case of a move operation from an unencrypted to an encrypted
665
+            // storage the old encrypted version would stay with "0" while the
666
+            // correct value would be "1". Thus we manually set the value to "1"
667
+            // for those cases.
668
+            // See also https://github.com/owncloud/core/issues/23078
669
+            if($encryptedVersion === 0) {
670
+                $encryptedVersion = 1;
671
+            }
672
+
673
+            $cacheInformation['encryptedVersion'] = $encryptedVersion;
674
+        }
675
+
676
+        // in case of a rename we need to manipulate the source cache because
677
+        // this information will be kept for the new target
678
+        if ($isRename) {
679
+            $sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
680
+        } else {
681
+            $this->getCache()->put($targetInternalPath, $cacheInformation);
682
+        }
683
+    }
684
+
685
+    /**
686
+     * copy file between two storages
687
+     *
688
+     * @param Storage\IStorage $sourceStorage
689
+     * @param string $sourceInternalPath
690
+     * @param string $targetInternalPath
691
+     * @param bool $preserveMtime
692
+     * @param bool $isRename
693
+     * @return bool
694
+     * @throws \Exception
695
+     */
696
+    private function copyBetweenStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
697
+
698
+        // for versions we have nothing to do, because versions should always use the
699
+        // key from the original file. Just create a 1:1 copy and done
700
+        if ($this->isVersion($targetInternalPath) ||
701
+            $this->isVersion($sourceInternalPath)) {
702
+            // remember that we try to create a version so that we can detect it during
703
+            // fopen($sourceInternalPath) and by-pass the encryption in order to
704
+            // create a 1:1 copy of the file
705
+            $this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
706
+            $result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
707
+            $this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
708
+            if ($result) {
709
+                $info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
710
+                // make sure that we update the unencrypted size for the version
711
+                if (isset($info['encrypted']) && $info['encrypted'] === true) {
712
+                    $this->updateUnencryptedSize(
713
+                        $this->getFullPath($targetInternalPath),
714
+                        $info['size']
715
+                    );
716
+                }
717
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
718
+            }
719
+            return $result;
720
+        }
721
+
722
+        // first copy the keys that we reuse the existing file key on the target location
723
+        // and don't create a new one which would break versions for example.
724
+        $mount = $this->mountManager->findByStorageId($sourceStorage->getId());
725
+        if (count($mount) === 1) {
726
+            $mountPoint = $mount[0]->getMountPoint();
727
+            $source = $mountPoint . '/' . $sourceInternalPath;
728
+            $target = $this->getFullPath($targetInternalPath);
729
+            $this->copyKeys($source, $target);
730
+        } else {
731
+            $this->logger->error('Could not find mount point, can\'t keep encryption keys');
732
+        }
733
+
734
+        if ($sourceStorage->is_dir($sourceInternalPath)) {
735
+            $dh = $sourceStorage->opendir($sourceInternalPath);
736
+            $result = $this->mkdir($targetInternalPath);
737
+            if (is_resource($dh)) {
738
+                while ($result and ($file = readdir($dh)) !== false) {
739
+                    if (!Filesystem::isIgnoredDir($file)) {
740
+                        $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
741
+                    }
742
+                }
743
+            }
744
+        } else {
745
+            try {
746
+                $source = $sourceStorage->fopen($sourceInternalPath, 'r');
747
+                $target = $this->fopen($targetInternalPath, 'w');
748
+                list(, $result) = \OC_Helper::streamCopy($source, $target);
749
+                fclose($source);
750
+                fclose($target);
751
+            } catch (\Exception $e) {
752
+                fclose($source);
753
+                fclose($target);
754
+                throw $e;
755
+            }
756
+            if($result) {
757
+                if ($preserveMtime) {
758
+                    $this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
759
+                }
760
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
761
+            } else {
762
+                // delete partially written target file
763
+                $this->unlink($targetInternalPath);
764
+                // delete cache entry that was created by fopen
765
+                $this->getCache()->remove($targetInternalPath);
766
+            }
767
+        }
768
+        return (bool)$result;
769
+
770
+    }
771
+
772
+    /**
773
+     * get the path to a local version of the file.
774
+     * The local version of the file can be temporary and doesn't have to be persistent across requests
775
+     *
776
+     * @param string $path
777
+     * @return string
778
+     */
779
+    public function getLocalFile($path) {
780
+        if ($this->encryptionManager->isEnabled()) {
781
+            $cachedFile = $this->getCachedFile($path);
782
+            if (is_string($cachedFile)) {
783
+                return $cachedFile;
784
+            }
785
+        }
786
+        return $this->storage->getLocalFile($path);
787
+    }
788
+
789
+    /**
790
+     * Returns the wrapped storage's value for isLocal()
791
+     *
792
+     * @return bool wrapped storage's isLocal() value
793
+     */
794
+    public function isLocal() {
795
+        if ($this->encryptionManager->isEnabled()) {
796
+            return false;
797
+        }
798
+        return $this->storage->isLocal();
799
+    }
800
+
801
+    /**
802
+     * see http://php.net/manual/en/function.stat.php
803
+     * only the following keys are required in the result: size and mtime
804
+     *
805
+     * @param string $path
806
+     * @return array
807
+     */
808
+    public function stat($path) {
809
+        $stat = $this->storage->stat($path);
810
+        $fileSize = $this->filesize($path);
811
+        $stat['size'] = $fileSize;
812
+        $stat[7] = $fileSize;
813
+        return $stat;
814
+    }
815
+
816
+    /**
817
+     * see http://php.net/manual/en/function.hash.php
818
+     *
819
+     * @param string $type
820
+     * @param string $path
821
+     * @param bool $raw
822
+     * @return string
823
+     */
824
+    public function hash($type, $path, $raw = false) {
825
+        $fh = $this->fopen($path, 'rb');
826
+        $ctx = hash_init($type);
827
+        hash_update_stream($ctx, $fh);
828
+        fclose($fh);
829
+        return hash_final($ctx, $raw);
830
+    }
831
+
832
+    /**
833
+     * return full path, including mount point
834
+     *
835
+     * @param string $path relative to mount point
836
+     * @return string full path including mount point
837
+     */
838
+    protected function getFullPath($path) {
839
+        return Filesystem::normalizePath($this->mountPoint . '/' . $path);
840
+    }
841
+
842
+    /**
843
+     * read first block of encrypted file, typically this will contain the
844
+     * encryption header
845
+     *
846
+     * @param string $path
847
+     * @return string
848
+     */
849
+    protected function readFirstBlock($path) {
850
+        $firstBlock = '';
851
+        if ($this->storage->file_exists($path)) {
852
+            $handle = $this->storage->fopen($path, 'r');
853
+            $firstBlock = fread($handle, $this->util->getHeaderSize());
854
+            fclose($handle);
855
+        }
856
+        return $firstBlock;
857
+    }
858
+
859
+    /**
860
+     * return header size of given file
861
+     *
862
+     * @param string $path
863
+     * @return int
864
+     */
865
+    protected function getHeaderSize($path) {
866
+        $headerSize = 0;
867
+        $realFile = $this->util->stripPartialFileExtension($path);
868
+        if ($this->storage->file_exists($realFile)) {
869
+            $path = $realFile;
870
+        }
871
+        $firstBlock = $this->readFirstBlock($path);
872
+
873
+        if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
874
+            $headerSize = $this->util->getHeaderSize();
875
+        }
876
+
877
+        return $headerSize;
878
+    }
879
+
880
+    /**
881
+     * parse raw header to array
882
+     *
883
+     * @param string $rawHeader
884
+     * @return array
885
+     */
886
+    protected function parseRawHeader($rawHeader) {
887
+        $result = array();
888
+        if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
889
+            $header = $rawHeader;
890
+            $endAt = strpos($header, Util::HEADER_END);
891
+            if ($endAt !== false) {
892
+                $header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
893
+
894
+                // +1 to not start with an ':' which would result in empty element at the beginning
895
+                $exploded = explode(':', substr($header, strlen(Util::HEADER_START)+1));
896
+
897
+                $element = array_shift($exploded);
898
+                while ($element !== Util::HEADER_END) {
899
+                    $result[$element] = array_shift($exploded);
900
+                    $element = array_shift($exploded);
901
+                }
902
+            }
903
+        }
904
+
905
+        return $result;
906
+    }
907
+
908
+    /**
909
+     * read header from file
910
+     *
911
+     * @param string $path
912
+     * @return array
913
+     */
914
+    protected function getHeader($path) {
915
+        $realFile = $this->util->stripPartialFileExtension($path);
916
+        $exists = $this->storage->file_exists($realFile);
917
+        if ($exists) {
918
+            $path = $realFile;
919
+        }
920
+
921
+        $firstBlock = $this->readFirstBlock($path);
922
+        $result = $this->parseRawHeader($firstBlock);
923
+
924
+        // if the header doesn't contain a encryption module we check if it is a
925
+        // legacy file. If true, we add the default encryption module
926
+        if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
927
+            if (!empty($result)) {
928
+                $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
929
+            } else if ($exists) {
930
+                // if the header was empty we have to check first if it is a encrypted file at all
931
+                // We would do query to filecache only if we know that entry in filecache exists
932
+                $info = $this->getCache()->get($path);
933
+                if (isset($info['encrypted']) && $info['encrypted'] === true) {
934
+                    $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
935
+                }
936
+            }
937
+        }
938
+
939
+        return $result;
940
+    }
941
+
942
+    /**
943
+     * read encryption module needed to read/write the file located at $path
944
+     *
945
+     * @param string $path
946
+     * @return null|\OCP\Encryption\IEncryptionModule
947
+     * @throws ModuleDoesNotExistsException
948
+     * @throws \Exception
949
+     */
950
+    protected function getEncryptionModule($path) {
951
+        $encryptionModule = null;
952
+        $header = $this->getHeader($path);
953
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
954
+        if (!empty($encryptionModuleId)) {
955
+            try {
956
+                $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
957
+            } catch (ModuleDoesNotExistsException $e) {
958
+                $this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
959
+                throw $e;
960
+            }
961
+        }
962
+
963
+        return $encryptionModule;
964
+    }
965
+
966
+    /**
967
+     * @param string $path
968
+     * @param int $unencryptedSize
969
+     */
970
+    public function updateUnencryptedSize($path, $unencryptedSize) {
971
+        $this->unencryptedSize[$path] = $unencryptedSize;
972
+    }
973
+
974
+    /**
975
+     * copy keys to new location
976
+     *
977
+     * @param string $source path relative to data/
978
+     * @param string $target path relative to data/
979
+     * @return bool
980
+     */
981
+    protected function copyKeys($source, $target) {
982
+        if (!$this->util->isExcluded($source)) {
983
+            return $this->keyStorage->copyKeys($source, $target);
984
+        }
985
+
986
+        return false;
987
+    }
988
+
989
+    /**
990
+     * check if path points to a files version
991
+     *
992
+     * @param $path
993
+     * @return bool
994
+     */
995
+    protected function isVersion($path) {
996
+        $normalized = Filesystem::normalizePath($path);
997
+        return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
998
+    }
999
+
1000
+    /**
1001
+     * check if the given storage should be encrypted or not
1002
+     *
1003
+     * @param $path
1004
+     * @return bool
1005
+     */
1006
+    protected function shouldEncrypt($path) {
1007
+        $fullPath = $this->getFullPath($path);
1008
+        $mountPointConfig = $this->mount->getOption('encrypt', true);
1009
+        if ($mountPointConfig === false) {
1010
+            return false;
1011
+        }
1012
+
1013
+        try {
1014
+            $encryptionModule = $this->getEncryptionModule($fullPath);
1015
+        } catch (ModuleDoesNotExistsException $e) {
1016
+            return false;
1017
+        }
1018
+
1019
+        if ($encryptionModule === null) {
1020
+            $encryptionModule = $this->encryptionManager->getEncryptionModule();
1021
+        }
1022
+
1023
+        return $encryptionModule->shouldEncrypt($fullPath);
1024
+
1025
+    }
1026 1026
 
1027 1027
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Access.php 1 patch
Indentation   +1868 added lines, -1868 removed lines patch added patch discarded remove patch
@@ -59,1633 +59,1633 @@  discard block
 block discarded – undo
59 59
  * @package OCA\User_LDAP
60 60
  */
61 61
 class Access extends LDAPUtility implements IUserTools {
62
-	const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
63
-
64
-	/** @var \OCA\User_LDAP\Connection */
65
-	public $connection;
66
-	/** @var Manager */
67
-	public $userManager;
68
-	//never ever check this var directly, always use getPagedSearchResultState
69
-	protected $pagedSearchedSuccessful;
70
-
71
-	/**
72
-	 * @var string[] $cookies an array of returned Paged Result cookies
73
-	 */
74
-	protected $cookies = array();
75
-
76
-	/**
77
-	 * @var string $lastCookie the last cookie returned from a Paged Results
78
-	 * operation, defaults to an empty string
79
-	 */
80
-	protected $lastCookie = '';
81
-
82
-	/**
83
-	 * @var AbstractMapping $userMapper
84
-	 */
85
-	protected $userMapper;
86
-
87
-	/**
88
-	* @var AbstractMapping $userMapper
89
-	*/
90
-	protected $groupMapper;
91
-
92
-	/**
93
-	 * @var \OCA\User_LDAP\Helper
94
-	 */
95
-	private $helper;
96
-	/** @var IConfig */
97
-	private $config;
98
-
99
-	public function __construct(
100
-		Connection $connection,
101
-		ILDAPWrapper $ldap,
102
-		Manager $userManager,
103
-		Helper $helper,
104
-		IConfig $config
105
-	) {
106
-		parent::__construct($ldap);
107
-		$this->connection = $connection;
108
-		$this->userManager = $userManager;
109
-		$this->userManager->setLdapAccess($this);
110
-		$this->helper = $helper;
111
-		$this->config = $config;
112
-	}
113
-
114
-	/**
115
-	 * sets the User Mapper
116
-	 * @param AbstractMapping $mapper
117
-	 */
118
-	public function setUserMapper(AbstractMapping $mapper) {
119
-		$this->userMapper = $mapper;
120
-	}
121
-
122
-	/**
123
-	 * returns the User Mapper
124
-	 * @throws \Exception
125
-	 * @return AbstractMapping
126
-	 */
127
-	public function getUserMapper() {
128
-		if(is_null($this->userMapper)) {
129
-			throw new \Exception('UserMapper was not assigned to this Access instance.');
130
-		}
131
-		return $this->userMapper;
132
-	}
133
-
134
-	/**
135
-	 * sets the Group Mapper
136
-	 * @param AbstractMapping $mapper
137
-	 */
138
-	public function setGroupMapper(AbstractMapping $mapper) {
139
-		$this->groupMapper = $mapper;
140
-	}
141
-
142
-	/**
143
-	 * returns the Group Mapper
144
-	 * @throws \Exception
145
-	 * @return AbstractMapping
146
-	 */
147
-	public function getGroupMapper() {
148
-		if(is_null($this->groupMapper)) {
149
-			throw new \Exception('GroupMapper was not assigned to this Access instance.');
150
-		}
151
-		return $this->groupMapper;
152
-	}
153
-
154
-	/**
155
-	 * @return bool
156
-	 */
157
-	private function checkConnection() {
158
-		return ($this->connection instanceof Connection);
159
-	}
160
-
161
-	/**
162
-	 * returns the Connection instance
163
-	 * @return \OCA\User_LDAP\Connection
164
-	 */
165
-	public function getConnection() {
166
-		return $this->connection;
167
-	}
168
-
169
-	/**
170
-	 * reads a given attribute for an LDAP record identified by a DN
171
-	 *
172
-	 * @param string $dn the record in question
173
-	 * @param string $attr the attribute that shall be retrieved
174
-	 *        if empty, just check the record's existence
175
-	 * @param string $filter
176
-	 * @return array|false an array of values on success or an empty
177
-	 *          array if $attr is empty, false otherwise
178
-	 * @throws ServerNotAvailableException
179
-	 */
180
-	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
181
-		if(!$this->checkConnection()) {
182
-			\OCP\Util::writeLog('user_ldap',
183
-				'No LDAP Connector assigned, access impossible for readAttribute.',
184
-				\OCP\Util::WARN);
185
-			return false;
186
-		}
187
-		$cr = $this->connection->getConnectionResource();
188
-		if(!$this->ldap->isResource($cr)) {
189
-			//LDAP not available
190
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
191
-			return false;
192
-		}
193
-		//Cancel possibly running Paged Results operation, otherwise we run in
194
-		//LDAP protocol errors
195
-		$this->abandonPagedSearch();
196
-		// openLDAP requires that we init a new Paged Search. Not needed by AD,
197
-		// but does not hurt either.
198
-		$pagingSize = (int)$this->connection->ldapPagingSize;
199
-		// 0 won't result in replies, small numbers may leave out groups
200
-		// (cf. #12306), 500 is default for paging and should work everywhere.
201
-		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
202
-		$attr = mb_strtolower($attr, 'UTF-8');
203
-		// the actual read attribute later may contain parameters on a ranged
204
-		// request, e.g. member;range=99-199. Depends on server reply.
205
-		$attrToRead = $attr;
206
-
207
-		$values = [];
208
-		$isRangeRequest = false;
209
-		do {
210
-			$result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
211
-			if(is_bool($result)) {
212
-				// when an exists request was run and it was successful, an empty
213
-				// array must be returned
214
-				return $result ? [] : false;
215
-			}
216
-
217
-			if (!$isRangeRequest) {
218
-				$values = $this->extractAttributeValuesFromResult($result, $attr);
219
-				if (!empty($values)) {
220
-					return $values;
221
-				}
222
-			}
223
-
224
-			$isRangeRequest = false;
225
-			$result = $this->extractRangeData($result, $attr);
226
-			if (!empty($result)) {
227
-				$normalizedResult = $this->extractAttributeValuesFromResult(
228
-					[ $attr => $result['values'] ],
229
-					$attr
230
-				);
231
-				$values = array_merge($values, $normalizedResult);
232
-
233
-				if($result['rangeHigh'] === '*') {
234
-					// when server replies with * as high range value, there are
235
-					// no more results left
236
-					return $values;
237
-				} else {
238
-					$low  = $result['rangeHigh'] + 1;
239
-					$attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
240
-					$isRangeRequest = true;
241
-				}
242
-			}
243
-		} while($isRangeRequest);
244
-
245
-		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
246
-		return false;
247
-	}
248
-
249
-	/**
250
-	 * Runs an read operation against LDAP
251
-	 *
252
-	 * @param resource $cr the LDAP connection
253
-	 * @param string $dn
254
-	 * @param string $attribute
255
-	 * @param string $filter
256
-	 * @param int $maxResults
257
-	 * @return array|bool false if there was any error, true if an exists check
258
-	 *                    was performed and the requested DN found, array with the
259
-	 *                    returned data on a successful usual operation
260
-	 * @throws ServerNotAvailableException
261
-	 */
262
-	public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
263
-		$this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
264
-		$dn = $this->helper->DNasBaseParameter($dn);
265
-		$rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
266
-		if (!$this->ldap->isResource($rr)) {
267
-			if ($attribute !== '') {
268
-				//do not throw this message on userExists check, irritates
269
-				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, \OCP\Util::DEBUG);
270
-			}
271
-			//in case an error occurs , e.g. object does not exist
272
-			return false;
273
-		}
274
-		if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
275
-			\OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', \OCP\Util::DEBUG);
276
-			return true;
277
-		}
278
-		$er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
279
-		if (!$this->ldap->isResource($er)) {
280
-			//did not match the filter, return false
281
-			return false;
282
-		}
283
-		//LDAP attributes are not case sensitive
284
-		$result = \OCP\Util::mb_array_change_key_case(
285
-			$this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
286
-
287
-		return $result;
288
-	}
289
-
290
-	/**
291
-	 * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
292
-	 * data if present.
293
-	 *
294
-	 * @param array $result from ILDAPWrapper::getAttributes()
295
-	 * @param string $attribute the attribute name that was read
296
-	 * @return string[]
297
-	 */
298
-	public function extractAttributeValuesFromResult($result, $attribute) {
299
-		$values = [];
300
-		if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
301
-			$lowercaseAttribute = strtolower($attribute);
302
-			for($i=0;$i<$result[$attribute]['count'];$i++) {
303
-				if($this->resemblesDN($attribute)) {
304
-					$values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
305
-				} elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
306
-					$values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
307
-				} else {
308
-					$values[] = $result[$attribute][$i];
309
-				}
310
-			}
311
-		}
312
-		return $values;
313
-	}
314
-
315
-	/**
316
-	 * Attempts to find ranged data in a getAttribute results and extracts the
317
-	 * returned values as well as information on the range and full attribute
318
-	 * name for further processing.
319
-	 *
320
-	 * @param array $result from ILDAPWrapper::getAttributes()
321
-	 * @param string $attribute the attribute name that was read. Without ";range=…"
322
-	 * @return array If a range was detected with keys 'values', 'attributeName',
323
-	 *               'attributeFull' and 'rangeHigh', otherwise empty.
324
-	 */
325
-	public function extractRangeData($result, $attribute) {
326
-		$keys = array_keys($result);
327
-		foreach($keys as $key) {
328
-			if($key !== $attribute && strpos($key, $attribute) === 0) {
329
-				$queryData = explode(';', $key);
330
-				if(strpos($queryData[1], 'range=') === 0) {
331
-					$high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
332
-					$data = [
333
-						'values' => $result[$key],
334
-						'attributeName' => $queryData[0],
335
-						'attributeFull' => $key,
336
-						'rangeHigh' => $high,
337
-					];
338
-					return $data;
339
-				}
340
-			}
341
-		}
342
-		return [];
343
-	}
62
+    const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
63
+
64
+    /** @var \OCA\User_LDAP\Connection */
65
+    public $connection;
66
+    /** @var Manager */
67
+    public $userManager;
68
+    //never ever check this var directly, always use getPagedSearchResultState
69
+    protected $pagedSearchedSuccessful;
70
+
71
+    /**
72
+     * @var string[] $cookies an array of returned Paged Result cookies
73
+     */
74
+    protected $cookies = array();
75
+
76
+    /**
77
+     * @var string $lastCookie the last cookie returned from a Paged Results
78
+     * operation, defaults to an empty string
79
+     */
80
+    protected $lastCookie = '';
81
+
82
+    /**
83
+     * @var AbstractMapping $userMapper
84
+     */
85
+    protected $userMapper;
86
+
87
+    /**
88
+     * @var AbstractMapping $userMapper
89
+     */
90
+    protected $groupMapper;
91
+
92
+    /**
93
+     * @var \OCA\User_LDAP\Helper
94
+     */
95
+    private $helper;
96
+    /** @var IConfig */
97
+    private $config;
98
+
99
+    public function __construct(
100
+        Connection $connection,
101
+        ILDAPWrapper $ldap,
102
+        Manager $userManager,
103
+        Helper $helper,
104
+        IConfig $config
105
+    ) {
106
+        parent::__construct($ldap);
107
+        $this->connection = $connection;
108
+        $this->userManager = $userManager;
109
+        $this->userManager->setLdapAccess($this);
110
+        $this->helper = $helper;
111
+        $this->config = $config;
112
+    }
113
+
114
+    /**
115
+     * sets the User Mapper
116
+     * @param AbstractMapping $mapper
117
+     */
118
+    public function setUserMapper(AbstractMapping $mapper) {
119
+        $this->userMapper = $mapper;
120
+    }
121
+
122
+    /**
123
+     * returns the User Mapper
124
+     * @throws \Exception
125
+     * @return AbstractMapping
126
+     */
127
+    public function getUserMapper() {
128
+        if(is_null($this->userMapper)) {
129
+            throw new \Exception('UserMapper was not assigned to this Access instance.');
130
+        }
131
+        return $this->userMapper;
132
+    }
133
+
134
+    /**
135
+     * sets the Group Mapper
136
+     * @param AbstractMapping $mapper
137
+     */
138
+    public function setGroupMapper(AbstractMapping $mapper) {
139
+        $this->groupMapper = $mapper;
140
+    }
141
+
142
+    /**
143
+     * returns the Group Mapper
144
+     * @throws \Exception
145
+     * @return AbstractMapping
146
+     */
147
+    public function getGroupMapper() {
148
+        if(is_null($this->groupMapper)) {
149
+            throw new \Exception('GroupMapper was not assigned to this Access instance.');
150
+        }
151
+        return $this->groupMapper;
152
+    }
153
+
154
+    /**
155
+     * @return bool
156
+     */
157
+    private function checkConnection() {
158
+        return ($this->connection instanceof Connection);
159
+    }
160
+
161
+    /**
162
+     * returns the Connection instance
163
+     * @return \OCA\User_LDAP\Connection
164
+     */
165
+    public function getConnection() {
166
+        return $this->connection;
167
+    }
168
+
169
+    /**
170
+     * reads a given attribute for an LDAP record identified by a DN
171
+     *
172
+     * @param string $dn the record in question
173
+     * @param string $attr the attribute that shall be retrieved
174
+     *        if empty, just check the record's existence
175
+     * @param string $filter
176
+     * @return array|false an array of values on success or an empty
177
+     *          array if $attr is empty, false otherwise
178
+     * @throws ServerNotAvailableException
179
+     */
180
+    public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
181
+        if(!$this->checkConnection()) {
182
+            \OCP\Util::writeLog('user_ldap',
183
+                'No LDAP Connector assigned, access impossible for readAttribute.',
184
+                \OCP\Util::WARN);
185
+            return false;
186
+        }
187
+        $cr = $this->connection->getConnectionResource();
188
+        if(!$this->ldap->isResource($cr)) {
189
+            //LDAP not available
190
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
191
+            return false;
192
+        }
193
+        //Cancel possibly running Paged Results operation, otherwise we run in
194
+        //LDAP protocol errors
195
+        $this->abandonPagedSearch();
196
+        // openLDAP requires that we init a new Paged Search. Not needed by AD,
197
+        // but does not hurt either.
198
+        $pagingSize = (int)$this->connection->ldapPagingSize;
199
+        // 0 won't result in replies, small numbers may leave out groups
200
+        // (cf. #12306), 500 is default for paging and should work everywhere.
201
+        $maxResults = $pagingSize > 20 ? $pagingSize : 500;
202
+        $attr = mb_strtolower($attr, 'UTF-8');
203
+        // the actual read attribute later may contain parameters on a ranged
204
+        // request, e.g. member;range=99-199. Depends on server reply.
205
+        $attrToRead = $attr;
206
+
207
+        $values = [];
208
+        $isRangeRequest = false;
209
+        do {
210
+            $result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
211
+            if(is_bool($result)) {
212
+                // when an exists request was run and it was successful, an empty
213
+                // array must be returned
214
+                return $result ? [] : false;
215
+            }
216
+
217
+            if (!$isRangeRequest) {
218
+                $values = $this->extractAttributeValuesFromResult($result, $attr);
219
+                if (!empty($values)) {
220
+                    return $values;
221
+                }
222
+            }
223
+
224
+            $isRangeRequest = false;
225
+            $result = $this->extractRangeData($result, $attr);
226
+            if (!empty($result)) {
227
+                $normalizedResult = $this->extractAttributeValuesFromResult(
228
+                    [ $attr => $result['values'] ],
229
+                    $attr
230
+                );
231
+                $values = array_merge($values, $normalizedResult);
232
+
233
+                if($result['rangeHigh'] === '*') {
234
+                    // when server replies with * as high range value, there are
235
+                    // no more results left
236
+                    return $values;
237
+                } else {
238
+                    $low  = $result['rangeHigh'] + 1;
239
+                    $attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
240
+                    $isRangeRequest = true;
241
+                }
242
+            }
243
+        } while($isRangeRequest);
244
+
245
+        \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
246
+        return false;
247
+    }
248
+
249
+    /**
250
+     * Runs an read operation against LDAP
251
+     *
252
+     * @param resource $cr the LDAP connection
253
+     * @param string $dn
254
+     * @param string $attribute
255
+     * @param string $filter
256
+     * @param int $maxResults
257
+     * @return array|bool false if there was any error, true if an exists check
258
+     *                    was performed and the requested DN found, array with the
259
+     *                    returned data on a successful usual operation
260
+     * @throws ServerNotAvailableException
261
+     */
262
+    public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
263
+        $this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
264
+        $dn = $this->helper->DNasBaseParameter($dn);
265
+        $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
266
+        if (!$this->ldap->isResource($rr)) {
267
+            if ($attribute !== '') {
268
+                //do not throw this message on userExists check, irritates
269
+                \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, \OCP\Util::DEBUG);
270
+            }
271
+            //in case an error occurs , e.g. object does not exist
272
+            return false;
273
+        }
274
+        if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
275
+            \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', \OCP\Util::DEBUG);
276
+            return true;
277
+        }
278
+        $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
279
+        if (!$this->ldap->isResource($er)) {
280
+            //did not match the filter, return false
281
+            return false;
282
+        }
283
+        //LDAP attributes are not case sensitive
284
+        $result = \OCP\Util::mb_array_change_key_case(
285
+            $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
286
+
287
+        return $result;
288
+    }
289
+
290
+    /**
291
+     * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
292
+     * data if present.
293
+     *
294
+     * @param array $result from ILDAPWrapper::getAttributes()
295
+     * @param string $attribute the attribute name that was read
296
+     * @return string[]
297
+     */
298
+    public function extractAttributeValuesFromResult($result, $attribute) {
299
+        $values = [];
300
+        if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
301
+            $lowercaseAttribute = strtolower($attribute);
302
+            for($i=0;$i<$result[$attribute]['count'];$i++) {
303
+                if($this->resemblesDN($attribute)) {
304
+                    $values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
305
+                } elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
306
+                    $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
307
+                } else {
308
+                    $values[] = $result[$attribute][$i];
309
+                }
310
+            }
311
+        }
312
+        return $values;
313
+    }
314
+
315
+    /**
316
+     * Attempts to find ranged data in a getAttribute results and extracts the
317
+     * returned values as well as information on the range and full attribute
318
+     * name for further processing.
319
+     *
320
+     * @param array $result from ILDAPWrapper::getAttributes()
321
+     * @param string $attribute the attribute name that was read. Without ";range=…"
322
+     * @return array If a range was detected with keys 'values', 'attributeName',
323
+     *               'attributeFull' and 'rangeHigh', otherwise empty.
324
+     */
325
+    public function extractRangeData($result, $attribute) {
326
+        $keys = array_keys($result);
327
+        foreach($keys as $key) {
328
+            if($key !== $attribute && strpos($key, $attribute) === 0) {
329
+                $queryData = explode(';', $key);
330
+                if(strpos($queryData[1], 'range=') === 0) {
331
+                    $high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
332
+                    $data = [
333
+                        'values' => $result[$key],
334
+                        'attributeName' => $queryData[0],
335
+                        'attributeFull' => $key,
336
+                        'rangeHigh' => $high,
337
+                    ];
338
+                    return $data;
339
+                }
340
+            }
341
+        }
342
+        return [];
343
+    }
344 344
 	
345
-	/**
346
-	 * Set password for an LDAP user identified by a DN
347
-	 *
348
-	 * @param string $userDN the user in question
349
-	 * @param string $password the new password
350
-	 * @return bool
351
-	 * @throws HintException
352
-	 * @throws \Exception
353
-	 */
354
-	public function setPassword($userDN, $password) {
355
-		if((int)$this->connection->turnOnPasswordChange !== 1) {
356
-			throw new \Exception('LDAP password changes are disabled.');
357
-		}
358
-		$cr = $this->connection->getConnectionResource();
359
-		if(!$this->ldap->isResource($cr)) {
360
-			//LDAP not available
361
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
362
-			return false;
363
-		}
364
-		try {
365
-			return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
366
-		} catch(ConstraintViolationException $e) {
367
-			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
368
-		}
369
-	}
370
-
371
-	/**
372
-	 * checks whether the given attributes value is probably a DN
373
-	 * @param string $attr the attribute in question
374
-	 * @return boolean if so true, otherwise false
375
-	 */
376
-	private function resemblesDN($attr) {
377
-		$resemblingAttributes = array(
378
-			'dn',
379
-			'uniquemember',
380
-			'member',
381
-			// memberOf is an "operational" attribute, without a definition in any RFC
382
-			'memberof'
383
-		);
384
-		return in_array($attr, $resemblingAttributes);
385
-	}
386
-
387
-	/**
388
-	 * checks whether the given string is probably a DN
389
-	 * @param string $string
390
-	 * @return boolean
391
-	 */
392
-	public function stringResemblesDN($string) {
393
-		$r = $this->ldap->explodeDN($string, 0);
394
-		// if exploding a DN succeeds and does not end up in
395
-		// an empty array except for $r[count] being 0.
396
-		return (is_array($r) && count($r) > 1);
397
-	}
398
-
399
-	/**
400
-	 * returns a DN-string that is cleaned from not domain parts, e.g.
401
-	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
402
-	 * becomes dc=foobar,dc=server,dc=org
403
-	 * @param string $dn
404
-	 * @return string
405
-	 */
406
-	public function getDomainDNFromDN($dn) {
407
-		$allParts = $this->ldap->explodeDN($dn, 0);
408
-		if($allParts === false) {
409
-			//not a valid DN
410
-			return '';
411
-		}
412
-		$domainParts = array();
413
-		$dcFound = false;
414
-		foreach($allParts as $part) {
415
-			if(!$dcFound && strpos($part, 'dc=') === 0) {
416
-				$dcFound = true;
417
-			}
418
-			if($dcFound) {
419
-				$domainParts[] = $part;
420
-			}
421
-		}
422
-		return implode(',', $domainParts);
423
-	}
424
-
425
-	/**
426
-	 * returns the LDAP DN for the given internal Nextcloud name of the group
427
-	 * @param string $name the Nextcloud name in question
428
-	 * @return string|false LDAP DN on success, otherwise false
429
-	 */
430
-	public function groupname2dn($name) {
431
-		return $this->groupMapper->getDNByName($name);
432
-	}
433
-
434
-	/**
435
-	 * returns the LDAP DN for the given internal Nextcloud name of the user
436
-	 * @param string $name the Nextcloud name in question
437
-	 * @return string|false with the LDAP DN on success, otherwise false
438
-	 */
439
-	public function username2dn($name) {
440
-		$fdn = $this->userMapper->getDNByName($name);
441
-
442
-		//Check whether the DN belongs to the Base, to avoid issues on multi-
443
-		//server setups
444
-		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
445
-			return $fdn;
446
-		}
447
-
448
-		return false;
449
-	}
450
-
451
-	/**
452
-	 * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
453
-	 * @param string $fdn the dn of the group object
454
-	 * @param string $ldapName optional, the display name of the object
455
-	 * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
456
-	 */
457
-	public function dn2groupname($fdn, $ldapName = null) {
458
-		//To avoid bypassing the base DN settings under certain circumstances
459
-		//with the group support, check whether the provided DN matches one of
460
-		//the given Bases
461
-		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
462
-			return false;
463
-		}
464
-
465
-		return $this->dn2ocname($fdn, $ldapName, false);
466
-	}
467
-
468
-	/**
469
-	 * accepts an array of group DNs and tests whether they match the user
470
-	 * filter by doing read operations against the group entries. Returns an
471
-	 * array of DNs that match the filter.
472
-	 *
473
-	 * @param string[] $groupDNs
474
-	 * @return string[]
475
-	 */
476
-	public function groupsMatchFilter($groupDNs) {
477
-		$validGroupDNs = [];
478
-		foreach($groupDNs as $dn) {
479
-			$cacheKey = 'groupsMatchFilter-'.$dn;
480
-			$groupMatchFilter = $this->connection->getFromCache($cacheKey);
481
-			if(!is_null($groupMatchFilter)) {
482
-				if($groupMatchFilter) {
483
-					$validGroupDNs[] = $dn;
484
-				}
485
-				continue;
486
-			}
487
-
488
-			// Check the base DN first. If this is not met already, we don't
489
-			// need to ask the server at all.
490
-			if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
491
-				$this->connection->writeToCache($cacheKey, false);
492
-				continue;
493
-			}
494
-
495
-			$result = $this->readAttribute($dn, 'cn', $this->connection->ldapGroupFilter);
496
-			if(is_array($result)) {
497
-				$this->connection->writeToCache($cacheKey, true);
498
-				$validGroupDNs[] = $dn;
499
-			} else {
500
-				$this->connection->writeToCache($cacheKey, false);
501
-			}
502
-
503
-		}
504
-		return $validGroupDNs;
505
-	}
506
-
507
-	/**
508
-	 * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
509
-	 * @param string $dn the dn of the user object
510
-	 * @param string $ldapName optional, the display name of the object
511
-	 * @return string|false with with the name to use in Nextcloud
512
-	 */
513
-	public function dn2username($fdn, $ldapName = null) {
514
-		//To avoid bypassing the base DN settings under certain circumstances
515
-		//with the group support, check whether the provided DN matches one of
516
-		//the given Bases
517
-		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
518
-			return false;
519
-		}
520
-
521
-		return $this->dn2ocname($fdn, $ldapName, true);
522
-	}
523
-
524
-	/**
525
-	 * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
526
-	 *
527
-	 * @param string $fdn the dn of the user object
528
-	 * @param string|null $ldapName optional, the display name of the object
529
-	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
530
-	 * @param bool|null $newlyMapped
531
-	 * @param array|null $record
532
-	 * @return false|string with with the name to use in Nextcloud
533
-	 * @throws \Exception
534
-	 */
535
-	public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
536
-		$newlyMapped = false;
537
-		if($isUser) {
538
-			$mapper = $this->getUserMapper();
539
-			$nameAttribute = $this->connection->ldapUserDisplayName;
540
-		} else {
541
-			$mapper = $this->getGroupMapper();
542
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
543
-		}
544
-
545
-		//let's try to retrieve the Nextcloud name from the mappings table
546
-		$ncName = $mapper->getNameByDN($fdn);
547
-		if(is_string($ncName)) {
548
-			return $ncName;
549
-		}
550
-
551
-		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
552
-		$uuid = $this->getUUID($fdn, $isUser, $record);
553
-		if(is_string($uuid)) {
554
-			$ncName = $mapper->getNameByUUID($uuid);
555
-			if(is_string($ncName)) {
556
-				$mapper->setDNbyUUID($fdn, $uuid);
557
-				return $ncName;
558
-			}
559
-		} else {
560
-			//If the UUID can't be detected something is foul.
561
-			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
562
-			return false;
563
-		}
564
-
565
-		if(is_null($ldapName)) {
566
-			$ldapName = $this->readAttribute($fdn, $nameAttribute);
567
-			if(!isset($ldapName[0]) && empty($ldapName[0])) {
568
-				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
569
-				return false;
570
-			}
571
-			$ldapName = $ldapName[0];
572
-		}
573
-
574
-		if($isUser) {
575
-			$usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
576
-			if ($usernameAttribute !== '') {
577
-				$username = $this->readAttribute($fdn, $usernameAttribute);
578
-				$username = $username[0];
579
-			} else {
580
-				$username = $uuid;
581
-			}
582
-			$intName = $this->sanitizeUsername($username);
583
-		} else {
584
-			$intName = $ldapName;
585
-		}
586
-
587
-		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
588
-		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
589
-		//NOTE: mind, disabling cache affects only this instance! Using it
590
-		// outside of core user management will still cache the user as non-existing.
591
-		$originalTTL = $this->connection->ldapCacheTTL;
592
-		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
593
-		if(($isUser && $intName !== '' && !\OC::$server->getUserManager()->userExists($intName))
594
-			|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) {
595
-			if($mapper->map($fdn, $intName, $uuid)) {
596
-				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
597
-				$newlyMapped = true;
598
-				return $intName;
599
-			}
600
-		}
601
-		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
602
-
603
-		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
604
-		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
605
-			$newlyMapped = true;
606
-			return $altName;
607
-		}
608
-
609
-		//if everything else did not help..
610
-		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
611
-		return false;
612
-	}
613
-
614
-	/**
615
-	 * gives back the user names as they are used ownClod internally
616
-	 * @param array $ldapUsers as returned by fetchList()
617
-	 * @return array an array with the user names to use in Nextcloud
618
-	 *
619
-	 * gives back the user names as they are used ownClod internally
620
-	 */
621
-	public function nextcloudUserNames($ldapUsers) {
622
-		return $this->ldap2NextcloudNames($ldapUsers, true);
623
-	}
624
-
625
-	/**
626
-	 * gives back the group names as they are used ownClod internally
627
-	 * @param array $ldapGroups as returned by fetchList()
628
-	 * @return array an array with the group names to use in Nextcloud
629
-	 *
630
-	 * gives back the group names as they are used ownClod internally
631
-	 */
632
-	public function nextcloudGroupNames($ldapGroups) {
633
-		return $this->ldap2NextcloudNames($ldapGroups, false);
634
-	}
635
-
636
-	/**
637
-	 * @param array $ldapObjects as returned by fetchList()
638
-	 * @param bool $isUsers
639
-	 * @return array
640
-	 */
641
-	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
642
-		if($isUsers) {
643
-			$nameAttribute = $this->connection->ldapUserDisplayName;
644
-			$sndAttribute  = $this->connection->ldapUserDisplayName2;
645
-		} else {
646
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
647
-		}
648
-		$nextcloudNames = array();
649
-
650
-		foreach($ldapObjects as $ldapObject) {
651
-			$nameByLDAP = null;
652
-			if(    isset($ldapObject[$nameAttribute])
653
-				&& is_array($ldapObject[$nameAttribute])
654
-				&& isset($ldapObject[$nameAttribute][0])
655
-			) {
656
-				// might be set, but not necessarily. if so, we use it.
657
-				$nameByLDAP = $ldapObject[$nameAttribute][0];
658
-			}
659
-
660
-			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
661
-			if($ncName) {
662
-				$nextcloudNames[] = $ncName;
663
-				if($isUsers) {
664
-					//cache the user names so it does not need to be retrieved
665
-					//again later (e.g. sharing dialogue).
666
-					if(is_null($nameByLDAP)) {
667
-						continue;
668
-					}
669
-					$sndName = isset($ldapObject[$sndAttribute][0])
670
-						? $ldapObject[$sndAttribute][0] : '';
671
-					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
672
-				}
673
-			}
674
-		}
675
-		return $nextcloudNames;
676
-	}
677
-
678
-	/**
679
-	 * caches the user display name
680
-	 * @param string $ocName the internal Nextcloud username
681
-	 * @param string|false $home the home directory path
682
-	 */
683
-	public function cacheUserHome($ocName, $home) {
684
-		$cacheKey = 'getHome'.$ocName;
685
-		$this->connection->writeToCache($cacheKey, $home);
686
-	}
687
-
688
-	/**
689
-	 * caches a user as existing
690
-	 * @param string $ocName the internal Nextcloud username
691
-	 */
692
-	public function cacheUserExists($ocName) {
693
-		$this->connection->writeToCache('userExists'.$ocName, true);
694
-	}
695
-
696
-	/**
697
-	 * caches the user display name
698
-	 * @param string $ocName the internal Nextcloud username
699
-	 * @param string $displayName the display name
700
-	 * @param string $displayName2 the second display name
701
-	 */
702
-	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
703
-		$user = $this->userManager->get($ocName);
704
-		if($user === null) {
705
-			return;
706
-		}
707
-		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
708
-		$cacheKeyTrunk = 'getDisplayName';
709
-		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
710
-	}
711
-
712
-	/**
713
-	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
714
-	 * @param string $name the display name of the object
715
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
716
-	 *
717
-	 * Instead of using this method directly, call
718
-	 * createAltInternalOwnCloudName($name, true)
719
-	 */
720
-	private function _createAltInternalOwnCloudNameForUsers($name) {
721
-		$attempts = 0;
722
-		//while loop is just a precaution. If a name is not generated within
723
-		//20 attempts, something else is very wrong. Avoids infinite loop.
724
-		while($attempts < 20){
725
-			$altName = $name . '_' . rand(1000,9999);
726
-			if(!\OC::$server->getUserManager()->userExists($altName)) {
727
-				return $altName;
728
-			}
729
-			$attempts++;
730
-		}
731
-		return false;
732
-	}
733
-
734
-	/**
735
-	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
736
-	 * @param string $name the display name of the object
737
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
738
-	 *
739
-	 * Instead of using this method directly, call
740
-	 * createAltInternalOwnCloudName($name, false)
741
-	 *
742
-	 * Group names are also used as display names, so we do a sequential
743
-	 * numbering, e.g. Developers_42 when there are 41 other groups called
744
-	 * "Developers"
745
-	 */
746
-	private function _createAltInternalOwnCloudNameForGroups($name) {
747
-		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
748
-		if(!$usedNames || count($usedNames) === 0) {
749
-			$lastNo = 1; //will become name_2
750
-		} else {
751
-			natsort($usedNames);
752
-			$lastName = array_pop($usedNames);
753
-			$lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
754
-		}
755
-		$altName = $name.'_'. (string)($lastNo+1);
756
-		unset($usedNames);
757
-
758
-		$attempts = 1;
759
-		while($attempts < 21){
760
-			// Check to be really sure it is unique
761
-			// while loop is just a precaution. If a name is not generated within
762
-			// 20 attempts, something else is very wrong. Avoids infinite loop.
763
-			if(!\OC::$server->getGroupManager()->groupExists($altName)) {
764
-				return $altName;
765
-			}
766
-			$altName = $name . '_' . ($lastNo + $attempts);
767
-			$attempts++;
768
-		}
769
-		return false;
770
-	}
771
-
772
-	/**
773
-	 * creates a unique name for internal Nextcloud use.
774
-	 * @param string $name the display name of the object
775
-	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
776
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
777
-	 */
778
-	private function createAltInternalOwnCloudName($name, $isUser) {
779
-		$originalTTL = $this->connection->ldapCacheTTL;
780
-		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
781
-		if($isUser) {
782
-			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
783
-		} else {
784
-			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
785
-		}
786
-		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
787
-
788
-		return $altName;
789
-	}
790
-
791
-	/**
792
-	 * fetches a list of users according to a provided loginName and utilizing
793
-	 * the login filter.
794
-	 *
795
-	 * @param string $loginName
796
-	 * @param array $attributes optional, list of attributes to read
797
-	 * @return array
798
-	 */
799
-	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
800
-		$loginName = $this->escapeFilterPart($loginName);
801
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
802
-		return $this->fetchListOfUsers($filter, $attributes);
803
-	}
804
-
805
-	/**
806
-	 * counts the number of users according to a provided loginName and
807
-	 * utilizing the login filter.
808
-	 *
809
-	 * @param string $loginName
810
-	 * @return int
811
-	 */
812
-	public function countUsersByLoginName($loginName) {
813
-		$loginName = $this->escapeFilterPart($loginName);
814
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
815
-		return $this->countUsers($filter);
816
-	}
817
-
818
-	/**
819
-	 * @param string $filter
820
-	 * @param string|string[] $attr
821
-	 * @param int $limit
822
-	 * @param int $offset
823
-	 * @param bool $forceApplyAttributes
824
-	 * @return array
825
-	 */
826
-	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
827
-		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
828
-		$recordsToUpdate = $ldapRecords;
829
-		if(!$forceApplyAttributes) {
830
-			$isBackgroundJobModeAjax = $this->config
831
-					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
832
-			$recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
833
-				$newlyMapped = false;
834
-				$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
835
-				if(is_string($uid)) {
836
-					$this->cacheUserExists($uid);
837
-				}
838
-				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
839
-			});
840
-		}
841
-		$this->batchApplyUserAttributes($recordsToUpdate);
842
-		return $this->fetchList($ldapRecords, count($attr) > 1);
843
-	}
844
-
845
-	/**
846
-	 * provided with an array of LDAP user records the method will fetch the
847
-	 * user object and requests it to process the freshly fetched attributes and
848
-	 * and their values
849
-	 * @param array $ldapRecords
850
-	 */
851
-	public function batchApplyUserAttributes(array $ldapRecords){
852
-		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
853
-		foreach($ldapRecords as $userRecord) {
854
-			if(!isset($userRecord[$displayNameAttribute])) {
855
-				// displayName is obligatory
856
-				continue;
857
-			}
858
-			$ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
859
-			if($ocName === false) {
860
-				continue;
861
-			}
862
-			$user = $this->userManager->get($ocName);
863
-			if($user instanceof OfflineUser) {
864
-				$user->unmark();
865
-				$user = $this->userManager->get($ocName);
866
-			}
867
-			if ($user !== null) {
868
-				$user->processAttributes($userRecord);
869
-			} else {
870
-				\OC::$server->getLogger()->debug(
871
-					"The ldap user manager returned null for $ocName",
872
-					['app'=>'user_ldap']
873
-				);
874
-			}
875
-		}
876
-	}
877
-
878
-	/**
879
-	 * @param string $filter
880
-	 * @param string|string[] $attr
881
-	 * @param int $limit
882
-	 * @param int $offset
883
-	 * @return array
884
-	 */
885
-	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
886
-		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), count($attr) > 1);
887
-	}
888
-
889
-	/**
890
-	 * @param array $list
891
-	 * @param bool $manyAttributes
892
-	 * @return array
893
-	 */
894
-	private function fetchList($list, $manyAttributes) {
895
-		if(is_array($list)) {
896
-			if($manyAttributes) {
897
-				return $list;
898
-			} else {
899
-				$list = array_reduce($list, function($carry, $item) {
900
-					$attribute = array_keys($item)[0];
901
-					$carry[] = $item[$attribute][0];
902
-					return $carry;
903
-				}, array());
904
-				return array_unique($list, SORT_LOCALE_STRING);
905
-			}
906
-		}
907
-
908
-		//error cause actually, maybe throw an exception in future.
909
-		return array();
910
-	}
911
-
912
-	/**
913
-	 * executes an LDAP search, optimized for Users
914
-	 * @param string $filter the LDAP filter for the search
915
-	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
916
-	 * @param integer $limit
917
-	 * @param integer $offset
918
-	 * @return array with the search result
919
-	 *
920
-	 * Executes an LDAP search
921
-	 */
922
-	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
923
-		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
924
-	}
925
-
926
-	/**
927
-	 * @param string $filter
928
-	 * @param string|string[] $attr
929
-	 * @param int $limit
930
-	 * @param int $offset
931
-	 * @return false|int
932
-	 */
933
-	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
934
-		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
935
-	}
936
-
937
-	/**
938
-	 * executes an LDAP search, optimized for Groups
939
-	 * @param string $filter the LDAP filter for the search
940
-	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
941
-	 * @param integer $limit
942
-	 * @param integer $offset
943
-	 * @return array with the search result
944
-	 *
945
-	 * Executes an LDAP search
946
-	 */
947
-	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
948
-		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
949
-	}
950
-
951
-	/**
952
-	 * returns the number of available groups
953
-	 * @param string $filter the LDAP search filter
954
-	 * @param string[] $attr optional
955
-	 * @param int|null $limit
956
-	 * @param int|null $offset
957
-	 * @return int|bool
958
-	 */
959
-	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
960
-		return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
961
-	}
962
-
963
-	/**
964
-	 * returns the number of available objects on the base DN
965
-	 *
966
-	 * @param int|null $limit
967
-	 * @param int|null $offset
968
-	 * @return int|bool
969
-	 */
970
-	public function countObjects($limit = null, $offset = null) {
971
-		return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
972
-	}
973
-
974
-	/**
975
-	 * Returns the LDAP handler
976
-	 * @throws \OC\ServerNotAvailableException
977
-	 */
978
-
979
-	/**
980
-	 * @return mixed
981
-	 * @throws \OC\ServerNotAvailableException
982
-	 */
983
-	private function invokeLDAPMethod() {
984
-		$arguments = func_get_args();
985
-		$command = array_shift($arguments);
986
-		$cr = array_shift($arguments);
987
-		if (!method_exists($this->ldap, $command)) {
988
-			return null;
989
-		}
990
-		array_unshift($arguments, $cr);
991
-		// php no longer supports call-time pass-by-reference
992
-		// thus cannot support controlPagedResultResponse as the third argument
993
-		// is a reference
994
-		$doMethod = function () use ($command, &$arguments) {
995
-			if ($command == 'controlPagedResultResponse') {
996
-				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
997
-			} else {
998
-				return call_user_func_array(array($this->ldap, $command), $arguments);
999
-			}
1000
-		};
1001
-		try {
1002
-			$ret = $doMethod();
1003
-		} catch (ServerNotAvailableException $e) {
1004
-			/* Server connection lost, attempt to reestablish it
345
+    /**
346
+     * Set password for an LDAP user identified by a DN
347
+     *
348
+     * @param string $userDN the user in question
349
+     * @param string $password the new password
350
+     * @return bool
351
+     * @throws HintException
352
+     * @throws \Exception
353
+     */
354
+    public function setPassword($userDN, $password) {
355
+        if((int)$this->connection->turnOnPasswordChange !== 1) {
356
+            throw new \Exception('LDAP password changes are disabled.');
357
+        }
358
+        $cr = $this->connection->getConnectionResource();
359
+        if(!$this->ldap->isResource($cr)) {
360
+            //LDAP not available
361
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
362
+            return false;
363
+        }
364
+        try {
365
+            return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
366
+        } catch(ConstraintViolationException $e) {
367
+            throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
368
+        }
369
+    }
370
+
371
+    /**
372
+     * checks whether the given attributes value is probably a DN
373
+     * @param string $attr the attribute in question
374
+     * @return boolean if so true, otherwise false
375
+     */
376
+    private function resemblesDN($attr) {
377
+        $resemblingAttributes = array(
378
+            'dn',
379
+            'uniquemember',
380
+            'member',
381
+            // memberOf is an "operational" attribute, without a definition in any RFC
382
+            'memberof'
383
+        );
384
+        return in_array($attr, $resemblingAttributes);
385
+    }
386
+
387
+    /**
388
+     * checks whether the given string is probably a DN
389
+     * @param string $string
390
+     * @return boolean
391
+     */
392
+    public function stringResemblesDN($string) {
393
+        $r = $this->ldap->explodeDN($string, 0);
394
+        // if exploding a DN succeeds and does not end up in
395
+        // an empty array except for $r[count] being 0.
396
+        return (is_array($r) && count($r) > 1);
397
+    }
398
+
399
+    /**
400
+     * returns a DN-string that is cleaned from not domain parts, e.g.
401
+     * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
402
+     * becomes dc=foobar,dc=server,dc=org
403
+     * @param string $dn
404
+     * @return string
405
+     */
406
+    public function getDomainDNFromDN($dn) {
407
+        $allParts = $this->ldap->explodeDN($dn, 0);
408
+        if($allParts === false) {
409
+            //not a valid DN
410
+            return '';
411
+        }
412
+        $domainParts = array();
413
+        $dcFound = false;
414
+        foreach($allParts as $part) {
415
+            if(!$dcFound && strpos($part, 'dc=') === 0) {
416
+                $dcFound = true;
417
+            }
418
+            if($dcFound) {
419
+                $domainParts[] = $part;
420
+            }
421
+        }
422
+        return implode(',', $domainParts);
423
+    }
424
+
425
+    /**
426
+     * returns the LDAP DN for the given internal Nextcloud name of the group
427
+     * @param string $name the Nextcloud name in question
428
+     * @return string|false LDAP DN on success, otherwise false
429
+     */
430
+    public function groupname2dn($name) {
431
+        return $this->groupMapper->getDNByName($name);
432
+    }
433
+
434
+    /**
435
+     * returns the LDAP DN for the given internal Nextcloud name of the user
436
+     * @param string $name the Nextcloud name in question
437
+     * @return string|false with the LDAP DN on success, otherwise false
438
+     */
439
+    public function username2dn($name) {
440
+        $fdn = $this->userMapper->getDNByName($name);
441
+
442
+        //Check whether the DN belongs to the Base, to avoid issues on multi-
443
+        //server setups
444
+        if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
445
+            return $fdn;
446
+        }
447
+
448
+        return false;
449
+    }
450
+
451
+    /**
452
+     * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
453
+     * @param string $fdn the dn of the group object
454
+     * @param string $ldapName optional, the display name of the object
455
+     * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
456
+     */
457
+    public function dn2groupname($fdn, $ldapName = null) {
458
+        //To avoid bypassing the base DN settings under certain circumstances
459
+        //with the group support, check whether the provided DN matches one of
460
+        //the given Bases
461
+        if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
462
+            return false;
463
+        }
464
+
465
+        return $this->dn2ocname($fdn, $ldapName, false);
466
+    }
467
+
468
+    /**
469
+     * accepts an array of group DNs and tests whether they match the user
470
+     * filter by doing read operations against the group entries. Returns an
471
+     * array of DNs that match the filter.
472
+     *
473
+     * @param string[] $groupDNs
474
+     * @return string[]
475
+     */
476
+    public function groupsMatchFilter($groupDNs) {
477
+        $validGroupDNs = [];
478
+        foreach($groupDNs as $dn) {
479
+            $cacheKey = 'groupsMatchFilter-'.$dn;
480
+            $groupMatchFilter = $this->connection->getFromCache($cacheKey);
481
+            if(!is_null($groupMatchFilter)) {
482
+                if($groupMatchFilter) {
483
+                    $validGroupDNs[] = $dn;
484
+                }
485
+                continue;
486
+            }
487
+
488
+            // Check the base DN first. If this is not met already, we don't
489
+            // need to ask the server at all.
490
+            if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
491
+                $this->connection->writeToCache($cacheKey, false);
492
+                continue;
493
+            }
494
+
495
+            $result = $this->readAttribute($dn, 'cn', $this->connection->ldapGroupFilter);
496
+            if(is_array($result)) {
497
+                $this->connection->writeToCache($cacheKey, true);
498
+                $validGroupDNs[] = $dn;
499
+            } else {
500
+                $this->connection->writeToCache($cacheKey, false);
501
+            }
502
+
503
+        }
504
+        return $validGroupDNs;
505
+    }
506
+
507
+    /**
508
+     * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
509
+     * @param string $dn the dn of the user object
510
+     * @param string $ldapName optional, the display name of the object
511
+     * @return string|false with with the name to use in Nextcloud
512
+     */
513
+    public function dn2username($fdn, $ldapName = null) {
514
+        //To avoid bypassing the base DN settings under certain circumstances
515
+        //with the group support, check whether the provided DN matches one of
516
+        //the given Bases
517
+        if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
518
+            return false;
519
+        }
520
+
521
+        return $this->dn2ocname($fdn, $ldapName, true);
522
+    }
523
+
524
+    /**
525
+     * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
526
+     *
527
+     * @param string $fdn the dn of the user object
528
+     * @param string|null $ldapName optional, the display name of the object
529
+     * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
530
+     * @param bool|null $newlyMapped
531
+     * @param array|null $record
532
+     * @return false|string with with the name to use in Nextcloud
533
+     * @throws \Exception
534
+     */
535
+    public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
536
+        $newlyMapped = false;
537
+        if($isUser) {
538
+            $mapper = $this->getUserMapper();
539
+            $nameAttribute = $this->connection->ldapUserDisplayName;
540
+        } else {
541
+            $mapper = $this->getGroupMapper();
542
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
543
+        }
544
+
545
+        //let's try to retrieve the Nextcloud name from the mappings table
546
+        $ncName = $mapper->getNameByDN($fdn);
547
+        if(is_string($ncName)) {
548
+            return $ncName;
549
+        }
550
+
551
+        //second try: get the UUID and check if it is known. Then, update the DN and return the name.
552
+        $uuid = $this->getUUID($fdn, $isUser, $record);
553
+        if(is_string($uuid)) {
554
+            $ncName = $mapper->getNameByUUID($uuid);
555
+            if(is_string($ncName)) {
556
+                $mapper->setDNbyUUID($fdn, $uuid);
557
+                return $ncName;
558
+            }
559
+        } else {
560
+            //If the UUID can't be detected something is foul.
561
+            \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
562
+            return false;
563
+        }
564
+
565
+        if(is_null($ldapName)) {
566
+            $ldapName = $this->readAttribute($fdn, $nameAttribute);
567
+            if(!isset($ldapName[0]) && empty($ldapName[0])) {
568
+                \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
569
+                return false;
570
+            }
571
+            $ldapName = $ldapName[0];
572
+        }
573
+
574
+        if($isUser) {
575
+            $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
576
+            if ($usernameAttribute !== '') {
577
+                $username = $this->readAttribute($fdn, $usernameAttribute);
578
+                $username = $username[0];
579
+            } else {
580
+                $username = $uuid;
581
+            }
582
+            $intName = $this->sanitizeUsername($username);
583
+        } else {
584
+            $intName = $ldapName;
585
+        }
586
+
587
+        //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
588
+        //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
589
+        //NOTE: mind, disabling cache affects only this instance! Using it
590
+        // outside of core user management will still cache the user as non-existing.
591
+        $originalTTL = $this->connection->ldapCacheTTL;
592
+        $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
593
+        if(($isUser && $intName !== '' && !\OC::$server->getUserManager()->userExists($intName))
594
+            || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) {
595
+            if($mapper->map($fdn, $intName, $uuid)) {
596
+                $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
597
+                $newlyMapped = true;
598
+                return $intName;
599
+            }
600
+        }
601
+        $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
602
+
603
+        $altName = $this->createAltInternalOwnCloudName($intName, $isUser);
604
+        if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
605
+            $newlyMapped = true;
606
+            return $altName;
607
+        }
608
+
609
+        //if everything else did not help..
610
+        \OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
611
+        return false;
612
+    }
613
+
614
+    /**
615
+     * gives back the user names as they are used ownClod internally
616
+     * @param array $ldapUsers as returned by fetchList()
617
+     * @return array an array with the user names to use in Nextcloud
618
+     *
619
+     * gives back the user names as they are used ownClod internally
620
+     */
621
+    public function nextcloudUserNames($ldapUsers) {
622
+        return $this->ldap2NextcloudNames($ldapUsers, true);
623
+    }
624
+
625
+    /**
626
+     * gives back the group names as they are used ownClod internally
627
+     * @param array $ldapGroups as returned by fetchList()
628
+     * @return array an array with the group names to use in Nextcloud
629
+     *
630
+     * gives back the group names as they are used ownClod internally
631
+     */
632
+    public function nextcloudGroupNames($ldapGroups) {
633
+        return $this->ldap2NextcloudNames($ldapGroups, false);
634
+    }
635
+
636
+    /**
637
+     * @param array $ldapObjects as returned by fetchList()
638
+     * @param bool $isUsers
639
+     * @return array
640
+     */
641
+    private function ldap2NextcloudNames($ldapObjects, $isUsers) {
642
+        if($isUsers) {
643
+            $nameAttribute = $this->connection->ldapUserDisplayName;
644
+            $sndAttribute  = $this->connection->ldapUserDisplayName2;
645
+        } else {
646
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
647
+        }
648
+        $nextcloudNames = array();
649
+
650
+        foreach($ldapObjects as $ldapObject) {
651
+            $nameByLDAP = null;
652
+            if(    isset($ldapObject[$nameAttribute])
653
+                && is_array($ldapObject[$nameAttribute])
654
+                && isset($ldapObject[$nameAttribute][0])
655
+            ) {
656
+                // might be set, but not necessarily. if so, we use it.
657
+                $nameByLDAP = $ldapObject[$nameAttribute][0];
658
+            }
659
+
660
+            $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
661
+            if($ncName) {
662
+                $nextcloudNames[] = $ncName;
663
+                if($isUsers) {
664
+                    //cache the user names so it does not need to be retrieved
665
+                    //again later (e.g. sharing dialogue).
666
+                    if(is_null($nameByLDAP)) {
667
+                        continue;
668
+                    }
669
+                    $sndName = isset($ldapObject[$sndAttribute][0])
670
+                        ? $ldapObject[$sndAttribute][0] : '';
671
+                    $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
672
+                }
673
+            }
674
+        }
675
+        return $nextcloudNames;
676
+    }
677
+
678
+    /**
679
+     * caches the user display name
680
+     * @param string $ocName the internal Nextcloud username
681
+     * @param string|false $home the home directory path
682
+     */
683
+    public function cacheUserHome($ocName, $home) {
684
+        $cacheKey = 'getHome'.$ocName;
685
+        $this->connection->writeToCache($cacheKey, $home);
686
+    }
687
+
688
+    /**
689
+     * caches a user as existing
690
+     * @param string $ocName the internal Nextcloud username
691
+     */
692
+    public function cacheUserExists($ocName) {
693
+        $this->connection->writeToCache('userExists'.$ocName, true);
694
+    }
695
+
696
+    /**
697
+     * caches the user display name
698
+     * @param string $ocName the internal Nextcloud username
699
+     * @param string $displayName the display name
700
+     * @param string $displayName2 the second display name
701
+     */
702
+    public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
703
+        $user = $this->userManager->get($ocName);
704
+        if($user === null) {
705
+            return;
706
+        }
707
+        $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
708
+        $cacheKeyTrunk = 'getDisplayName';
709
+        $this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
710
+    }
711
+
712
+    /**
713
+     * creates a unique name for internal Nextcloud use for users. Don't call it directly.
714
+     * @param string $name the display name of the object
715
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
716
+     *
717
+     * Instead of using this method directly, call
718
+     * createAltInternalOwnCloudName($name, true)
719
+     */
720
+    private function _createAltInternalOwnCloudNameForUsers($name) {
721
+        $attempts = 0;
722
+        //while loop is just a precaution. If a name is not generated within
723
+        //20 attempts, something else is very wrong. Avoids infinite loop.
724
+        while($attempts < 20){
725
+            $altName = $name . '_' . rand(1000,9999);
726
+            if(!\OC::$server->getUserManager()->userExists($altName)) {
727
+                return $altName;
728
+            }
729
+            $attempts++;
730
+        }
731
+        return false;
732
+    }
733
+
734
+    /**
735
+     * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
736
+     * @param string $name the display name of the object
737
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
738
+     *
739
+     * Instead of using this method directly, call
740
+     * createAltInternalOwnCloudName($name, false)
741
+     *
742
+     * Group names are also used as display names, so we do a sequential
743
+     * numbering, e.g. Developers_42 when there are 41 other groups called
744
+     * "Developers"
745
+     */
746
+    private function _createAltInternalOwnCloudNameForGroups($name) {
747
+        $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
748
+        if(!$usedNames || count($usedNames) === 0) {
749
+            $lastNo = 1; //will become name_2
750
+        } else {
751
+            natsort($usedNames);
752
+            $lastName = array_pop($usedNames);
753
+            $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
754
+        }
755
+        $altName = $name.'_'. (string)($lastNo+1);
756
+        unset($usedNames);
757
+
758
+        $attempts = 1;
759
+        while($attempts < 21){
760
+            // Check to be really sure it is unique
761
+            // while loop is just a precaution. If a name is not generated within
762
+            // 20 attempts, something else is very wrong. Avoids infinite loop.
763
+            if(!\OC::$server->getGroupManager()->groupExists($altName)) {
764
+                return $altName;
765
+            }
766
+            $altName = $name . '_' . ($lastNo + $attempts);
767
+            $attempts++;
768
+        }
769
+        return false;
770
+    }
771
+
772
+    /**
773
+     * creates a unique name for internal Nextcloud use.
774
+     * @param string $name the display name of the object
775
+     * @param boolean $isUser whether name should be created for a user (true) or a group (false)
776
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
777
+     */
778
+    private function createAltInternalOwnCloudName($name, $isUser) {
779
+        $originalTTL = $this->connection->ldapCacheTTL;
780
+        $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
781
+        if($isUser) {
782
+            $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
783
+        } else {
784
+            $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
785
+        }
786
+        $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
787
+
788
+        return $altName;
789
+    }
790
+
791
+    /**
792
+     * fetches a list of users according to a provided loginName and utilizing
793
+     * the login filter.
794
+     *
795
+     * @param string $loginName
796
+     * @param array $attributes optional, list of attributes to read
797
+     * @return array
798
+     */
799
+    public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
800
+        $loginName = $this->escapeFilterPart($loginName);
801
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
802
+        return $this->fetchListOfUsers($filter, $attributes);
803
+    }
804
+
805
+    /**
806
+     * counts the number of users according to a provided loginName and
807
+     * utilizing the login filter.
808
+     *
809
+     * @param string $loginName
810
+     * @return int
811
+     */
812
+    public function countUsersByLoginName($loginName) {
813
+        $loginName = $this->escapeFilterPart($loginName);
814
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
815
+        return $this->countUsers($filter);
816
+    }
817
+
818
+    /**
819
+     * @param string $filter
820
+     * @param string|string[] $attr
821
+     * @param int $limit
822
+     * @param int $offset
823
+     * @param bool $forceApplyAttributes
824
+     * @return array
825
+     */
826
+    public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
827
+        $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
828
+        $recordsToUpdate = $ldapRecords;
829
+        if(!$forceApplyAttributes) {
830
+            $isBackgroundJobModeAjax = $this->config
831
+                    ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
832
+            $recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
833
+                $newlyMapped = false;
834
+                $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
835
+                if(is_string($uid)) {
836
+                    $this->cacheUserExists($uid);
837
+                }
838
+                return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
839
+            });
840
+        }
841
+        $this->batchApplyUserAttributes($recordsToUpdate);
842
+        return $this->fetchList($ldapRecords, count($attr) > 1);
843
+    }
844
+
845
+    /**
846
+     * provided with an array of LDAP user records the method will fetch the
847
+     * user object and requests it to process the freshly fetched attributes and
848
+     * and their values
849
+     * @param array $ldapRecords
850
+     */
851
+    public function batchApplyUserAttributes(array $ldapRecords){
852
+        $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
853
+        foreach($ldapRecords as $userRecord) {
854
+            if(!isset($userRecord[$displayNameAttribute])) {
855
+                // displayName is obligatory
856
+                continue;
857
+            }
858
+            $ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
859
+            if($ocName === false) {
860
+                continue;
861
+            }
862
+            $user = $this->userManager->get($ocName);
863
+            if($user instanceof OfflineUser) {
864
+                $user->unmark();
865
+                $user = $this->userManager->get($ocName);
866
+            }
867
+            if ($user !== null) {
868
+                $user->processAttributes($userRecord);
869
+            } else {
870
+                \OC::$server->getLogger()->debug(
871
+                    "The ldap user manager returned null for $ocName",
872
+                    ['app'=>'user_ldap']
873
+                );
874
+            }
875
+        }
876
+    }
877
+
878
+    /**
879
+     * @param string $filter
880
+     * @param string|string[] $attr
881
+     * @param int $limit
882
+     * @param int $offset
883
+     * @return array
884
+     */
885
+    public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
886
+        return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), count($attr) > 1);
887
+    }
888
+
889
+    /**
890
+     * @param array $list
891
+     * @param bool $manyAttributes
892
+     * @return array
893
+     */
894
+    private function fetchList($list, $manyAttributes) {
895
+        if(is_array($list)) {
896
+            if($manyAttributes) {
897
+                return $list;
898
+            } else {
899
+                $list = array_reduce($list, function($carry, $item) {
900
+                    $attribute = array_keys($item)[0];
901
+                    $carry[] = $item[$attribute][0];
902
+                    return $carry;
903
+                }, array());
904
+                return array_unique($list, SORT_LOCALE_STRING);
905
+            }
906
+        }
907
+
908
+        //error cause actually, maybe throw an exception in future.
909
+        return array();
910
+    }
911
+
912
+    /**
913
+     * executes an LDAP search, optimized for Users
914
+     * @param string $filter the LDAP filter for the search
915
+     * @param string|string[] $attr optional, when a certain attribute shall be filtered out
916
+     * @param integer $limit
917
+     * @param integer $offset
918
+     * @return array with the search result
919
+     *
920
+     * Executes an LDAP search
921
+     */
922
+    public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
923
+        return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
924
+    }
925
+
926
+    /**
927
+     * @param string $filter
928
+     * @param string|string[] $attr
929
+     * @param int $limit
930
+     * @param int $offset
931
+     * @return false|int
932
+     */
933
+    public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
934
+        return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
935
+    }
936
+
937
+    /**
938
+     * executes an LDAP search, optimized for Groups
939
+     * @param string $filter the LDAP filter for the search
940
+     * @param string|string[] $attr optional, when a certain attribute shall be filtered out
941
+     * @param integer $limit
942
+     * @param integer $offset
943
+     * @return array with the search result
944
+     *
945
+     * Executes an LDAP search
946
+     */
947
+    public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
948
+        return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
949
+    }
950
+
951
+    /**
952
+     * returns the number of available groups
953
+     * @param string $filter the LDAP search filter
954
+     * @param string[] $attr optional
955
+     * @param int|null $limit
956
+     * @param int|null $offset
957
+     * @return int|bool
958
+     */
959
+    public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
960
+        return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
961
+    }
962
+
963
+    /**
964
+     * returns the number of available objects on the base DN
965
+     *
966
+     * @param int|null $limit
967
+     * @param int|null $offset
968
+     * @return int|bool
969
+     */
970
+    public function countObjects($limit = null, $offset = null) {
971
+        return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
972
+    }
973
+
974
+    /**
975
+     * Returns the LDAP handler
976
+     * @throws \OC\ServerNotAvailableException
977
+     */
978
+
979
+    /**
980
+     * @return mixed
981
+     * @throws \OC\ServerNotAvailableException
982
+     */
983
+    private function invokeLDAPMethod() {
984
+        $arguments = func_get_args();
985
+        $command = array_shift($arguments);
986
+        $cr = array_shift($arguments);
987
+        if (!method_exists($this->ldap, $command)) {
988
+            return null;
989
+        }
990
+        array_unshift($arguments, $cr);
991
+        // php no longer supports call-time pass-by-reference
992
+        // thus cannot support controlPagedResultResponse as the third argument
993
+        // is a reference
994
+        $doMethod = function () use ($command, &$arguments) {
995
+            if ($command == 'controlPagedResultResponse') {
996
+                throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
997
+            } else {
998
+                return call_user_func_array(array($this->ldap, $command), $arguments);
999
+            }
1000
+        };
1001
+        try {
1002
+            $ret = $doMethod();
1003
+        } catch (ServerNotAvailableException $e) {
1004
+            /* Server connection lost, attempt to reestablish it
1005 1005
 			 * Maybe implement exponential backoff?
1006 1006
 			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1007 1007
 			 */
1008
-			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", \OCP\Util::DEBUG);
1009
-			$this->connection->resetConnectionResource();
1010
-			$cr = $this->connection->getConnectionResource();
1011
-
1012
-			if(!$this->ldap->isResource($cr)) {
1013
-				// Seems like we didn't find any resource.
1014
-				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG);
1015
-				throw $e;
1016
-			}
1017
-
1018
-			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1019
-			$ret = $doMethod();
1020
-		}
1021
-		return $ret;
1022
-	}
1023
-
1024
-	/**
1025
-	 * retrieved. Results will according to the order in the array.
1026
-	 *
1027
-	 * @param $filter
1028
-	 * @param $base
1029
-	 * @param string[]|string|null $attr
1030
-	 * @param int $limit optional, maximum results to be counted
1031
-	 * @param int $offset optional, a starting point
1032
-	 * @return array|false array with the search result as first value and pagedSearchOK as
1033
-	 * second | false if not successful
1034
-	 * @throws ServerNotAvailableException
1035
-	 */
1036
-	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1037
-		if(!is_null($attr) && !is_array($attr)) {
1038
-			$attr = array(mb_strtolower($attr, 'UTF-8'));
1039
-		}
1040
-
1041
-		// See if we have a resource, in case not cancel with message
1042
-		$cr = $this->connection->getConnectionResource();
1043
-		if(!$this->ldap->isResource($cr)) {
1044
-			// Seems like we didn't find any resource.
1045
-			// Return an empty array just like before.
1046
-			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
1047
-			return false;
1048
-		}
1049
-
1050
-		//check whether paged search should be attempted
1051
-		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1052
-
1053
-		$linkResources = array_pad(array(), count($base), $cr);
1054
-		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1055
-		// cannot use $cr anymore, might have changed in the previous call!
1056
-		$error = $this->ldap->errno($this->connection->getConnectionResource());
1057
-		if(!is_array($sr) || $error !== 0) {
1058
-			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
1059
-			return false;
1060
-		}
1061
-
1062
-		return array($sr, $pagedSearchOK);
1063
-	}
1064
-
1065
-	/**
1066
-	 * processes an LDAP paged search operation
1067
-	 * @param array $sr the array containing the LDAP search resources
1068
-	 * @param string $filter the LDAP filter for the search
1069
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1070
-	 * @param int $iFoundItems number of results in the single search operation
1071
-	 * @param int $limit maximum results to be counted
1072
-	 * @param int $offset a starting point
1073
-	 * @param bool $pagedSearchOK whether a paged search has been executed
1074
-	 * @param bool $skipHandling required for paged search when cookies to
1075
-	 * prior results need to be gained
1076
-	 * @return bool cookie validity, true if we have more pages, false otherwise.
1077
-	 */
1078
-	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1079
-		$cookie = null;
1080
-		if($pagedSearchOK) {
1081
-			$cr = $this->connection->getConnectionResource();
1082
-			foreach($sr as $key => $res) {
1083
-				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1084
-					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1085
-				}
1086
-			}
1087
-
1088
-			//browsing through prior pages to get the cookie for the new one
1089
-			if($skipHandling) {
1090
-				return false;
1091
-			}
1092
-			// if count is bigger, then the server does not support
1093
-			// paged search. Instead, he did a normal search. We set a
1094
-			// flag here, so the callee knows how to deal with it.
1095
-			if($iFoundItems <= $limit) {
1096
-				$this->pagedSearchedSuccessful = true;
1097
-			}
1098
-		} else {
1099
-			if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1100
-				\OC::$server->getLogger()->debug(
1101
-					'Paged search was not available',
1102
-					[ 'app' => 'user_ldap' ]
1103
-				);
1104
-			}
1105
-		}
1106
-		/* ++ Fixing RHDS searches with pages with zero results ++
1008
+            \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", \OCP\Util::DEBUG);
1009
+            $this->connection->resetConnectionResource();
1010
+            $cr = $this->connection->getConnectionResource();
1011
+
1012
+            if(!$this->ldap->isResource($cr)) {
1013
+                // Seems like we didn't find any resource.
1014
+                \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG);
1015
+                throw $e;
1016
+            }
1017
+
1018
+            $arguments[0] = array_pad([], count($arguments[0]), $cr);
1019
+            $ret = $doMethod();
1020
+        }
1021
+        return $ret;
1022
+    }
1023
+
1024
+    /**
1025
+     * retrieved. Results will according to the order in the array.
1026
+     *
1027
+     * @param $filter
1028
+     * @param $base
1029
+     * @param string[]|string|null $attr
1030
+     * @param int $limit optional, maximum results to be counted
1031
+     * @param int $offset optional, a starting point
1032
+     * @return array|false array with the search result as first value and pagedSearchOK as
1033
+     * second | false if not successful
1034
+     * @throws ServerNotAvailableException
1035
+     */
1036
+    private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1037
+        if(!is_null($attr) && !is_array($attr)) {
1038
+            $attr = array(mb_strtolower($attr, 'UTF-8'));
1039
+        }
1040
+
1041
+        // See if we have a resource, in case not cancel with message
1042
+        $cr = $this->connection->getConnectionResource();
1043
+        if(!$this->ldap->isResource($cr)) {
1044
+            // Seems like we didn't find any resource.
1045
+            // Return an empty array just like before.
1046
+            \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
1047
+            return false;
1048
+        }
1049
+
1050
+        //check whether paged search should be attempted
1051
+        $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1052
+
1053
+        $linkResources = array_pad(array(), count($base), $cr);
1054
+        $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1055
+        // cannot use $cr anymore, might have changed in the previous call!
1056
+        $error = $this->ldap->errno($this->connection->getConnectionResource());
1057
+        if(!is_array($sr) || $error !== 0) {
1058
+            \OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
1059
+            return false;
1060
+        }
1061
+
1062
+        return array($sr, $pagedSearchOK);
1063
+    }
1064
+
1065
+    /**
1066
+     * processes an LDAP paged search operation
1067
+     * @param array $sr the array containing the LDAP search resources
1068
+     * @param string $filter the LDAP filter for the search
1069
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1070
+     * @param int $iFoundItems number of results in the single search operation
1071
+     * @param int $limit maximum results to be counted
1072
+     * @param int $offset a starting point
1073
+     * @param bool $pagedSearchOK whether a paged search has been executed
1074
+     * @param bool $skipHandling required for paged search when cookies to
1075
+     * prior results need to be gained
1076
+     * @return bool cookie validity, true if we have more pages, false otherwise.
1077
+     */
1078
+    private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1079
+        $cookie = null;
1080
+        if($pagedSearchOK) {
1081
+            $cr = $this->connection->getConnectionResource();
1082
+            foreach($sr as $key => $res) {
1083
+                if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1084
+                    $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1085
+                }
1086
+            }
1087
+
1088
+            //browsing through prior pages to get the cookie for the new one
1089
+            if($skipHandling) {
1090
+                return false;
1091
+            }
1092
+            // if count is bigger, then the server does not support
1093
+            // paged search. Instead, he did a normal search. We set a
1094
+            // flag here, so the callee knows how to deal with it.
1095
+            if($iFoundItems <= $limit) {
1096
+                $this->pagedSearchedSuccessful = true;
1097
+            }
1098
+        } else {
1099
+            if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1100
+                \OC::$server->getLogger()->debug(
1101
+                    'Paged search was not available',
1102
+                    [ 'app' => 'user_ldap' ]
1103
+                );
1104
+            }
1105
+        }
1106
+        /* ++ Fixing RHDS searches with pages with zero results ++
1107 1107
 		 * Return cookie status. If we don't have more pages, with RHDS
1108 1108
 		 * cookie is null, with openldap cookie is an empty string and
1109 1109
 		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1110 1110
 		 */
1111
-		return !empty($cookie) || $cookie === '0';
1112
-	}
1113
-
1114
-	/**
1115
-	 * executes an LDAP search, but counts the results only
1116
-	 *
1117
-	 * @param string $filter the LDAP filter for the search
1118
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1119
-	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1120
-	 * retrieved. Results will according to the order in the array.
1121
-	 * @param int $limit optional, maximum results to be counted
1122
-	 * @param int $offset optional, a starting point
1123
-	 * @param bool $skipHandling indicates whether the pages search operation is
1124
-	 * completed
1125
-	 * @return int|false Integer or false if the search could not be initialized
1126
-	 * @throws ServerNotAvailableException
1127
-	 */
1128
-	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1129
-		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
1130
-
1131
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1132
-		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1133
-			$limitPerPage = $limit;
1134
-		}
1135
-
1136
-		$counter = 0;
1137
-		$count = null;
1138
-		$this->connection->getConnectionResource();
1139
-
1140
-		do {
1141
-			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1142
-			if($search === false) {
1143
-				return $counter > 0 ? $counter : false;
1144
-			}
1145
-			list($sr, $pagedSearchOK) = $search;
1146
-
1147
-			/* ++ Fixing RHDS searches with pages with zero results ++
1111
+        return !empty($cookie) || $cookie === '0';
1112
+    }
1113
+
1114
+    /**
1115
+     * executes an LDAP search, but counts the results only
1116
+     *
1117
+     * @param string $filter the LDAP filter for the search
1118
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1119
+     * @param string|string[] $attr optional, array, one or more attributes that shall be
1120
+     * retrieved. Results will according to the order in the array.
1121
+     * @param int $limit optional, maximum results to be counted
1122
+     * @param int $offset optional, a starting point
1123
+     * @param bool $skipHandling indicates whether the pages search operation is
1124
+     * completed
1125
+     * @return int|false Integer or false if the search could not be initialized
1126
+     * @throws ServerNotAvailableException
1127
+     */
1128
+    private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1129
+        \OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
1130
+
1131
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1132
+        if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1133
+            $limitPerPage = $limit;
1134
+        }
1135
+
1136
+        $counter = 0;
1137
+        $count = null;
1138
+        $this->connection->getConnectionResource();
1139
+
1140
+        do {
1141
+            $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1142
+            if($search === false) {
1143
+                return $counter > 0 ? $counter : false;
1144
+            }
1145
+            list($sr, $pagedSearchOK) = $search;
1146
+
1147
+            /* ++ Fixing RHDS searches with pages with zero results ++
1148 1148
 			 * countEntriesInSearchResults() method signature changed
1149 1149
 			 * by removing $limit and &$hasHitLimit parameters
1150 1150
 			 */
1151
-			$count = $this->countEntriesInSearchResults($sr);
1152
-			$counter += $count;
1151
+            $count = $this->countEntriesInSearchResults($sr);
1152
+            $counter += $count;
1153 1153
 
1154
-			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1155
-										$offset, $pagedSearchOK, $skipHandling);
1156
-			$offset += $limitPerPage;
1157
-			/* ++ Fixing RHDS searches with pages with zero results ++
1154
+            $hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1155
+                                        $offset, $pagedSearchOK, $skipHandling);
1156
+            $offset += $limitPerPage;
1157
+            /* ++ Fixing RHDS searches with pages with zero results ++
1158 1158
 			 * Continue now depends on $hasMorePages value
1159 1159
 			 */
1160
-			$continue = $pagedSearchOK && $hasMorePages;
1161
-		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1162
-
1163
-		return $counter;
1164
-	}
1165
-
1166
-	/**
1167
-	 * @param array $searchResults
1168
-	 * @return int
1169
-	 */
1170
-	private function countEntriesInSearchResults($searchResults) {
1171
-		$counter = 0;
1172
-
1173
-		foreach($searchResults as $res) {
1174
-			$count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1175
-			$counter += $count;
1176
-		}
1177
-
1178
-		return $counter;
1179
-	}
1180
-
1181
-	/**
1182
-	 * Executes an LDAP search
1183
-	 *
1184
-	 * @param string $filter the LDAP filter for the search
1185
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1186
-	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1187
-	 * @param int $limit
1188
-	 * @param int $offset
1189
-	 * @param bool $skipHandling
1190
-	 * @return array with the search result
1191
-	 * @throws ServerNotAvailableException
1192
-	 */
1193
-	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1194
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1195
-		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1196
-			$limitPerPage = $limit;
1197
-		}
1198
-
1199
-		/* ++ Fixing RHDS searches with pages with zero results ++
1160
+            $continue = $pagedSearchOK && $hasMorePages;
1161
+        } while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1162
+
1163
+        return $counter;
1164
+    }
1165
+
1166
+    /**
1167
+     * @param array $searchResults
1168
+     * @return int
1169
+     */
1170
+    private function countEntriesInSearchResults($searchResults) {
1171
+        $counter = 0;
1172
+
1173
+        foreach($searchResults as $res) {
1174
+            $count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1175
+            $counter += $count;
1176
+        }
1177
+
1178
+        return $counter;
1179
+    }
1180
+
1181
+    /**
1182
+     * Executes an LDAP search
1183
+     *
1184
+     * @param string $filter the LDAP filter for the search
1185
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1186
+     * @param string|string[] $attr optional, array, one or more attributes that shall be
1187
+     * @param int $limit
1188
+     * @param int $offset
1189
+     * @param bool $skipHandling
1190
+     * @return array with the search result
1191
+     * @throws ServerNotAvailableException
1192
+     */
1193
+    public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1194
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1195
+        if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1196
+            $limitPerPage = $limit;
1197
+        }
1198
+
1199
+        /* ++ Fixing RHDS searches with pages with zero results ++
1200 1200
 		 * As we can have pages with zero results and/or pages with less
1201 1201
 		 * than $limit results but with a still valid server 'cookie',
1202 1202
 		 * loops through until we get $continue equals true and
1203 1203
 		 * $findings['count'] < $limit
1204 1204
 		 */
1205
-		$findings = [];
1206
-		$savedoffset = $offset;
1207
-		do {
1208
-			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1209
-			if($search === false) {
1210
-				return [];
1211
-			}
1212
-			list($sr, $pagedSearchOK) = $search;
1213
-			$cr = $this->connection->getConnectionResource();
1214
-
1215
-			if($skipHandling) {
1216
-				//i.e. result do not need to be fetched, we just need the cookie
1217
-				//thus pass 1 or any other value as $iFoundItems because it is not
1218
-				//used
1219
-				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1220
-								$offset, $pagedSearchOK,
1221
-								$skipHandling);
1222
-				return array();
1223
-			}
1224
-
1225
-			$iFoundItems = 0;
1226
-			foreach($sr as $res) {
1227
-				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1228
-				$iFoundItems = max($iFoundItems, $findings['count']);
1229
-				unset($findings['count']);
1230
-			}
1231
-
1232
-			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1233
-				$limitPerPage, $offset, $pagedSearchOK,
1234
-										$skipHandling);
1235
-			$offset += $limitPerPage;
1236
-		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1237
-		// reseting offset
1238
-		$offset = $savedoffset;
1239
-
1240
-		// if we're here, probably no connection resource is returned.
1241
-		// to make Nextcloud behave nicely, we simply give back an empty array.
1242
-		if(is_null($findings)) {
1243
-			return array();
1244
-		}
1245
-
1246
-		if(!is_null($attr)) {
1247
-			$selection = [];
1248
-			$i = 0;
1249
-			foreach($findings as $item) {
1250
-				if(!is_array($item)) {
1251
-					continue;
1252
-				}
1253
-				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1254
-				foreach($attr as $key) {
1255
-					if(isset($item[$key])) {
1256
-						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1257
-							unset($item[$key]['count']);
1258
-						}
1259
-						if($key !== 'dn') {
1260
-							if($this->resemblesDN($key)) {
1261
-								$selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1262
-							} else if($key === 'objectguid' || $key === 'guid') {
1263
-								$selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1264
-							} else {
1265
-								$selection[$i][$key] = $item[$key];
1266
-							}
1267
-						} else {
1268
-							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1269
-						}
1270
-					}
1271
-
1272
-				}
1273
-				$i++;
1274
-			}
1275
-			$findings = $selection;
1276
-		}
1277
-		//we slice the findings, when
1278
-		//a) paged search unsuccessful, though attempted
1279
-		//b) no paged search, but limit set
1280
-		if((!$this->getPagedSearchResultState()
1281
-			&& $pagedSearchOK)
1282
-			|| (
1283
-				!$pagedSearchOK
1284
-				&& !is_null($limit)
1285
-			)
1286
-		) {
1287
-			$findings = array_slice($findings, (int)$offset, $limit);
1288
-		}
1289
-		return $findings;
1290
-	}
1291
-
1292
-	/**
1293
-	 * @param string $name
1294
-	 * @return bool|mixed|string
1295
-	 */
1296
-	public function sanitizeUsername($name) {
1297
-		if($this->connection->ldapIgnoreNamingRules) {
1298
-			return trim($name);
1299
-		}
1300
-
1301
-		// Transliteration
1302
-		// latin characters to ASCII
1303
-		$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1304
-
1305
-		// Replacements
1306
-		$name = str_replace(' ', '_', $name);
1307
-
1308
-		// Every remaining disallowed characters will be removed
1309
-		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1310
-
1311
-		return $name;
1312
-	}
1313
-
1314
-	/**
1315
-	* escapes (user provided) parts for LDAP filter
1316
-	* @param string $input, the provided value
1317
-	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1318
-	* @return string the escaped string
1319
-	*/
1320
-	public function escapeFilterPart($input, $allowAsterisk = false) {
1321
-		$asterisk = '';
1322
-		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1323
-			$asterisk = '*';
1324
-			$input = mb_substr($input, 1, null, 'UTF-8');
1325
-		}
1326
-		$search  = array('*', '\\', '(', ')');
1327
-		$replace = array('\\*', '\\\\', '\\(', '\\)');
1328
-		return $asterisk . str_replace($search, $replace, $input);
1329
-	}
1330
-
1331
-	/**
1332
-	 * combines the input filters with AND
1333
-	 * @param string[] $filters the filters to connect
1334
-	 * @return string the combined filter
1335
-	 */
1336
-	public function combineFilterWithAnd($filters) {
1337
-		return $this->combineFilter($filters, '&');
1338
-	}
1339
-
1340
-	/**
1341
-	 * combines the input filters with OR
1342
-	 * @param string[] $filters the filters to connect
1343
-	 * @return string the combined filter
1344
-	 * Combines Filter arguments with OR
1345
-	 */
1346
-	public function combineFilterWithOr($filters) {
1347
-		return $this->combineFilter($filters, '|');
1348
-	}
1349
-
1350
-	/**
1351
-	 * combines the input filters with given operator
1352
-	 * @param string[] $filters the filters to connect
1353
-	 * @param string $operator either & or |
1354
-	 * @return string the combined filter
1355
-	 */
1356
-	private function combineFilter($filters, $operator) {
1357
-		$combinedFilter = '('.$operator;
1358
-		foreach($filters as $filter) {
1359
-			if ($filter !== '' && $filter[0] !== '(') {
1360
-				$filter = '('.$filter.')';
1361
-			}
1362
-			$combinedFilter.=$filter;
1363
-		}
1364
-		$combinedFilter.=')';
1365
-		return $combinedFilter;
1366
-	}
1367
-
1368
-	/**
1369
-	 * creates a filter part for to perform search for users
1370
-	 * @param string $search the search term
1371
-	 * @return string the final filter part to use in LDAP searches
1372
-	 */
1373
-	public function getFilterPartForUserSearch($search) {
1374
-		return $this->getFilterPartForSearch($search,
1375
-			$this->connection->ldapAttributesForUserSearch,
1376
-			$this->connection->ldapUserDisplayName);
1377
-	}
1378
-
1379
-	/**
1380
-	 * creates a filter part for to perform search for groups
1381
-	 * @param string $search the search term
1382
-	 * @return string the final filter part to use in LDAP searches
1383
-	 */
1384
-	public function getFilterPartForGroupSearch($search) {
1385
-		return $this->getFilterPartForSearch($search,
1386
-			$this->connection->ldapAttributesForGroupSearch,
1387
-			$this->connection->ldapGroupDisplayName);
1388
-	}
1389
-
1390
-	/**
1391
-	 * creates a filter part for searches by splitting up the given search
1392
-	 * string into single words
1393
-	 * @param string $search the search term
1394
-	 * @param string[] $searchAttributes needs to have at least two attributes,
1395
-	 * otherwise it does not make sense :)
1396
-	 * @return string the final filter part to use in LDAP searches
1397
-	 * @throws \Exception
1398
-	 */
1399
-	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1400
-		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1401
-			throw new \Exception('searchAttributes must be an array with at least two string');
1402
-		}
1403
-		$searchWords = explode(' ', trim($search));
1404
-		$wordFilters = array();
1405
-		foreach($searchWords as $word) {
1406
-			$word = $this->prepareSearchTerm($word);
1407
-			//every word needs to appear at least once
1408
-			$wordMatchOneAttrFilters = array();
1409
-			foreach($searchAttributes as $attr) {
1410
-				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1411
-			}
1412
-			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1413
-		}
1414
-		return $this->combineFilterWithAnd($wordFilters);
1415
-	}
1416
-
1417
-	/**
1418
-	 * creates a filter part for searches
1419
-	 * @param string $search the search term
1420
-	 * @param string[]|null $searchAttributes
1421
-	 * @param string $fallbackAttribute a fallback attribute in case the user
1422
-	 * did not define search attributes. Typically the display name attribute.
1423
-	 * @return string the final filter part to use in LDAP searches
1424
-	 */
1425
-	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1426
-		$filter = array();
1427
-		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1428
-		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1429
-			try {
1430
-				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1431
-			} catch(\Exception $e) {
1432
-				\OCP\Util::writeLog(
1433
-					'user_ldap',
1434
-					'Creating advanced filter for search failed, falling back to simple method.',
1435
-					\OCP\Util::INFO
1436
-				);
1437
-			}
1438
-		}
1439
-
1440
-		$search = $this->prepareSearchTerm($search);
1441
-		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1442
-			if ($fallbackAttribute === '') {
1443
-				return '';
1444
-			}
1445
-			$filter[] = $fallbackAttribute . '=' . $search;
1446
-		} else {
1447
-			foreach($searchAttributes as $attribute) {
1448
-				$filter[] = $attribute . '=' . $search;
1449
-			}
1450
-		}
1451
-		if(count($filter) === 1) {
1452
-			return '('.$filter[0].')';
1453
-		}
1454
-		return $this->combineFilterWithOr($filter);
1455
-	}
1456
-
1457
-	/**
1458
-	 * returns the search term depending on whether we are allowed
1459
-	 * list users found by ldap with the current input appended by
1460
-	 * a *
1461
-	 * @return string
1462
-	 */
1463
-	private function prepareSearchTerm($term) {
1464
-		$config = \OC::$server->getConfig();
1465
-
1466
-		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1467
-
1468
-		$result = $term;
1469
-		if ($term === '') {
1470
-			$result = '*';
1471
-		} else if ($allowEnum !== 'no') {
1472
-			$result = $term . '*';
1473
-		}
1474
-		return $result;
1475
-	}
1476
-
1477
-	/**
1478
-	 * returns the filter used for counting users
1479
-	 * @return string
1480
-	 */
1481
-	public function getFilterForUserCount() {
1482
-		$filter = $this->combineFilterWithAnd(array(
1483
-			$this->connection->ldapUserFilter,
1484
-			$this->connection->ldapUserDisplayName . '=*'
1485
-		));
1486
-
1487
-		return $filter;
1488
-	}
1489
-
1490
-	/**
1491
-	 * @param string $name
1492
-	 * @param string $password
1493
-	 * @return bool
1494
-	 */
1495
-	public function areCredentialsValid($name, $password) {
1496
-		$name = $this->helper->DNasBaseParameter($name);
1497
-		$testConnection = clone $this->connection;
1498
-		$credentials = array(
1499
-			'ldapAgentName' => $name,
1500
-			'ldapAgentPassword' => $password
1501
-		);
1502
-		if(!$testConnection->setConfiguration($credentials)) {
1503
-			return false;
1504
-		}
1505
-		return $testConnection->bind();
1506
-	}
1507
-
1508
-	/**
1509
-	 * reverse lookup of a DN given a known UUID
1510
-	 *
1511
-	 * @param string $uuid
1512
-	 * @return string
1513
-	 * @throws \Exception
1514
-	 */
1515
-	public function getUserDnByUuid($uuid) {
1516
-		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1517
-		$filter       = $this->connection->ldapUserFilter;
1518
-		$base         = $this->connection->ldapBaseUsers;
1519
-
1520
-		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1521
-			// Sacrebleu! The UUID attribute is unknown :( We need first an
1522
-			// existing DN to be able to reliably detect it.
1523
-			$result = $this->search($filter, $base, ['dn'], 1);
1524
-			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1525
-				throw new \Exception('Cannot determine UUID attribute');
1526
-			}
1527
-			$dn = $result[0]['dn'][0];
1528
-			if(!$this->detectUuidAttribute($dn, true)) {
1529
-				throw new \Exception('Cannot determine UUID attribute');
1530
-			}
1531
-		} else {
1532
-			// The UUID attribute is either known or an override is given.
1533
-			// By calling this method we ensure that $this->connection->$uuidAttr
1534
-			// is definitely set
1535
-			if(!$this->detectUuidAttribute('', true)) {
1536
-				throw new \Exception('Cannot determine UUID attribute');
1537
-			}
1538
-		}
1539
-
1540
-		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1541
-		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1542
-			$uuid = $this->formatGuid2ForFilterUser($uuid);
1543
-		}
1544
-
1545
-		$filter = $uuidAttr . '=' . $uuid;
1546
-		$result = $this->searchUsers($filter, ['dn'], 2);
1547
-		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1548
-			// we put the count into account to make sure that this is
1549
-			// really unique
1550
-			return $result[0]['dn'][0];
1551
-		}
1552
-
1553
-		throw new \Exception('Cannot determine UUID attribute');
1554
-	}
1555
-
1556
-	/**
1557
-	 * auto-detects the directory's UUID attribute
1558
-	 *
1559
-	 * @param string $dn a known DN used to check against
1560
-	 * @param bool $isUser
1561
-	 * @param bool $force the detection should be run, even if it is not set to auto
1562
-	 * @param array|null $ldapRecord
1563
-	 * @return bool true on success, false otherwise
1564
-	 */
1565
-	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1566
-		if($isUser) {
1567
-			$uuidAttr     = 'ldapUuidUserAttribute';
1568
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1569
-		} else {
1570
-			$uuidAttr     = 'ldapUuidGroupAttribute';
1571
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1572
-		}
1573
-
1574
-		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1575
-			return true;
1576
-		}
1577
-
1578
-		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1579
-			$this->connection->$uuidAttr = $uuidOverride;
1580
-			return true;
1581
-		}
1582
-
1583
-		foreach(self::UUID_ATTRIBUTES as $attribute) {
1584
-			if($ldapRecord !== null) {
1585
-				// we have the info from LDAP already, we don't need to talk to the server again
1586
-				if(isset($ldapRecord[$attribute])) {
1587
-					$this->connection->$uuidAttr = $attribute;
1588
-					return true;
1589
-				} else {
1590
-					continue;
1591
-				}
1592
-			}
1593
-
1594
-			$value = $this->readAttribute($dn, $attribute);
1595
-			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1596
-				\OCP\Util::writeLog('user_ldap',
1597
-									'Setting '.$attribute.' as '.$uuidAttr,
1598
-									\OCP\Util::DEBUG);
1599
-				$this->connection->$uuidAttr = $attribute;
1600
-				return true;
1601
-			}
1602
-		}
1603
-		\OCP\Util::writeLog('user_ldap',
1604
-							'Could not autodetect the UUID attribute',
1605
-							\OCP\Util::ERROR);
1606
-
1607
-		return false;
1608
-	}
1609
-
1610
-	/**
1611
-	 * @param string $dn
1612
-	 * @param bool $isUser
1613
-	 * @param null $ldapRecord
1614
-	 * @return bool|string
1615
-	 */
1616
-	public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1617
-		if($isUser) {
1618
-			$uuidAttr     = 'ldapUuidUserAttribute';
1619
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1620
-		} else {
1621
-			$uuidAttr     = 'ldapUuidGroupAttribute';
1622
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1623
-		}
1624
-
1625
-		$uuid = false;
1626
-		if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1627
-			$attr = $this->connection->$uuidAttr;
1628
-			$uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1629
-			if( !is_array($uuid)
1630
-				&& $uuidOverride !== ''
1631
-				&& $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1632
-			{
1633
-				$uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1634
-					? $ldapRecord[$this->connection->$uuidAttr]
1635
-					: $this->readAttribute($dn, $this->connection->$uuidAttr);
1636
-			}
1637
-			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1638
-				$uuid = $uuid[0];
1639
-			}
1640
-		}
1641
-
1642
-		return $uuid;
1643
-	}
1644
-
1645
-	/**
1646
-	 * converts a binary ObjectGUID into a string representation
1647
-	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1648
-	 * @return string
1649
-	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1650
-	 */
1651
-	private function convertObjectGUID2Str($oguid) {
1652
-		$hex_guid = bin2hex($oguid);
1653
-		$hex_guid_to_guid_str = '';
1654
-		for($k = 1; $k <= 4; ++$k) {
1655
-			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1656
-		}
1657
-		$hex_guid_to_guid_str .= '-';
1658
-		for($k = 1; $k <= 2; ++$k) {
1659
-			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1660
-		}
1661
-		$hex_guid_to_guid_str .= '-';
1662
-		for($k = 1; $k <= 2; ++$k) {
1663
-			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1664
-		}
1665
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1666
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1667
-
1668
-		return strtoupper($hex_guid_to_guid_str);
1669
-	}
1670
-
1671
-	/**
1672
-	 * the first three blocks of the string-converted GUID happen to be in
1673
-	 * reverse order. In order to use it in a filter, this needs to be
1674
-	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1675
-	 * to every two hax figures.
1676
-	 *
1677
-	 * If an invalid string is passed, it will be returned without change.
1678
-	 *
1679
-	 * @param string $guid
1680
-	 * @return string
1681
-	 */
1682
-	public function formatGuid2ForFilterUser($guid) {
1683
-		if(!is_string($guid)) {
1684
-			throw new \InvalidArgumentException('String expected');
1685
-		}
1686
-		$blocks = explode('-', $guid);
1687
-		if(count($blocks) !== 5) {
1688
-			/*
1205
+        $findings = [];
1206
+        $savedoffset = $offset;
1207
+        do {
1208
+            $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1209
+            if($search === false) {
1210
+                return [];
1211
+            }
1212
+            list($sr, $pagedSearchOK) = $search;
1213
+            $cr = $this->connection->getConnectionResource();
1214
+
1215
+            if($skipHandling) {
1216
+                //i.e. result do not need to be fetched, we just need the cookie
1217
+                //thus pass 1 or any other value as $iFoundItems because it is not
1218
+                //used
1219
+                $this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1220
+                                $offset, $pagedSearchOK,
1221
+                                $skipHandling);
1222
+                return array();
1223
+            }
1224
+
1225
+            $iFoundItems = 0;
1226
+            foreach($sr as $res) {
1227
+                $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1228
+                $iFoundItems = max($iFoundItems, $findings['count']);
1229
+                unset($findings['count']);
1230
+            }
1231
+
1232
+            $continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1233
+                $limitPerPage, $offset, $pagedSearchOK,
1234
+                                        $skipHandling);
1235
+            $offset += $limitPerPage;
1236
+        } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1237
+        // reseting offset
1238
+        $offset = $savedoffset;
1239
+
1240
+        // if we're here, probably no connection resource is returned.
1241
+        // to make Nextcloud behave nicely, we simply give back an empty array.
1242
+        if(is_null($findings)) {
1243
+            return array();
1244
+        }
1245
+
1246
+        if(!is_null($attr)) {
1247
+            $selection = [];
1248
+            $i = 0;
1249
+            foreach($findings as $item) {
1250
+                if(!is_array($item)) {
1251
+                    continue;
1252
+                }
1253
+                $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1254
+                foreach($attr as $key) {
1255
+                    if(isset($item[$key])) {
1256
+                        if(is_array($item[$key]) && isset($item[$key]['count'])) {
1257
+                            unset($item[$key]['count']);
1258
+                        }
1259
+                        if($key !== 'dn') {
1260
+                            if($this->resemblesDN($key)) {
1261
+                                $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1262
+                            } else if($key === 'objectguid' || $key === 'guid') {
1263
+                                $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1264
+                            } else {
1265
+                                $selection[$i][$key] = $item[$key];
1266
+                            }
1267
+                        } else {
1268
+                            $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1269
+                        }
1270
+                    }
1271
+
1272
+                }
1273
+                $i++;
1274
+            }
1275
+            $findings = $selection;
1276
+        }
1277
+        //we slice the findings, when
1278
+        //a) paged search unsuccessful, though attempted
1279
+        //b) no paged search, but limit set
1280
+        if((!$this->getPagedSearchResultState()
1281
+            && $pagedSearchOK)
1282
+            || (
1283
+                !$pagedSearchOK
1284
+                && !is_null($limit)
1285
+            )
1286
+        ) {
1287
+            $findings = array_slice($findings, (int)$offset, $limit);
1288
+        }
1289
+        return $findings;
1290
+    }
1291
+
1292
+    /**
1293
+     * @param string $name
1294
+     * @return bool|mixed|string
1295
+     */
1296
+    public function sanitizeUsername($name) {
1297
+        if($this->connection->ldapIgnoreNamingRules) {
1298
+            return trim($name);
1299
+        }
1300
+
1301
+        // Transliteration
1302
+        // latin characters to ASCII
1303
+        $name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1304
+
1305
+        // Replacements
1306
+        $name = str_replace(' ', '_', $name);
1307
+
1308
+        // Every remaining disallowed characters will be removed
1309
+        $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1310
+
1311
+        return $name;
1312
+    }
1313
+
1314
+    /**
1315
+     * escapes (user provided) parts for LDAP filter
1316
+     * @param string $input, the provided value
1317
+     * @param bool $allowAsterisk whether in * at the beginning should be preserved
1318
+     * @return string the escaped string
1319
+     */
1320
+    public function escapeFilterPart($input, $allowAsterisk = false) {
1321
+        $asterisk = '';
1322
+        if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1323
+            $asterisk = '*';
1324
+            $input = mb_substr($input, 1, null, 'UTF-8');
1325
+        }
1326
+        $search  = array('*', '\\', '(', ')');
1327
+        $replace = array('\\*', '\\\\', '\\(', '\\)');
1328
+        return $asterisk . str_replace($search, $replace, $input);
1329
+    }
1330
+
1331
+    /**
1332
+     * combines the input filters with AND
1333
+     * @param string[] $filters the filters to connect
1334
+     * @return string the combined filter
1335
+     */
1336
+    public function combineFilterWithAnd($filters) {
1337
+        return $this->combineFilter($filters, '&');
1338
+    }
1339
+
1340
+    /**
1341
+     * combines the input filters with OR
1342
+     * @param string[] $filters the filters to connect
1343
+     * @return string the combined filter
1344
+     * Combines Filter arguments with OR
1345
+     */
1346
+    public function combineFilterWithOr($filters) {
1347
+        return $this->combineFilter($filters, '|');
1348
+    }
1349
+
1350
+    /**
1351
+     * combines the input filters with given operator
1352
+     * @param string[] $filters the filters to connect
1353
+     * @param string $operator either & or |
1354
+     * @return string the combined filter
1355
+     */
1356
+    private function combineFilter($filters, $operator) {
1357
+        $combinedFilter = '('.$operator;
1358
+        foreach($filters as $filter) {
1359
+            if ($filter !== '' && $filter[0] !== '(') {
1360
+                $filter = '('.$filter.')';
1361
+            }
1362
+            $combinedFilter.=$filter;
1363
+        }
1364
+        $combinedFilter.=')';
1365
+        return $combinedFilter;
1366
+    }
1367
+
1368
+    /**
1369
+     * creates a filter part for to perform search for users
1370
+     * @param string $search the search term
1371
+     * @return string the final filter part to use in LDAP searches
1372
+     */
1373
+    public function getFilterPartForUserSearch($search) {
1374
+        return $this->getFilterPartForSearch($search,
1375
+            $this->connection->ldapAttributesForUserSearch,
1376
+            $this->connection->ldapUserDisplayName);
1377
+    }
1378
+
1379
+    /**
1380
+     * creates a filter part for to perform search for groups
1381
+     * @param string $search the search term
1382
+     * @return string the final filter part to use in LDAP searches
1383
+     */
1384
+    public function getFilterPartForGroupSearch($search) {
1385
+        return $this->getFilterPartForSearch($search,
1386
+            $this->connection->ldapAttributesForGroupSearch,
1387
+            $this->connection->ldapGroupDisplayName);
1388
+    }
1389
+
1390
+    /**
1391
+     * creates a filter part for searches by splitting up the given search
1392
+     * string into single words
1393
+     * @param string $search the search term
1394
+     * @param string[] $searchAttributes needs to have at least two attributes,
1395
+     * otherwise it does not make sense :)
1396
+     * @return string the final filter part to use in LDAP searches
1397
+     * @throws \Exception
1398
+     */
1399
+    private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1400
+        if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1401
+            throw new \Exception('searchAttributes must be an array with at least two string');
1402
+        }
1403
+        $searchWords = explode(' ', trim($search));
1404
+        $wordFilters = array();
1405
+        foreach($searchWords as $word) {
1406
+            $word = $this->prepareSearchTerm($word);
1407
+            //every word needs to appear at least once
1408
+            $wordMatchOneAttrFilters = array();
1409
+            foreach($searchAttributes as $attr) {
1410
+                $wordMatchOneAttrFilters[] = $attr . '=' . $word;
1411
+            }
1412
+            $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1413
+        }
1414
+        return $this->combineFilterWithAnd($wordFilters);
1415
+    }
1416
+
1417
+    /**
1418
+     * creates a filter part for searches
1419
+     * @param string $search the search term
1420
+     * @param string[]|null $searchAttributes
1421
+     * @param string $fallbackAttribute a fallback attribute in case the user
1422
+     * did not define search attributes. Typically the display name attribute.
1423
+     * @return string the final filter part to use in LDAP searches
1424
+     */
1425
+    private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1426
+        $filter = array();
1427
+        $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1428
+        if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1429
+            try {
1430
+                return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1431
+            } catch(\Exception $e) {
1432
+                \OCP\Util::writeLog(
1433
+                    'user_ldap',
1434
+                    'Creating advanced filter for search failed, falling back to simple method.',
1435
+                    \OCP\Util::INFO
1436
+                );
1437
+            }
1438
+        }
1439
+
1440
+        $search = $this->prepareSearchTerm($search);
1441
+        if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1442
+            if ($fallbackAttribute === '') {
1443
+                return '';
1444
+            }
1445
+            $filter[] = $fallbackAttribute . '=' . $search;
1446
+        } else {
1447
+            foreach($searchAttributes as $attribute) {
1448
+                $filter[] = $attribute . '=' . $search;
1449
+            }
1450
+        }
1451
+        if(count($filter) === 1) {
1452
+            return '('.$filter[0].')';
1453
+        }
1454
+        return $this->combineFilterWithOr($filter);
1455
+    }
1456
+
1457
+    /**
1458
+     * returns the search term depending on whether we are allowed
1459
+     * list users found by ldap with the current input appended by
1460
+     * a *
1461
+     * @return string
1462
+     */
1463
+    private function prepareSearchTerm($term) {
1464
+        $config = \OC::$server->getConfig();
1465
+
1466
+        $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1467
+
1468
+        $result = $term;
1469
+        if ($term === '') {
1470
+            $result = '*';
1471
+        } else if ($allowEnum !== 'no') {
1472
+            $result = $term . '*';
1473
+        }
1474
+        return $result;
1475
+    }
1476
+
1477
+    /**
1478
+     * returns the filter used for counting users
1479
+     * @return string
1480
+     */
1481
+    public function getFilterForUserCount() {
1482
+        $filter = $this->combineFilterWithAnd(array(
1483
+            $this->connection->ldapUserFilter,
1484
+            $this->connection->ldapUserDisplayName . '=*'
1485
+        ));
1486
+
1487
+        return $filter;
1488
+    }
1489
+
1490
+    /**
1491
+     * @param string $name
1492
+     * @param string $password
1493
+     * @return bool
1494
+     */
1495
+    public function areCredentialsValid($name, $password) {
1496
+        $name = $this->helper->DNasBaseParameter($name);
1497
+        $testConnection = clone $this->connection;
1498
+        $credentials = array(
1499
+            'ldapAgentName' => $name,
1500
+            'ldapAgentPassword' => $password
1501
+        );
1502
+        if(!$testConnection->setConfiguration($credentials)) {
1503
+            return false;
1504
+        }
1505
+        return $testConnection->bind();
1506
+    }
1507
+
1508
+    /**
1509
+     * reverse lookup of a DN given a known UUID
1510
+     *
1511
+     * @param string $uuid
1512
+     * @return string
1513
+     * @throws \Exception
1514
+     */
1515
+    public function getUserDnByUuid($uuid) {
1516
+        $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1517
+        $filter       = $this->connection->ldapUserFilter;
1518
+        $base         = $this->connection->ldapBaseUsers;
1519
+
1520
+        if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1521
+            // Sacrebleu! The UUID attribute is unknown :( We need first an
1522
+            // existing DN to be able to reliably detect it.
1523
+            $result = $this->search($filter, $base, ['dn'], 1);
1524
+            if(!isset($result[0]) || !isset($result[0]['dn'])) {
1525
+                throw new \Exception('Cannot determine UUID attribute');
1526
+            }
1527
+            $dn = $result[0]['dn'][0];
1528
+            if(!$this->detectUuidAttribute($dn, true)) {
1529
+                throw new \Exception('Cannot determine UUID attribute');
1530
+            }
1531
+        } else {
1532
+            // The UUID attribute is either known or an override is given.
1533
+            // By calling this method we ensure that $this->connection->$uuidAttr
1534
+            // is definitely set
1535
+            if(!$this->detectUuidAttribute('', true)) {
1536
+                throw new \Exception('Cannot determine UUID attribute');
1537
+            }
1538
+        }
1539
+
1540
+        $uuidAttr = $this->connection->ldapUuidUserAttribute;
1541
+        if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1542
+            $uuid = $this->formatGuid2ForFilterUser($uuid);
1543
+        }
1544
+
1545
+        $filter = $uuidAttr . '=' . $uuid;
1546
+        $result = $this->searchUsers($filter, ['dn'], 2);
1547
+        if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1548
+            // we put the count into account to make sure that this is
1549
+            // really unique
1550
+            return $result[0]['dn'][0];
1551
+        }
1552
+
1553
+        throw new \Exception('Cannot determine UUID attribute');
1554
+    }
1555
+
1556
+    /**
1557
+     * auto-detects the directory's UUID attribute
1558
+     *
1559
+     * @param string $dn a known DN used to check against
1560
+     * @param bool $isUser
1561
+     * @param bool $force the detection should be run, even if it is not set to auto
1562
+     * @param array|null $ldapRecord
1563
+     * @return bool true on success, false otherwise
1564
+     */
1565
+    private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1566
+        if($isUser) {
1567
+            $uuidAttr     = 'ldapUuidUserAttribute';
1568
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1569
+        } else {
1570
+            $uuidAttr     = 'ldapUuidGroupAttribute';
1571
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1572
+        }
1573
+
1574
+        if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1575
+            return true;
1576
+        }
1577
+
1578
+        if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1579
+            $this->connection->$uuidAttr = $uuidOverride;
1580
+            return true;
1581
+        }
1582
+
1583
+        foreach(self::UUID_ATTRIBUTES as $attribute) {
1584
+            if($ldapRecord !== null) {
1585
+                // we have the info from LDAP already, we don't need to talk to the server again
1586
+                if(isset($ldapRecord[$attribute])) {
1587
+                    $this->connection->$uuidAttr = $attribute;
1588
+                    return true;
1589
+                } else {
1590
+                    continue;
1591
+                }
1592
+            }
1593
+
1594
+            $value = $this->readAttribute($dn, $attribute);
1595
+            if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1596
+                \OCP\Util::writeLog('user_ldap',
1597
+                                    'Setting '.$attribute.' as '.$uuidAttr,
1598
+                                    \OCP\Util::DEBUG);
1599
+                $this->connection->$uuidAttr = $attribute;
1600
+                return true;
1601
+            }
1602
+        }
1603
+        \OCP\Util::writeLog('user_ldap',
1604
+                            'Could not autodetect the UUID attribute',
1605
+                            \OCP\Util::ERROR);
1606
+
1607
+        return false;
1608
+    }
1609
+
1610
+    /**
1611
+     * @param string $dn
1612
+     * @param bool $isUser
1613
+     * @param null $ldapRecord
1614
+     * @return bool|string
1615
+     */
1616
+    public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1617
+        if($isUser) {
1618
+            $uuidAttr     = 'ldapUuidUserAttribute';
1619
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1620
+        } else {
1621
+            $uuidAttr     = 'ldapUuidGroupAttribute';
1622
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1623
+        }
1624
+
1625
+        $uuid = false;
1626
+        if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1627
+            $attr = $this->connection->$uuidAttr;
1628
+            $uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1629
+            if( !is_array($uuid)
1630
+                && $uuidOverride !== ''
1631
+                && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1632
+            {
1633
+                $uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1634
+                    ? $ldapRecord[$this->connection->$uuidAttr]
1635
+                    : $this->readAttribute($dn, $this->connection->$uuidAttr);
1636
+            }
1637
+            if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1638
+                $uuid = $uuid[0];
1639
+            }
1640
+        }
1641
+
1642
+        return $uuid;
1643
+    }
1644
+
1645
+    /**
1646
+     * converts a binary ObjectGUID into a string representation
1647
+     * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1648
+     * @return string
1649
+     * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1650
+     */
1651
+    private function convertObjectGUID2Str($oguid) {
1652
+        $hex_guid = bin2hex($oguid);
1653
+        $hex_guid_to_guid_str = '';
1654
+        for($k = 1; $k <= 4; ++$k) {
1655
+            $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1656
+        }
1657
+        $hex_guid_to_guid_str .= '-';
1658
+        for($k = 1; $k <= 2; ++$k) {
1659
+            $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1660
+        }
1661
+        $hex_guid_to_guid_str .= '-';
1662
+        for($k = 1; $k <= 2; ++$k) {
1663
+            $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1664
+        }
1665
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1666
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1667
+
1668
+        return strtoupper($hex_guid_to_guid_str);
1669
+    }
1670
+
1671
+    /**
1672
+     * the first three blocks of the string-converted GUID happen to be in
1673
+     * reverse order. In order to use it in a filter, this needs to be
1674
+     * corrected. Furthermore the dashes need to be replaced and \\ preprended
1675
+     * to every two hax figures.
1676
+     *
1677
+     * If an invalid string is passed, it will be returned without change.
1678
+     *
1679
+     * @param string $guid
1680
+     * @return string
1681
+     */
1682
+    public function formatGuid2ForFilterUser($guid) {
1683
+        if(!is_string($guid)) {
1684
+            throw new \InvalidArgumentException('String expected');
1685
+        }
1686
+        $blocks = explode('-', $guid);
1687
+        if(count($blocks) !== 5) {
1688
+            /*
1689 1689
 			 * Why not throw an Exception instead? This method is a utility
1690 1690
 			 * called only when trying to figure out whether a "missing" known
1691 1691
 			 * LDAP user was or was not renamed on the LDAP server. And this
@@ -1696,270 +1696,270 @@  discard block
 block discarded – undo
1696 1696
 			 * an exception here would kill the experience for a valid, acting
1697 1697
 			 * user. Instead we write a log message.
1698 1698
 			 */
1699
-			\OC::$server->getLogger()->info(
1700
-				'Passed string does not resemble a valid GUID. Known UUID ' .
1701
-				'({uuid}) probably does not match UUID configuration.',
1702
-				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1703
-			);
1704
-			return $guid;
1705
-		}
1706
-		for($i=0; $i < 3; $i++) {
1707
-			$pairs = str_split($blocks[$i], 2);
1708
-			$pairs = array_reverse($pairs);
1709
-			$blocks[$i] = implode('', $pairs);
1710
-		}
1711
-		for($i=0; $i < 5; $i++) {
1712
-			$pairs = str_split($blocks[$i], 2);
1713
-			$blocks[$i] = '\\' . implode('\\', $pairs);
1714
-		}
1715
-		return implode('', $blocks);
1716
-	}
1717
-
1718
-	/**
1719
-	 * gets a SID of the domain of the given dn
1720
-	 * @param string $dn
1721
-	 * @return string|bool
1722
-	 */
1723
-	public function getSID($dn) {
1724
-		$domainDN = $this->getDomainDNFromDN($dn);
1725
-		$cacheKey = 'getSID-'.$domainDN;
1726
-		$sid = $this->connection->getFromCache($cacheKey);
1727
-		if(!is_null($sid)) {
1728
-			return $sid;
1729
-		}
1730
-
1731
-		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1732
-		if(!is_array($objectSid) || empty($objectSid)) {
1733
-			$this->connection->writeToCache($cacheKey, false);
1734
-			return false;
1735
-		}
1736
-		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1737
-		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1738
-
1739
-		return $domainObjectSid;
1740
-	}
1741
-
1742
-	/**
1743
-	 * converts a binary SID into a string representation
1744
-	 * @param string $sid
1745
-	 * @return string
1746
-	 */
1747
-	public function convertSID2Str($sid) {
1748
-		// The format of a SID binary string is as follows:
1749
-		// 1 byte for the revision level
1750
-		// 1 byte for the number n of variable sub-ids
1751
-		// 6 bytes for identifier authority value
1752
-		// n*4 bytes for n sub-ids
1753
-		//
1754
-		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1755
-		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1756
-		$revision = ord($sid[0]);
1757
-		$numberSubID = ord($sid[1]);
1758
-
1759
-		$subIdStart = 8; // 1 + 1 + 6
1760
-		$subIdLength = 4;
1761
-		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1762
-			// Incorrect number of bytes present.
1763
-			return '';
1764
-		}
1765
-
1766
-		// 6 bytes = 48 bits can be represented using floats without loss of
1767
-		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1768
-		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1769
-
1770
-		$subIDs = array();
1771
-		for ($i = 0; $i < $numberSubID; $i++) {
1772
-			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1773
-			$subIDs[] = sprintf('%u', $subID[1]);
1774
-		}
1775
-
1776
-		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1777
-		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1778
-	}
1779
-
1780
-	/**
1781
-	 * checks if the given DN is part of the given base DN(s)
1782
-	 * @param string $dn the DN
1783
-	 * @param string[] $bases array containing the allowed base DN or DNs
1784
-	 * @return bool
1785
-	 */
1786
-	public function isDNPartOfBase($dn, $bases) {
1787
-		$belongsToBase = false;
1788
-		$bases = $this->helper->sanitizeDN($bases);
1789
-
1790
-		foreach($bases as $base) {
1791
-			$belongsToBase = true;
1792
-			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1793
-				$belongsToBase = false;
1794
-			}
1795
-			if($belongsToBase) {
1796
-				break;
1797
-			}
1798
-		}
1799
-		return $belongsToBase;
1800
-	}
1801
-
1802
-	/**
1803
-	 * resets a running Paged Search operation
1804
-	 */
1805
-	private function abandonPagedSearch() {
1806
-		if($this->connection->hasPagedResultSupport) {
1807
-			$cr = $this->connection->getConnectionResource();
1808
-			$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1809
-			$this->getPagedSearchResultState();
1810
-			$this->lastCookie = '';
1811
-			$this->cookies = array();
1812
-		}
1813
-	}
1814
-
1815
-	/**
1816
-	 * get a cookie for the next LDAP paged search
1817
-	 * @param string $base a string with the base DN for the search
1818
-	 * @param string $filter the search filter to identify the correct search
1819
-	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1820
-	 * @param int $offset the offset for the new search to identify the correct search really good
1821
-	 * @return string containing the key or empty if none is cached
1822
-	 */
1823
-	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1824
-		if($offset === 0) {
1825
-			return '';
1826
-		}
1827
-		$offset -= $limit;
1828
-		//we work with cache here
1829
-		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1830
-		$cookie = '';
1831
-		if(isset($this->cookies[$cacheKey])) {
1832
-			$cookie = $this->cookies[$cacheKey];
1833
-			if(is_null($cookie)) {
1834
-				$cookie = '';
1835
-			}
1836
-		}
1837
-		return $cookie;
1838
-	}
1839
-
1840
-	/**
1841
-	 * checks whether an LDAP paged search operation has more pages that can be
1842
-	 * retrieved, typically when offset and limit are provided.
1843
-	 *
1844
-	 * Be very careful to use it: the last cookie value, which is inspected, can
1845
-	 * be reset by other operations. Best, call it immediately after a search(),
1846
-	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1847
-	 * well. Don't rely on it with any fetchList-method.
1848
-	 * @return bool
1849
-	 */
1850
-	public function hasMoreResults() {
1851
-		if(!$this->connection->hasPagedResultSupport) {
1852
-			return false;
1853
-		}
1854
-
1855
-		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1856
-			// as in RFC 2696, when all results are returned, the cookie will
1857
-			// be empty.
1858
-			return false;
1859
-		}
1860
-
1861
-		return true;
1862
-	}
1863
-
1864
-	/**
1865
-	 * set a cookie for LDAP paged search run
1866
-	 * @param string $base a string with the base DN for the search
1867
-	 * @param string $filter the search filter to identify the correct search
1868
-	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1869
-	 * @param int $offset the offset for the run search to identify the correct search really good
1870
-	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1871
-	 * @return void
1872
-	 */
1873
-	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1874
-		// allow '0' for 389ds
1875
-		if(!empty($cookie) || $cookie === '0') {
1876
-			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1877
-			$this->cookies[$cacheKey] = $cookie;
1878
-			$this->lastCookie = $cookie;
1879
-		}
1880
-	}
1881
-
1882
-	/**
1883
-	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1884
-	 * @return boolean|null true on success, null or false otherwise
1885
-	 */
1886
-	public function getPagedSearchResultState() {
1887
-		$result = $this->pagedSearchedSuccessful;
1888
-		$this->pagedSearchedSuccessful = null;
1889
-		return $result;
1890
-	}
1891
-
1892
-	/**
1893
-	 * Prepares a paged search, if possible
1894
-	 * @param string $filter the LDAP filter for the search
1895
-	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1896
-	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1897
-	 * @param int $limit
1898
-	 * @param int $offset
1899
-	 * @return bool|true
1900
-	 */
1901
-	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1902
-		$pagedSearchOK = false;
1903
-		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1904
-			$offset = (int)$offset; //can be null
1905
-			\OCP\Util::writeLog('user_ldap',
1906
-				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1907
-				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1908
-				\OCP\Util::DEBUG);
1909
-			//get the cookie from the search for the previous search, required by LDAP
1910
-			foreach($bases as $base) {
1911
-
1912
-				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1913
-				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1914
-					// no cookie known from a potential previous search. We need
1915
-					// to start from 0 to come to the desired page. cookie value
1916
-					// of '0' is valid, because 389ds
1917
-					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1918
-					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1919
-					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1920
-					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1921
-					// '0' is valid, because 389ds
1922
-					//TODO: remember this, probably does not change in the next request...
1923
-					if(empty($cookie) && $cookie !== '0') {
1924
-						$cookie = null;
1925
-					}
1926
-				}
1927
-				if(!is_null($cookie)) {
1928
-					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1929
-					$this->abandonPagedSearch();
1930
-					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1931
-						$this->connection->getConnectionResource(), $limit,
1932
-						false, $cookie);
1933
-					if(!$pagedSearchOK) {
1934
-						return false;
1935
-					}
1936
-					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1937
-				} else {
1938
-					$e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
1939
-					\OC::$server->getLogger()->logException($e, ['level' => Util::DEBUG]);
1940
-				}
1941
-
1942
-			}
1943
-		/* ++ Fixing RHDS searches with pages with zero results ++
1699
+            \OC::$server->getLogger()->info(
1700
+                'Passed string does not resemble a valid GUID. Known UUID ' .
1701
+                '({uuid}) probably does not match UUID configuration.',
1702
+                [ 'app' => 'user_ldap', 'uuid' => $guid ]
1703
+            );
1704
+            return $guid;
1705
+        }
1706
+        for($i=0; $i < 3; $i++) {
1707
+            $pairs = str_split($blocks[$i], 2);
1708
+            $pairs = array_reverse($pairs);
1709
+            $blocks[$i] = implode('', $pairs);
1710
+        }
1711
+        for($i=0; $i < 5; $i++) {
1712
+            $pairs = str_split($blocks[$i], 2);
1713
+            $blocks[$i] = '\\' . implode('\\', $pairs);
1714
+        }
1715
+        return implode('', $blocks);
1716
+    }
1717
+
1718
+    /**
1719
+     * gets a SID of the domain of the given dn
1720
+     * @param string $dn
1721
+     * @return string|bool
1722
+     */
1723
+    public function getSID($dn) {
1724
+        $domainDN = $this->getDomainDNFromDN($dn);
1725
+        $cacheKey = 'getSID-'.$domainDN;
1726
+        $sid = $this->connection->getFromCache($cacheKey);
1727
+        if(!is_null($sid)) {
1728
+            return $sid;
1729
+        }
1730
+
1731
+        $objectSid = $this->readAttribute($domainDN, 'objectsid');
1732
+        if(!is_array($objectSid) || empty($objectSid)) {
1733
+            $this->connection->writeToCache($cacheKey, false);
1734
+            return false;
1735
+        }
1736
+        $domainObjectSid = $this->convertSID2Str($objectSid[0]);
1737
+        $this->connection->writeToCache($cacheKey, $domainObjectSid);
1738
+
1739
+        return $domainObjectSid;
1740
+    }
1741
+
1742
+    /**
1743
+     * converts a binary SID into a string representation
1744
+     * @param string $sid
1745
+     * @return string
1746
+     */
1747
+    public function convertSID2Str($sid) {
1748
+        // The format of a SID binary string is as follows:
1749
+        // 1 byte for the revision level
1750
+        // 1 byte for the number n of variable sub-ids
1751
+        // 6 bytes for identifier authority value
1752
+        // n*4 bytes for n sub-ids
1753
+        //
1754
+        // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1755
+        //  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1756
+        $revision = ord($sid[0]);
1757
+        $numberSubID = ord($sid[1]);
1758
+
1759
+        $subIdStart = 8; // 1 + 1 + 6
1760
+        $subIdLength = 4;
1761
+        if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1762
+            // Incorrect number of bytes present.
1763
+            return '';
1764
+        }
1765
+
1766
+        // 6 bytes = 48 bits can be represented using floats without loss of
1767
+        // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1768
+        $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1769
+
1770
+        $subIDs = array();
1771
+        for ($i = 0; $i < $numberSubID; $i++) {
1772
+            $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1773
+            $subIDs[] = sprintf('%u', $subID[1]);
1774
+        }
1775
+
1776
+        // Result for example above: S-1-5-21-249921958-728525901-1594176202
1777
+        return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1778
+    }
1779
+
1780
+    /**
1781
+     * checks if the given DN is part of the given base DN(s)
1782
+     * @param string $dn the DN
1783
+     * @param string[] $bases array containing the allowed base DN or DNs
1784
+     * @return bool
1785
+     */
1786
+    public function isDNPartOfBase($dn, $bases) {
1787
+        $belongsToBase = false;
1788
+        $bases = $this->helper->sanitizeDN($bases);
1789
+
1790
+        foreach($bases as $base) {
1791
+            $belongsToBase = true;
1792
+            if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1793
+                $belongsToBase = false;
1794
+            }
1795
+            if($belongsToBase) {
1796
+                break;
1797
+            }
1798
+        }
1799
+        return $belongsToBase;
1800
+    }
1801
+
1802
+    /**
1803
+     * resets a running Paged Search operation
1804
+     */
1805
+    private function abandonPagedSearch() {
1806
+        if($this->connection->hasPagedResultSupport) {
1807
+            $cr = $this->connection->getConnectionResource();
1808
+            $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1809
+            $this->getPagedSearchResultState();
1810
+            $this->lastCookie = '';
1811
+            $this->cookies = array();
1812
+        }
1813
+    }
1814
+
1815
+    /**
1816
+     * get a cookie for the next LDAP paged search
1817
+     * @param string $base a string with the base DN for the search
1818
+     * @param string $filter the search filter to identify the correct search
1819
+     * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1820
+     * @param int $offset the offset for the new search to identify the correct search really good
1821
+     * @return string containing the key or empty if none is cached
1822
+     */
1823
+    private function getPagedResultCookie($base, $filter, $limit, $offset) {
1824
+        if($offset === 0) {
1825
+            return '';
1826
+        }
1827
+        $offset -= $limit;
1828
+        //we work with cache here
1829
+        $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1830
+        $cookie = '';
1831
+        if(isset($this->cookies[$cacheKey])) {
1832
+            $cookie = $this->cookies[$cacheKey];
1833
+            if(is_null($cookie)) {
1834
+                $cookie = '';
1835
+            }
1836
+        }
1837
+        return $cookie;
1838
+    }
1839
+
1840
+    /**
1841
+     * checks whether an LDAP paged search operation has more pages that can be
1842
+     * retrieved, typically when offset and limit are provided.
1843
+     *
1844
+     * Be very careful to use it: the last cookie value, which is inspected, can
1845
+     * be reset by other operations. Best, call it immediately after a search(),
1846
+     * searchUsers() or searchGroups() call. count-methods are probably safe as
1847
+     * well. Don't rely on it with any fetchList-method.
1848
+     * @return bool
1849
+     */
1850
+    public function hasMoreResults() {
1851
+        if(!$this->connection->hasPagedResultSupport) {
1852
+            return false;
1853
+        }
1854
+
1855
+        if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1856
+            // as in RFC 2696, when all results are returned, the cookie will
1857
+            // be empty.
1858
+            return false;
1859
+        }
1860
+
1861
+        return true;
1862
+    }
1863
+
1864
+    /**
1865
+     * set a cookie for LDAP paged search run
1866
+     * @param string $base a string with the base DN for the search
1867
+     * @param string $filter the search filter to identify the correct search
1868
+     * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1869
+     * @param int $offset the offset for the run search to identify the correct search really good
1870
+     * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1871
+     * @return void
1872
+     */
1873
+    private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1874
+        // allow '0' for 389ds
1875
+        if(!empty($cookie) || $cookie === '0') {
1876
+            $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1877
+            $this->cookies[$cacheKey] = $cookie;
1878
+            $this->lastCookie = $cookie;
1879
+        }
1880
+    }
1881
+
1882
+    /**
1883
+     * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1884
+     * @return boolean|null true on success, null or false otherwise
1885
+     */
1886
+    public function getPagedSearchResultState() {
1887
+        $result = $this->pagedSearchedSuccessful;
1888
+        $this->pagedSearchedSuccessful = null;
1889
+        return $result;
1890
+    }
1891
+
1892
+    /**
1893
+     * Prepares a paged search, if possible
1894
+     * @param string $filter the LDAP filter for the search
1895
+     * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1896
+     * @param string[] $attr optional, when a certain attribute shall be filtered outside
1897
+     * @param int $limit
1898
+     * @param int $offset
1899
+     * @return bool|true
1900
+     */
1901
+    private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1902
+        $pagedSearchOK = false;
1903
+        if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1904
+            $offset = (int)$offset; //can be null
1905
+            \OCP\Util::writeLog('user_ldap',
1906
+                'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1907
+                .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1908
+                \OCP\Util::DEBUG);
1909
+            //get the cookie from the search for the previous search, required by LDAP
1910
+            foreach($bases as $base) {
1911
+
1912
+                $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1913
+                if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1914
+                    // no cookie known from a potential previous search. We need
1915
+                    // to start from 0 to come to the desired page. cookie value
1916
+                    // of '0' is valid, because 389ds
1917
+                    $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1918
+                    $this->search($filter, array($base), $attr, $limit, $reOffset, true);
1919
+                    $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1920
+                    //still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1921
+                    // '0' is valid, because 389ds
1922
+                    //TODO: remember this, probably does not change in the next request...
1923
+                    if(empty($cookie) && $cookie !== '0') {
1924
+                        $cookie = null;
1925
+                    }
1926
+                }
1927
+                if(!is_null($cookie)) {
1928
+                    //since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1929
+                    $this->abandonPagedSearch();
1930
+                    $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1931
+                        $this->connection->getConnectionResource(), $limit,
1932
+                        false, $cookie);
1933
+                    if(!$pagedSearchOK) {
1934
+                        return false;
1935
+                    }
1936
+                    \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1937
+                } else {
1938
+                    $e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
1939
+                    \OC::$server->getLogger()->logException($e, ['level' => Util::DEBUG]);
1940
+                }
1941
+
1942
+            }
1943
+        /* ++ Fixing RHDS searches with pages with zero results ++
1944 1944
 		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
1945 1945
 		 * due to pages with zero results.
1946 1946
 		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
1947 1947
 		 * if we don't have a previous paged search.
1948 1948
 		 */
1949
-		} else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1950
-			// a search without limit was requested. However, if we do use
1951
-			// Paged Search once, we always must do it. This requires us to
1952
-			// initialize it with the configured page size.
1953
-			$this->abandonPagedSearch();
1954
-			// in case someone set it to 0 … use 500, otherwise no results will
1955
-			// be returned.
1956
-			$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
1957
-			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1958
-				$this->connection->getConnectionResource(),
1959
-				$pageSize, false, '');
1960
-		}
1961
-
1962
-		return $pagedSearchOK;
1963
-	}
1949
+        } else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1950
+            // a search without limit was requested. However, if we do use
1951
+            // Paged Search once, we always must do it. This requires us to
1952
+            // initialize it with the configured page size.
1953
+            $this->abandonPagedSearch();
1954
+            // in case someone set it to 0 … use 500, otherwise no results will
1955
+            // be returned.
1956
+            $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
1957
+            $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1958
+                $this->connection->getConnectionResource(),
1959
+                $pageSize, false, '');
1960
+        }
1961
+
1962
+        return $pagedSearchOK;
1963
+    }
1964 1964
 
1965 1965
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Group_LDAP.php 2 patches
Indentation   +1139 added lines, -1139 removed lines patch added patch discarded remove patch
@@ -45,1144 +45,1144 @@
 block discarded – undo
45 45
 use OCP\GroupInterface;
46 46
 
47 47
 class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP {
48
-	protected $enabled = false;
49
-
50
-	/**
51
-	 * @var string[] $cachedGroupMembers array of users with gid as key
52
-	 */
53
-	protected $cachedGroupMembers;
54
-
55
-	/**
56
-	 * @var string[] $cachedGroupsByMember array of groups with uid as key
57
-	 */
58
-	protected $cachedGroupsByMember;
59
-
60
-	/** @var GroupPluginManager */
61
-	protected $groupPluginManager;
62
-
63
-	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
64
-		parent::__construct($access);
65
-		$filter = $this->access->connection->ldapGroupFilter;
66
-		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
67
-		if(!empty($filter) && !empty($gassoc)) {
68
-			$this->enabled = true;
69
-		}
70
-
71
-		$this->cachedGroupMembers = new CappedMemoryCache();
72
-		$this->cachedGroupsByMember = new CappedMemoryCache();
73
-		$this->groupPluginManager = $groupPluginManager;
74
-	}
75
-
76
-	/**
77
-	 * is user in group?
78
-	 * @param string $uid uid of the user
79
-	 * @param string $gid gid of the group
80
-	 * @return bool
81
-	 *
82
-	 * Checks whether the user is member of a group or not.
83
-	 */
84
-	public function inGroup($uid, $gid) {
85
-		if(!$this->enabled) {
86
-			return false;
87
-		}
88
-		$cacheKey = 'inGroup'.$uid.':'.$gid;
89
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
90
-		if(!is_null($inGroup)) {
91
-			return (bool)$inGroup;
92
-		}
93
-
94
-		$userDN = $this->access->username2dn($uid);
95
-
96
-		if(isset($this->cachedGroupMembers[$gid])) {
97
-			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
98
-			return $isInGroup;
99
-		}
100
-
101
-		$cacheKeyMembers = 'inGroup-members:'.$gid;
102
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
103
-		if(!is_null($members)) {
104
-			$this->cachedGroupMembers[$gid] = $members;
105
-			$isInGroup = in_array($userDN, $members);
106
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
107
-			return $isInGroup;
108
-		}
109
-
110
-		$groupDN = $this->access->groupname2dn($gid);
111
-		// just in case
112
-		if(!$groupDN || !$userDN) {
113
-			$this->access->connection->writeToCache($cacheKey, false);
114
-			return false;
115
-		}
116
-
117
-		//check primary group first
118
-		if($gid === $this->getUserPrimaryGroup($userDN)) {
119
-			$this->access->connection->writeToCache($cacheKey, true);
120
-			return true;
121
-		}
122
-
123
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
124
-		$members = $this->_groupMembers($groupDN);
125
-		$members = array_keys($members); // uids are returned as keys
126
-		if(!is_array($members) || count($members) === 0) {
127
-			$this->access->connection->writeToCache($cacheKey, false);
128
-			return false;
129
-		}
130
-
131
-		//extra work if we don't get back user DNs
132
-		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
133
-			$dns = array();
134
-			$filterParts = array();
135
-			$bytes = 0;
136
-			foreach($members as $mid) {
137
-				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
138
-				$filterParts[] = $filter;
139
-				$bytes += strlen($filter);
140
-				if($bytes >= 9000000) {
141
-					// AD has a default input buffer of 10 MB, we do not want
142
-					// to take even the chance to exceed it
143
-					$filter = $this->access->combineFilterWithOr($filterParts);
144
-					$bytes = 0;
145
-					$filterParts = array();
146
-					$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
147
-					$dns = array_merge($dns, $users);
148
-				}
149
-			}
150
-			if(count($filterParts) > 0) {
151
-				$filter = $this->access->combineFilterWithOr($filterParts);
152
-				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
153
-				$dns = array_merge($dns, $users);
154
-			}
155
-			$members = $dns;
156
-		}
157
-
158
-		$isInGroup = in_array($userDN, $members);
159
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
160
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
161
-		$this->cachedGroupMembers[$gid] = $members;
162
-
163
-		return $isInGroup;
164
-	}
165
-
166
-	/**
167
-	 * @param string $dnGroup
168
-	 * @return array
169
-	 *
170
-	 * For a group that has user membership defined by an LDAP search url attribute returns the users
171
-	 * that match the search url otherwise returns an empty array.
172
-	 */
173
-	public function getDynamicGroupMembers($dnGroup) {
174
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
175
-
176
-		if (empty($dynamicGroupMemberURL)) {
177
-			return array();
178
-		}
179
-
180
-		$dynamicMembers = array();
181
-		$memberURLs = $this->access->readAttribute(
182
-			$dnGroup,
183
-			$dynamicGroupMemberURL,
184
-			$this->access->connection->ldapGroupFilter
185
-		);
186
-		if ($memberURLs !== false) {
187
-			// this group has the 'memberURL' attribute so this is a dynamic group
188
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
189
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
190
-			$pos = strpos($memberURLs[0], '(');
191
-			if ($pos !== false) {
192
-				$memberUrlFilter = substr($memberURLs[0], $pos);
193
-				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
194
-				$dynamicMembers = array();
195
-				foreach($foundMembers as $value) {
196
-					$dynamicMembers[$value['dn'][0]] = 1;
197
-				}
198
-			} else {
199
-				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
200
-					'of group ' . $dnGroup, \OCP\Util::DEBUG);
201
-			}
202
-		}
203
-		return $dynamicMembers;
204
-	}
205
-
206
-	/**
207
-	 * @param string $dnGroup
208
-	 * @param array|null &$seen
209
-	 * @return array|mixed|null
210
-	 */
211
-	private function _groupMembers($dnGroup, &$seen = null) {
212
-		if ($seen === null) {
213
-			$seen = array();
214
-		}
215
-		$allMembers = array();
216
-		if (array_key_exists($dnGroup, $seen)) {
217
-			// avoid loops
218
-			return array();
219
-		}
220
-		// used extensively in cron job, caching makes sense for nested groups
221
-		$cacheKey = '_groupMembers'.$dnGroup;
222
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
223
-		if(!is_null($groupMembers)) {
224
-			return $groupMembers;
225
-		}
226
-		$seen[$dnGroup] = 1;
227
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
228
-												$this->access->connection->ldapGroupFilter);
229
-		if (is_array($members)) {
230
-			foreach ($members as $memberDN) {
231
-				$allMembers[$memberDN] = 1;
232
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
233
-				if (!empty($nestedGroups)) {
234
-					$subMembers = $this->_groupMembers($memberDN, $seen);
235
-					if ($subMembers) {
236
-						$allMembers = array_merge($allMembers, $subMembers);
237
-					}
238
-				}
239
-			}
240
-		}
241
-
242
-		$allMembers = array_merge($allMembers, $this->getDynamicGroupMembers($dnGroup));
243
-
244
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
245
-		return $allMembers;
246
-	}
247
-
248
-	/**
249
-	 * @param string $DN
250
-	 * @param array|null &$seen
251
-	 * @return array
252
-	 */
253
-	private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
254
-		if ($seen === null) {
255
-			$seen = array();
256
-		}
257
-		if (array_key_exists($DN, $seen)) {
258
-			// avoid loops
259
-			return array();
260
-		}
261
-		$seen[$DN] = 1;
262
-		$groups = $this->access->readAttribute($DN, 'memberOf');
263
-		if (!is_array($groups)) {
264
-			return array();
265
-		}
266
-		$groups = $this->access->groupsMatchFilter($groups);
267
-		$allGroups =  $groups;
268
-		$nestedGroups = $this->access->connection->ldapNestedGroups;
269
-		if ((int)$nestedGroups === 1) {
270
-			foreach ($groups as $group) {
271
-				$subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
272
-				$allGroups = array_merge($allGroups, $subGroups);
273
-			}
274
-		}
275
-		return $allGroups;
276
-	}
277
-
278
-	/**
279
-	 * translates a gidNumber into an ownCloud internal name
280
-	 * @param string $gid as given by gidNumber on POSIX LDAP
281
-	 * @param string $dn a DN that belongs to the same domain as the group
282
-	 * @return string|bool
283
-	 */
284
-	public function gidNumber2Name($gid, $dn) {
285
-		$cacheKey = 'gidNumberToName' . $gid;
286
-		$groupName = $this->access->connection->getFromCache($cacheKey);
287
-		if(!is_null($groupName) && isset($groupName)) {
288
-			return $groupName;
289
-		}
290
-
291
-		//we need to get the DN from LDAP
292
-		$filter = $this->access->combineFilterWithAnd([
293
-			$this->access->connection->ldapGroupFilter,
294
-			'objectClass=posixGroup',
295
-			$this->access->connection->ldapGidNumber . '=' . $gid
296
-		]);
297
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
298
-		if(empty($result)) {
299
-			return false;
300
-		}
301
-		$dn = $result[0]['dn'][0];
302
-
303
-		//and now the group name
304
-		//NOTE once we have separate ownCloud group IDs and group names we can
305
-		//directly read the display name attribute instead of the DN
306
-		$name = $this->access->dn2groupname($dn);
307
-
308
-		$this->access->connection->writeToCache($cacheKey, $name);
309
-
310
-		return $name;
311
-	}
312
-
313
-	/**
314
-	 * returns the entry's gidNumber
315
-	 * @param string $dn
316
-	 * @param string $attribute
317
-	 * @return string|bool
318
-	 */
319
-	private function getEntryGidNumber($dn, $attribute) {
320
-		$value = $this->access->readAttribute($dn, $attribute);
321
-		if(is_array($value) && !empty($value)) {
322
-			return $value[0];
323
-		}
324
-		return false;
325
-	}
326
-
327
-	/**
328
-	 * returns the group's primary ID
329
-	 * @param string $dn
330
-	 * @return string|bool
331
-	 */
332
-	public function getGroupGidNumber($dn) {
333
-		return $this->getEntryGidNumber($dn, 'gidNumber');
334
-	}
335
-
336
-	/**
337
-	 * returns the user's gidNumber
338
-	 * @param string $dn
339
-	 * @return string|bool
340
-	 */
341
-	public function getUserGidNumber($dn) {
342
-		$gidNumber = false;
343
-		if($this->access->connection->hasGidNumber) {
344
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
345
-			if($gidNumber === false) {
346
-				$this->access->connection->hasGidNumber = false;
347
-			}
348
-		}
349
-		return $gidNumber;
350
-	}
351
-
352
-	/**
353
-	 * returns a filter for a "users has specific gid" search or count operation
354
-	 *
355
-	 * @param string $groupDN
356
-	 * @param string $search
357
-	 * @return string
358
-	 * @throws \Exception
359
-	 */
360
-	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
361
-		$groupID = $this->getGroupGidNumber($groupDN);
362
-		if($groupID === false) {
363
-			throw new \Exception('Not a valid group');
364
-		}
365
-
366
-		$filterParts = [];
367
-		$filterParts[] = $this->access->getFilterForUserCount();
368
-		if ($search !== '') {
369
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
370
-		}
371
-		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
372
-
373
-		return $this->access->combineFilterWithAnd($filterParts);
374
-	}
375
-
376
-	/**
377
-	 * returns a list of users that have the given group as gid number
378
-	 *
379
-	 * @param string $groupDN
380
-	 * @param string $search
381
-	 * @param int $limit
382
-	 * @param int $offset
383
-	 * @return string[]
384
-	 */
385
-	public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
386
-		try {
387
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
388
-			$users = $this->access->fetchListOfUsers(
389
-				$filter,
390
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
391
-				$limit,
392
-				$offset
393
-			);
394
-			return $this->access->nextcloudUserNames($users);
395
-		} catch (\Exception $e) {
396
-			return [];
397
-		}
398
-	}
399
-
400
-	/**
401
-	 * returns the number of users that have the given group as gid number
402
-	 *
403
-	 * @param string $groupDN
404
-	 * @param string $search
405
-	 * @param int $limit
406
-	 * @param int $offset
407
-	 * @return int
408
-	 */
409
-	public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
410
-		try {
411
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
412
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
413
-			return (int)$users;
414
-		} catch (\Exception $e) {
415
-			return 0;
416
-		}
417
-	}
418
-
419
-	/**
420
-	 * gets the gidNumber of a user
421
-	 * @param string $dn
422
-	 * @return string
423
-	 */
424
-	public function getUserGroupByGid($dn) {
425
-		$groupID = $this->getUserGidNumber($dn);
426
-		if($groupID !== false) {
427
-			$groupName = $this->gidNumber2Name($groupID, $dn);
428
-			if($groupName !== false) {
429
-				return $groupName;
430
-			}
431
-		}
432
-
433
-		return false;
434
-	}
435
-
436
-	/**
437
-	 * translates a primary group ID into an Nextcloud internal name
438
-	 * @param string $gid as given by primaryGroupID on AD
439
-	 * @param string $dn a DN that belongs to the same domain as the group
440
-	 * @return string|bool
441
-	 */
442
-	public function primaryGroupID2Name($gid, $dn) {
443
-		$cacheKey = 'primaryGroupIDtoName';
444
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
445
-		if(!is_null($groupNames) && isset($groupNames[$gid])) {
446
-			return $groupNames[$gid];
447
-		}
448
-
449
-		$domainObjectSid = $this->access->getSID($dn);
450
-		if($domainObjectSid === false) {
451
-			return false;
452
-		}
453
-
454
-		//we need to get the DN from LDAP
455
-		$filter = $this->access->combineFilterWithAnd(array(
456
-			$this->access->connection->ldapGroupFilter,
457
-			'objectsid=' . $domainObjectSid . '-' . $gid
458
-		));
459
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
460
-		if(empty($result)) {
461
-			return false;
462
-		}
463
-		$dn = $result[0]['dn'][0];
464
-
465
-		//and now the group name
466
-		//NOTE once we have separate Nextcloud group IDs and group names we can
467
-		//directly read the display name attribute instead of the DN
468
-		$name = $this->access->dn2groupname($dn);
469
-
470
-		$this->access->connection->writeToCache($cacheKey, $name);
471
-
472
-		return $name;
473
-	}
474
-
475
-	/**
476
-	 * returns the entry's primary group ID
477
-	 * @param string $dn
478
-	 * @param string $attribute
479
-	 * @return string|bool
480
-	 */
481
-	private function getEntryGroupID($dn, $attribute) {
482
-		$value = $this->access->readAttribute($dn, $attribute);
483
-		if(is_array($value) && !empty($value)) {
484
-			return $value[0];
485
-		}
486
-		return false;
487
-	}
488
-
489
-	/**
490
-	 * returns the group's primary ID
491
-	 * @param string $dn
492
-	 * @return string|bool
493
-	 */
494
-	public function getGroupPrimaryGroupID($dn) {
495
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
496
-	}
497
-
498
-	/**
499
-	 * returns the user's primary group ID
500
-	 * @param string $dn
501
-	 * @return string|bool
502
-	 */
503
-	public function getUserPrimaryGroupIDs($dn) {
504
-		$primaryGroupID = false;
505
-		if($this->access->connection->hasPrimaryGroups) {
506
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
507
-			if($primaryGroupID === false) {
508
-				$this->access->connection->hasPrimaryGroups = false;
509
-			}
510
-		}
511
-		return $primaryGroupID;
512
-	}
513
-
514
-	/**
515
-	 * returns a filter for a "users in primary group" search or count operation
516
-	 *
517
-	 * @param string $groupDN
518
-	 * @param string $search
519
-	 * @return string
520
-	 * @throws \Exception
521
-	 */
522
-	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
523
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
524
-		if($groupID === false) {
525
-			throw new \Exception('Not a valid group');
526
-		}
527
-
528
-		$filterParts = [];
529
-		$filterParts[] = $this->access->getFilterForUserCount();
530
-		if ($search !== '') {
531
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
532
-		}
533
-		$filterParts[] = 'primaryGroupID=' . $groupID;
534
-
535
-		return $this->access->combineFilterWithAnd($filterParts);
536
-	}
537
-
538
-	/**
539
-	 * returns a list of users that have the given group as primary group
540
-	 *
541
-	 * @param string $groupDN
542
-	 * @param string $search
543
-	 * @param int $limit
544
-	 * @param int $offset
545
-	 * @return string[]
546
-	 */
547
-	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
548
-		try {
549
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
550
-			$users = $this->access->fetchListOfUsers(
551
-				$filter,
552
-				array($this->access->connection->ldapUserDisplayName, 'dn'),
553
-				$limit,
554
-				$offset
555
-			);
556
-			return $this->access->nextcloudUserNames($users);
557
-		} catch (\Exception $e) {
558
-			return array();
559
-		}
560
-	}
561
-
562
-	/**
563
-	 * returns the number of users that have the given group as primary group
564
-	 *
565
-	 * @param string $groupDN
566
-	 * @param string $search
567
-	 * @param int $limit
568
-	 * @param int $offset
569
-	 * @return int
570
-	 */
571
-	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
572
-		try {
573
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
574
-			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
575
-			return (int)$users;
576
-		} catch (\Exception $e) {
577
-			return 0;
578
-		}
579
-	}
580
-
581
-	/**
582
-	 * gets the primary group of a user
583
-	 * @param string $dn
584
-	 * @return string
585
-	 */
586
-	public function getUserPrimaryGroup($dn) {
587
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
588
-		if($groupID !== false) {
589
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
590
-			if($groupName !== false) {
591
-				return $groupName;
592
-			}
593
-		}
594
-
595
-		return false;
596
-	}
597
-
598
-	/**
599
-	 * Get all groups a user belongs to
600
-	 * @param string $uid Name of the user
601
-	 * @return array with group names
602
-	 *
603
-	 * This function fetches all groups a user belongs to. It does not check
604
-	 * if the user exists at all.
605
-	 *
606
-	 * This function includes groups based on dynamic group membership.
607
-	 */
608
-	public function getUserGroups($uid) {
609
-		if(!$this->enabled) {
610
-			return array();
611
-		}
612
-		$cacheKey = 'getUserGroups'.$uid;
613
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
614
-		if(!is_null($userGroups)) {
615
-			return $userGroups;
616
-		}
617
-		$userDN = $this->access->username2dn($uid);
618
-		if(!$userDN) {
619
-			$this->access->connection->writeToCache($cacheKey, array());
620
-			return array();
621
-		}
622
-
623
-		$groups = [];
624
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
625
-		$gidGroupName = $this->getUserGroupByGid($userDN);
626
-
627
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
628
-
629
-		if (!empty($dynamicGroupMemberURL)) {
630
-			// look through dynamic groups to add them to the result array if needed
631
-			$groupsToMatch = $this->access->fetchListOfGroups(
632
-				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
633
-			foreach($groupsToMatch as $dynamicGroup) {
634
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
635
-					continue;
636
-				}
637
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
638
-				if ($pos !== false) {
639
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
640
-					// apply filter via ldap search to see if this user is in this
641
-					// dynamic group
642
-					$userMatch = $this->access->readAttribute(
643
-						$userDN,
644
-						$this->access->connection->ldapUserDisplayName,
645
-						$memberUrlFilter
646
-					);
647
-					if ($userMatch !== false) {
648
-						// match found so this user is in this group
649
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
650
-						if(is_string($groupName)) {
651
-							// be sure to never return false if the dn could not be
652
-							// resolved to a name, for whatever reason.
653
-							$groups[] = $groupName;
654
-						}
655
-					}
656
-				} else {
657
-					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
658
-						'of group ' . print_r($dynamicGroup, true), \OCP\Util::DEBUG);
659
-				}
660
-			}
661
-		}
662
-
663
-		// if possible, read out membership via memberOf. It's far faster than
664
-		// performing a search, which still is a fallback later.
665
-		// memberof doesn't support memberuid, so skip it here.
666
-		if((int)$this->access->connection->hasMemberOfFilterSupport === 1
667
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
668
-		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
669
-		    ) {
670
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
671
-			if (is_array($groupDNs)) {
672
-				foreach ($groupDNs as $dn) {
673
-					$groupName = $this->access->dn2groupname($dn);
674
-					if(is_string($groupName)) {
675
-						// be sure to never return false if the dn could not be
676
-						// resolved to a name, for whatever reason.
677
-						$groups[] = $groupName;
678
-					}
679
-				}
680
-			}
681
-
682
-			if($primaryGroup !== false) {
683
-				$groups[] = $primaryGroup;
684
-			}
685
-			if($gidGroupName !== false) {
686
-				$groups[] = $gidGroupName;
687
-			}
688
-			$this->access->connection->writeToCache($cacheKey, $groups);
689
-			return $groups;
690
-		}
691
-
692
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
693
-		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
694
-			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
695
-		) {
696
-			$uid = $userDN;
697
-		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
698
-			$result = $this->access->readAttribute($userDN, 'uid');
699
-			if ($result === false) {
700
-				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
701
-					$this->access->connection->ldapHost, \OCP\Util::DEBUG);
702
-			}
703
-			$uid = $result[0];
704
-		} else {
705
-			// just in case
706
-			$uid = $userDN;
707
-		}
708
-
709
-		if(isset($this->cachedGroupsByMember[$uid])) {
710
-			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
711
-		} else {
712
-			$groupsByMember = array_values($this->getGroupsByMember($uid));
713
-			$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
714
-			$this->cachedGroupsByMember[$uid] = $groupsByMember;
715
-			$groups = array_merge($groups, $groupsByMember);
716
-		}
717
-
718
-		if($primaryGroup !== false) {
719
-			$groups[] = $primaryGroup;
720
-		}
721
-		if($gidGroupName !== false) {
722
-			$groups[] = $gidGroupName;
723
-		}
724
-
725
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
726
-		$this->access->connection->writeToCache($cacheKey, $groups);
727
-
728
-		return $groups;
729
-	}
730
-
731
-	/**
732
-	 * @param string $dn
733
-	 * @param array|null &$seen
734
-	 * @return array
735
-	 */
736
-	private function getGroupsByMember($dn, &$seen = null) {
737
-		if ($seen === null) {
738
-			$seen = array();
739
-		}
740
-		$allGroups = array();
741
-		if (array_key_exists($dn, $seen)) {
742
-			// avoid loops
743
-			return array();
744
-		}
745
-		$seen[$dn] = true;
746
-		$filter = $this->access->combineFilterWithAnd(array(
747
-			$this->access->connection->ldapGroupFilter,
748
-			$this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
749
-		));
750
-		$groups = $this->access->fetchListOfGroups($filter,
751
-			array($this->access->connection->ldapGroupDisplayName, 'dn'));
752
-		if (is_array($groups)) {
753
-			foreach ($groups as $groupobj) {
754
-				$groupDN = $groupobj['dn'][0];
755
-				$allGroups[$groupDN] = $groupobj;
756
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
757
-				if (!empty($nestedGroups)) {
758
-					$supergroups = $this->getGroupsByMember($groupDN, $seen);
759
-					if (is_array($supergroups) && (count($supergroups)>0)) {
760
-						$allGroups = array_merge($allGroups, $supergroups);
761
-					}
762
-				}
763
-			}
764
-		}
765
-		return $allGroups;
766
-	}
767
-
768
-	/**
769
-	 * get a list of all users in a group
770
-	 *
771
-	 * @param string $gid
772
-	 * @param string $search
773
-	 * @param int $limit
774
-	 * @param int $offset
775
-	 * @return array with user ids
776
-	 */
777
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
778
-		if(!$this->enabled) {
779
-			return array();
780
-		}
781
-		if(!$this->groupExists($gid)) {
782
-			return array();
783
-		}
784
-		$search = $this->access->escapeFilterPart($search, true);
785
-		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
786
-		// check for cache of the exact query
787
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
788
-		if(!is_null($groupUsers)) {
789
-			return $groupUsers;
790
-		}
791
-
792
-		// check for cache of the query without limit and offset
793
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
794
-		if(!is_null($groupUsers)) {
795
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
796
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
797
-			return $groupUsers;
798
-		}
799
-
800
-		if($limit === -1) {
801
-			$limit = null;
802
-		}
803
-		$groupDN = $this->access->groupname2dn($gid);
804
-		if(!$groupDN) {
805
-			// group couldn't be found, return empty resultset
806
-			$this->access->connection->writeToCache($cacheKey, array());
807
-			return array();
808
-		}
809
-
810
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
811
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
812
-		$members = array_keys($this->_groupMembers($groupDN));
813
-		if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
814
-			//in case users could not be retrieved, return empty result set
815
-			$this->access->connection->writeToCache($cacheKey, []);
816
-			return [];
817
-		}
818
-
819
-		$groupUsers = array();
820
-		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
821
-		$attrs = $this->access->userManager->getAttributes(true);
822
-		foreach($members as $member) {
823
-			if($isMemberUid) {
824
-				//we got uids, need to get their DNs to 'translate' them to user names
825
-				$filter = $this->access->combineFilterWithAnd(array(
826
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
827
-					$this->access->getFilterPartForUserSearch($search)
828
-				));
829
-				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
830
-				if(count($ldap_users) < 1) {
831
-					continue;
832
-				}
833
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
834
-			} else {
835
-				//we got DNs, check if we need to filter by search or we can give back all of them
836
-				if ($search !== '') {
837
-					if(!$this->access->readAttribute($member,
838
-						$this->access->connection->ldapUserDisplayName,
839
-						$this->access->getFilterPartForUserSearch($search))) {
840
-						continue;
841
-					}
842
-				}
843
-				// dn2username will also check if the users belong to the allowed base
844
-				if($ocname = $this->access->dn2username($member)) {
845
-					$groupUsers[] = $ocname;
846
-				}
847
-			}
848
-		}
849
-
850
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
851
-		natsort($groupUsers);
852
-		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
853
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
854
-
855
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
856
-
857
-		return $groupUsers;
858
-	}
859
-
860
-	/**
861
-	 * returns the number of users in a group, who match the search term
862
-	 * @param string $gid the internal group name
863
-	 * @param string $search optional, a search string
864
-	 * @return int|bool
865
-	 */
866
-	public function countUsersInGroup($gid, $search = '') {
867
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
868
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
869
-		}
870
-
871
-		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
872
-		if(!$this->enabled || !$this->groupExists($gid)) {
873
-			return false;
874
-		}
875
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
876
-		if(!is_null($groupUsers)) {
877
-			return $groupUsers;
878
-		}
879
-
880
-		$groupDN = $this->access->groupname2dn($gid);
881
-		if(!$groupDN) {
882
-			// group couldn't be found, return empty result set
883
-			$this->access->connection->writeToCache($cacheKey, false);
884
-			return false;
885
-		}
886
-
887
-		$members = array_keys($this->_groupMembers($groupDN));
888
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
889
-		if(!$members && $primaryUserCount === 0) {
890
-			//in case users could not be retrieved, return empty result set
891
-			$this->access->connection->writeToCache($cacheKey, false);
892
-			return false;
893
-		}
894
-
895
-		if ($search === '') {
896
-			$groupUsers = count($members) + $primaryUserCount;
897
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
898
-			return $groupUsers;
899
-		}
900
-		$search = $this->access->escapeFilterPart($search, true);
901
-		$isMemberUid =
902
-			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
903
-			=== 'memberuid');
904
-
905
-		//we need to apply the search filter
906
-		//alternatives that need to be checked:
907
-		//a) get all users by search filter and array_intersect them
908
-		//b) a, but only when less than 1k 10k ?k users like it is
909
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
910
-		//   and let it count.
911
-		//For now this is not important, because the only use of this method
912
-		//does not supply a search string
913
-		$groupUsers = array();
914
-		foreach($members as $member) {
915
-			if($isMemberUid) {
916
-				//we got uids, need to get their DNs to 'translate' them to user names
917
-				$filter = $this->access->combineFilterWithAnd(array(
918
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
919
-					$this->access->getFilterPartForUserSearch($search)
920
-				));
921
-				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
922
-				if(count($ldap_users) < 1) {
923
-					continue;
924
-				}
925
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
926
-			} else {
927
-				//we need to apply the search filter now
928
-				if(!$this->access->readAttribute($member,
929
-					$this->access->connection->ldapUserDisplayName,
930
-					$this->access->getFilterPartForUserSearch($search))) {
931
-					continue;
932
-				}
933
-				// dn2username will also check if the users belong to the allowed base
934
-				if($ocname = $this->access->dn2username($member)) {
935
-					$groupUsers[] = $ocname;
936
-				}
937
-			}
938
-		}
939
-
940
-		//and get users that have the group as primary
941
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
942
-
943
-		return count($groupUsers) + $primaryUsers;
944
-	}
945
-
946
-	/**
947
-	 * get a list of all groups
948
-	 *
949
-	 * @param string $search
950
-	 * @param $limit
951
-	 * @param int $offset
952
-	 * @return array with group names
953
-	 *
954
-	 * Returns a list with all groups (used by getGroups)
955
-	 */
956
-	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
957
-		if(!$this->enabled) {
958
-			return array();
959
-		}
960
-		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
961
-
962
-		//Check cache before driving unnecessary searches
963
-		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, \OCP\Util::DEBUG);
964
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
965
-		if(!is_null($ldap_groups)) {
966
-			return $ldap_groups;
967
-		}
968
-
969
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
970
-		// error. With a limit of 0, we get 0 results. So we pass null.
971
-		if($limit <= 0) {
972
-			$limit = null;
973
-		}
974
-		$filter = $this->access->combineFilterWithAnd(array(
975
-			$this->access->connection->ldapGroupFilter,
976
-			$this->access->getFilterPartForGroupSearch($search)
977
-		));
978
-		\OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, \OCP\Util::DEBUG);
979
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
980
-				array($this->access->connection->ldapGroupDisplayName, 'dn'),
981
-				$limit,
982
-				$offset);
983
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
984
-
985
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
986
-		return $ldap_groups;
987
-	}
988
-
989
-	/**
990
-	 * get a list of all groups using a paged search
991
-	 *
992
-	 * @param string $search
993
-	 * @param int $limit
994
-	 * @param int $offset
995
-	 * @return array with group names
996
-	 *
997
-	 * Returns a list with all groups
998
-	 * Uses a paged search if available to override a
999
-	 * server side search limit.
1000
-	 * (active directory has a limit of 1000 by default)
1001
-	 */
1002
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
1003
-		if(!$this->enabled) {
1004
-			return array();
1005
-		}
1006
-		$search = $this->access->escapeFilterPart($search, true);
1007
-		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1008
-		if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
1009
-			return $this->getGroupsChunk($search, $limit, $offset);
1010
-		}
1011
-		$maxGroups = 100000; // limit max results (just for safety reasons)
1012
-		if ($limit > -1) {
1013
-		   $overallLimit = min($limit + $offset, $maxGroups);
1014
-		} else {
1015
-		   $overallLimit = $maxGroups;
1016
-		}
1017
-		$chunkOffset = $offset;
1018
-		$allGroups = array();
1019
-		while ($chunkOffset < $overallLimit) {
1020
-			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1021
-			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1022
-			$nread = count($ldapGroups);
1023
-			\OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', \OCP\Util::DEBUG);
1024
-			if ($nread) {
1025
-				$allGroups = array_merge($allGroups, $ldapGroups);
1026
-				$chunkOffset += $nread;
1027
-			}
1028
-			if ($nread < $chunkLimit) {
1029
-				break;
1030
-			}
1031
-		}
1032
-		return $allGroups;
1033
-	}
1034
-
1035
-	/**
1036
-	 * @param string $group
1037
-	 * @return bool
1038
-	 */
1039
-	public function groupMatchesFilter($group) {
1040
-		return (strripos($group, $this->groupSearch) !== false);
1041
-	}
1042
-
1043
-	/**
1044
-	 * check if a group exists
1045
-	 * @param string $gid
1046
-	 * @return bool
1047
-	 */
1048
-	public function groupExists($gid) {
1049
-		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1050
-		if(!is_null($groupExists)) {
1051
-			return (bool)$groupExists;
1052
-		}
1053
-
1054
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1055
-		//only, requires more checking.
1056
-		$dn = $this->access->groupname2dn($gid);
1057
-		if(!$dn) {
1058
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1059
-			return false;
1060
-		}
1061
-
1062
-		//if group really still exists, we will be able to read its objectclass
1063
-		if(!is_array($this->access->readAttribute($dn, ''))) {
1064
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1065
-			return false;
1066
-		}
1067
-
1068
-		$this->access->connection->writeToCache('groupExists'.$gid, true);
1069
-		return true;
1070
-	}
1071
-
1072
-	/**
1073
-	* Check if backend implements actions
1074
-	* @param int $actions bitwise-or'ed actions
1075
-	* @return boolean
1076
-	*
1077
-	* Returns the supported actions as int to be
1078
-	* compared with GroupInterface::CREATE_GROUP etc.
1079
-	*/
1080
-	public function implementsActions($actions) {
1081
-		return (bool)((GroupInterface::COUNT_USERS |
1082
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1083
-	}
1084
-
1085
-	/**
1086
-	 * Return access for LDAP interaction.
1087
-	 * @return Access instance of Access for LDAP interaction
1088
-	 */
1089
-	public function getLDAPAccess($gid) {
1090
-		return $this->access;
1091
-	}
1092
-
1093
-	/**
1094
-	 * create a group
1095
-	 * @param string $gid
1096
-	 * @return bool
1097
-	 * @throws \Exception
1098
-	 */
1099
-	public function createGroup($gid) {
1100
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1101
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1102
-				//updates group mapping
1103
-				$this->access->dn2ocname($dn, $gid, false);
1104
-				$this->access->connection->writeToCache("groupExists".$gid, true);
1105
-			}
1106
-			return $dn != null;
1107
-		}
1108
-		throw new \Exception('Could not create group in LDAP backend.');
1109
-	}
1110
-
1111
-	/**
1112
-	 * delete a group
1113
-	 * @param string $gid gid of the group to delete
1114
-	 * @return bool
1115
-	 * @throws \Exception
1116
-	 */
1117
-	public function deleteGroup($gid) {
1118
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1119
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1120
-				#delete group in nextcloud internal db
1121
-				$this->access->getGroupMapper()->unmap($gid);
1122
-				$this->access->connection->writeToCache("groupExists".$gid, false);
1123
-			}
1124
-			return $ret;
1125
-		}
1126
-		throw new \Exception('Could not delete group in LDAP backend.');
1127
-	}
1128
-
1129
-	/**
1130
-	 * Add a user to a group
1131
-	 * @param string $uid Name of the user to add to group
1132
-	 * @param string $gid Name of the group in which add the user
1133
-	 * @return bool
1134
-	 * @throws \Exception
1135
-	 */
1136
-	public function addToGroup($uid, $gid) {
1137
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1138
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1139
-				$this->access->connection->clearCache();
1140
-			}
1141
-			return $ret;
1142
-		}
1143
-		throw new \Exception('Could not add user to group in LDAP backend.');
1144
-	}
1145
-
1146
-	/**
1147
-	 * Removes a user from a group
1148
-	 * @param string $uid Name of the user to remove from group
1149
-	 * @param string $gid Name of the group from which remove the user
1150
-	 * @return bool
1151
-	 * @throws \Exception
1152
-	 */
1153
-	public function removeFromGroup($uid, $gid) {
1154
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1155
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1156
-				$this->access->connection->clearCache();
1157
-			}
1158
-			return $ret;
1159
-		}
1160
-		throw new \Exception('Could not remove user from group in LDAP backend.');
1161
-	}
1162
-
1163
-	/**
1164
-	 * Gets group details
1165
-	 * @param string $gid Name of the group
1166
-	 * @return array | false
1167
-	 * @throws \Exception
1168
-	 */
1169
-	public function getGroupDetails($gid) {
1170
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1171
-			return $this->groupPluginManager->getGroupDetails($gid);
1172
-		}
1173
-		throw new \Exception('Could not get group details in LDAP backend.');
1174
-	}
1175
-
1176
-	/**
1177
-	 * Return LDAP connection resource from a cloned connection.
1178
-	 * The cloned connection needs to be closed manually.
1179
-	 * of the current access.
1180
-	 * @param string $gid
1181
-	 * @return resource of the LDAP connection
1182
-	 */
1183
-	public function getNewLDAPConnection($gid) {
1184
-		$connection = clone $this->access->getConnection();
1185
-		return $connection->getConnectionResource();
1186
-	}
48
+    protected $enabled = false;
49
+
50
+    /**
51
+     * @var string[] $cachedGroupMembers array of users with gid as key
52
+     */
53
+    protected $cachedGroupMembers;
54
+
55
+    /**
56
+     * @var string[] $cachedGroupsByMember array of groups with uid as key
57
+     */
58
+    protected $cachedGroupsByMember;
59
+
60
+    /** @var GroupPluginManager */
61
+    protected $groupPluginManager;
62
+
63
+    public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
64
+        parent::__construct($access);
65
+        $filter = $this->access->connection->ldapGroupFilter;
66
+        $gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
67
+        if(!empty($filter) && !empty($gassoc)) {
68
+            $this->enabled = true;
69
+        }
70
+
71
+        $this->cachedGroupMembers = new CappedMemoryCache();
72
+        $this->cachedGroupsByMember = new CappedMemoryCache();
73
+        $this->groupPluginManager = $groupPluginManager;
74
+    }
75
+
76
+    /**
77
+     * is user in group?
78
+     * @param string $uid uid of the user
79
+     * @param string $gid gid of the group
80
+     * @return bool
81
+     *
82
+     * Checks whether the user is member of a group or not.
83
+     */
84
+    public function inGroup($uid, $gid) {
85
+        if(!$this->enabled) {
86
+            return false;
87
+        }
88
+        $cacheKey = 'inGroup'.$uid.':'.$gid;
89
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
90
+        if(!is_null($inGroup)) {
91
+            return (bool)$inGroup;
92
+        }
93
+
94
+        $userDN = $this->access->username2dn($uid);
95
+
96
+        if(isset($this->cachedGroupMembers[$gid])) {
97
+            $isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
98
+            return $isInGroup;
99
+        }
100
+
101
+        $cacheKeyMembers = 'inGroup-members:'.$gid;
102
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
103
+        if(!is_null($members)) {
104
+            $this->cachedGroupMembers[$gid] = $members;
105
+            $isInGroup = in_array($userDN, $members);
106
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
107
+            return $isInGroup;
108
+        }
109
+
110
+        $groupDN = $this->access->groupname2dn($gid);
111
+        // just in case
112
+        if(!$groupDN || !$userDN) {
113
+            $this->access->connection->writeToCache($cacheKey, false);
114
+            return false;
115
+        }
116
+
117
+        //check primary group first
118
+        if($gid === $this->getUserPrimaryGroup($userDN)) {
119
+            $this->access->connection->writeToCache($cacheKey, true);
120
+            return true;
121
+        }
122
+
123
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
124
+        $members = $this->_groupMembers($groupDN);
125
+        $members = array_keys($members); // uids are returned as keys
126
+        if(!is_array($members) || count($members) === 0) {
127
+            $this->access->connection->writeToCache($cacheKey, false);
128
+            return false;
129
+        }
130
+
131
+        //extra work if we don't get back user DNs
132
+        if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
133
+            $dns = array();
134
+            $filterParts = array();
135
+            $bytes = 0;
136
+            foreach($members as $mid) {
137
+                $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
138
+                $filterParts[] = $filter;
139
+                $bytes += strlen($filter);
140
+                if($bytes >= 9000000) {
141
+                    // AD has a default input buffer of 10 MB, we do not want
142
+                    // to take even the chance to exceed it
143
+                    $filter = $this->access->combineFilterWithOr($filterParts);
144
+                    $bytes = 0;
145
+                    $filterParts = array();
146
+                    $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
147
+                    $dns = array_merge($dns, $users);
148
+                }
149
+            }
150
+            if(count($filterParts) > 0) {
151
+                $filter = $this->access->combineFilterWithOr($filterParts);
152
+                $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
153
+                $dns = array_merge($dns, $users);
154
+            }
155
+            $members = $dns;
156
+        }
157
+
158
+        $isInGroup = in_array($userDN, $members);
159
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
160
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
161
+        $this->cachedGroupMembers[$gid] = $members;
162
+
163
+        return $isInGroup;
164
+    }
165
+
166
+    /**
167
+     * @param string $dnGroup
168
+     * @return array
169
+     *
170
+     * For a group that has user membership defined by an LDAP search url attribute returns the users
171
+     * that match the search url otherwise returns an empty array.
172
+     */
173
+    public function getDynamicGroupMembers($dnGroup) {
174
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
175
+
176
+        if (empty($dynamicGroupMemberURL)) {
177
+            return array();
178
+        }
179
+
180
+        $dynamicMembers = array();
181
+        $memberURLs = $this->access->readAttribute(
182
+            $dnGroup,
183
+            $dynamicGroupMemberURL,
184
+            $this->access->connection->ldapGroupFilter
185
+        );
186
+        if ($memberURLs !== false) {
187
+            // this group has the 'memberURL' attribute so this is a dynamic group
188
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
189
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
190
+            $pos = strpos($memberURLs[0], '(');
191
+            if ($pos !== false) {
192
+                $memberUrlFilter = substr($memberURLs[0], $pos);
193
+                $foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
194
+                $dynamicMembers = array();
195
+                foreach($foundMembers as $value) {
196
+                    $dynamicMembers[$value['dn'][0]] = 1;
197
+                }
198
+            } else {
199
+                \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
200
+                    'of group ' . $dnGroup, \OCP\Util::DEBUG);
201
+            }
202
+        }
203
+        return $dynamicMembers;
204
+    }
205
+
206
+    /**
207
+     * @param string $dnGroup
208
+     * @param array|null &$seen
209
+     * @return array|mixed|null
210
+     */
211
+    private function _groupMembers($dnGroup, &$seen = null) {
212
+        if ($seen === null) {
213
+            $seen = array();
214
+        }
215
+        $allMembers = array();
216
+        if (array_key_exists($dnGroup, $seen)) {
217
+            // avoid loops
218
+            return array();
219
+        }
220
+        // used extensively in cron job, caching makes sense for nested groups
221
+        $cacheKey = '_groupMembers'.$dnGroup;
222
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
223
+        if(!is_null($groupMembers)) {
224
+            return $groupMembers;
225
+        }
226
+        $seen[$dnGroup] = 1;
227
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
228
+                                                $this->access->connection->ldapGroupFilter);
229
+        if (is_array($members)) {
230
+            foreach ($members as $memberDN) {
231
+                $allMembers[$memberDN] = 1;
232
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
233
+                if (!empty($nestedGroups)) {
234
+                    $subMembers = $this->_groupMembers($memberDN, $seen);
235
+                    if ($subMembers) {
236
+                        $allMembers = array_merge($allMembers, $subMembers);
237
+                    }
238
+                }
239
+            }
240
+        }
241
+
242
+        $allMembers = array_merge($allMembers, $this->getDynamicGroupMembers($dnGroup));
243
+
244
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
245
+        return $allMembers;
246
+    }
247
+
248
+    /**
249
+     * @param string $DN
250
+     * @param array|null &$seen
251
+     * @return array
252
+     */
253
+    private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
254
+        if ($seen === null) {
255
+            $seen = array();
256
+        }
257
+        if (array_key_exists($DN, $seen)) {
258
+            // avoid loops
259
+            return array();
260
+        }
261
+        $seen[$DN] = 1;
262
+        $groups = $this->access->readAttribute($DN, 'memberOf');
263
+        if (!is_array($groups)) {
264
+            return array();
265
+        }
266
+        $groups = $this->access->groupsMatchFilter($groups);
267
+        $allGroups =  $groups;
268
+        $nestedGroups = $this->access->connection->ldapNestedGroups;
269
+        if ((int)$nestedGroups === 1) {
270
+            foreach ($groups as $group) {
271
+                $subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
272
+                $allGroups = array_merge($allGroups, $subGroups);
273
+            }
274
+        }
275
+        return $allGroups;
276
+    }
277
+
278
+    /**
279
+     * translates a gidNumber into an ownCloud internal name
280
+     * @param string $gid as given by gidNumber on POSIX LDAP
281
+     * @param string $dn a DN that belongs to the same domain as the group
282
+     * @return string|bool
283
+     */
284
+    public function gidNumber2Name($gid, $dn) {
285
+        $cacheKey = 'gidNumberToName' . $gid;
286
+        $groupName = $this->access->connection->getFromCache($cacheKey);
287
+        if(!is_null($groupName) && isset($groupName)) {
288
+            return $groupName;
289
+        }
290
+
291
+        //we need to get the DN from LDAP
292
+        $filter = $this->access->combineFilterWithAnd([
293
+            $this->access->connection->ldapGroupFilter,
294
+            'objectClass=posixGroup',
295
+            $this->access->connection->ldapGidNumber . '=' . $gid
296
+        ]);
297
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
298
+        if(empty($result)) {
299
+            return false;
300
+        }
301
+        $dn = $result[0]['dn'][0];
302
+
303
+        //and now the group name
304
+        //NOTE once we have separate ownCloud group IDs and group names we can
305
+        //directly read the display name attribute instead of the DN
306
+        $name = $this->access->dn2groupname($dn);
307
+
308
+        $this->access->connection->writeToCache($cacheKey, $name);
309
+
310
+        return $name;
311
+    }
312
+
313
+    /**
314
+     * returns the entry's gidNumber
315
+     * @param string $dn
316
+     * @param string $attribute
317
+     * @return string|bool
318
+     */
319
+    private function getEntryGidNumber($dn, $attribute) {
320
+        $value = $this->access->readAttribute($dn, $attribute);
321
+        if(is_array($value) && !empty($value)) {
322
+            return $value[0];
323
+        }
324
+        return false;
325
+    }
326
+
327
+    /**
328
+     * returns the group's primary ID
329
+     * @param string $dn
330
+     * @return string|bool
331
+     */
332
+    public function getGroupGidNumber($dn) {
333
+        return $this->getEntryGidNumber($dn, 'gidNumber');
334
+    }
335
+
336
+    /**
337
+     * returns the user's gidNumber
338
+     * @param string $dn
339
+     * @return string|bool
340
+     */
341
+    public function getUserGidNumber($dn) {
342
+        $gidNumber = false;
343
+        if($this->access->connection->hasGidNumber) {
344
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
345
+            if($gidNumber === false) {
346
+                $this->access->connection->hasGidNumber = false;
347
+            }
348
+        }
349
+        return $gidNumber;
350
+    }
351
+
352
+    /**
353
+     * returns a filter for a "users has specific gid" search or count operation
354
+     *
355
+     * @param string $groupDN
356
+     * @param string $search
357
+     * @return string
358
+     * @throws \Exception
359
+     */
360
+    private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
361
+        $groupID = $this->getGroupGidNumber($groupDN);
362
+        if($groupID === false) {
363
+            throw new \Exception('Not a valid group');
364
+        }
365
+
366
+        $filterParts = [];
367
+        $filterParts[] = $this->access->getFilterForUserCount();
368
+        if ($search !== '') {
369
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
370
+        }
371
+        $filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
372
+
373
+        return $this->access->combineFilterWithAnd($filterParts);
374
+    }
375
+
376
+    /**
377
+     * returns a list of users that have the given group as gid number
378
+     *
379
+     * @param string $groupDN
380
+     * @param string $search
381
+     * @param int $limit
382
+     * @param int $offset
383
+     * @return string[]
384
+     */
385
+    public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
386
+        try {
387
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
388
+            $users = $this->access->fetchListOfUsers(
389
+                $filter,
390
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
391
+                $limit,
392
+                $offset
393
+            );
394
+            return $this->access->nextcloudUserNames($users);
395
+        } catch (\Exception $e) {
396
+            return [];
397
+        }
398
+    }
399
+
400
+    /**
401
+     * returns the number of users that have the given group as gid number
402
+     *
403
+     * @param string $groupDN
404
+     * @param string $search
405
+     * @param int $limit
406
+     * @param int $offset
407
+     * @return int
408
+     */
409
+    public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
410
+        try {
411
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
412
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
413
+            return (int)$users;
414
+        } catch (\Exception $e) {
415
+            return 0;
416
+        }
417
+    }
418
+
419
+    /**
420
+     * gets the gidNumber of a user
421
+     * @param string $dn
422
+     * @return string
423
+     */
424
+    public function getUserGroupByGid($dn) {
425
+        $groupID = $this->getUserGidNumber($dn);
426
+        if($groupID !== false) {
427
+            $groupName = $this->gidNumber2Name($groupID, $dn);
428
+            if($groupName !== false) {
429
+                return $groupName;
430
+            }
431
+        }
432
+
433
+        return false;
434
+    }
435
+
436
+    /**
437
+     * translates a primary group ID into an Nextcloud internal name
438
+     * @param string $gid as given by primaryGroupID on AD
439
+     * @param string $dn a DN that belongs to the same domain as the group
440
+     * @return string|bool
441
+     */
442
+    public function primaryGroupID2Name($gid, $dn) {
443
+        $cacheKey = 'primaryGroupIDtoName';
444
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
445
+        if(!is_null($groupNames) && isset($groupNames[$gid])) {
446
+            return $groupNames[$gid];
447
+        }
448
+
449
+        $domainObjectSid = $this->access->getSID($dn);
450
+        if($domainObjectSid === false) {
451
+            return false;
452
+        }
453
+
454
+        //we need to get the DN from LDAP
455
+        $filter = $this->access->combineFilterWithAnd(array(
456
+            $this->access->connection->ldapGroupFilter,
457
+            'objectsid=' . $domainObjectSid . '-' . $gid
458
+        ));
459
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
460
+        if(empty($result)) {
461
+            return false;
462
+        }
463
+        $dn = $result[0]['dn'][0];
464
+
465
+        //and now the group name
466
+        //NOTE once we have separate Nextcloud group IDs and group names we can
467
+        //directly read the display name attribute instead of the DN
468
+        $name = $this->access->dn2groupname($dn);
469
+
470
+        $this->access->connection->writeToCache($cacheKey, $name);
471
+
472
+        return $name;
473
+    }
474
+
475
+    /**
476
+     * returns the entry's primary group ID
477
+     * @param string $dn
478
+     * @param string $attribute
479
+     * @return string|bool
480
+     */
481
+    private function getEntryGroupID($dn, $attribute) {
482
+        $value = $this->access->readAttribute($dn, $attribute);
483
+        if(is_array($value) && !empty($value)) {
484
+            return $value[0];
485
+        }
486
+        return false;
487
+    }
488
+
489
+    /**
490
+     * returns the group's primary ID
491
+     * @param string $dn
492
+     * @return string|bool
493
+     */
494
+    public function getGroupPrimaryGroupID($dn) {
495
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
496
+    }
497
+
498
+    /**
499
+     * returns the user's primary group ID
500
+     * @param string $dn
501
+     * @return string|bool
502
+     */
503
+    public function getUserPrimaryGroupIDs($dn) {
504
+        $primaryGroupID = false;
505
+        if($this->access->connection->hasPrimaryGroups) {
506
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
507
+            if($primaryGroupID === false) {
508
+                $this->access->connection->hasPrimaryGroups = false;
509
+            }
510
+        }
511
+        return $primaryGroupID;
512
+    }
513
+
514
+    /**
515
+     * returns a filter for a "users in primary group" search or count operation
516
+     *
517
+     * @param string $groupDN
518
+     * @param string $search
519
+     * @return string
520
+     * @throws \Exception
521
+     */
522
+    private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
523
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
524
+        if($groupID === false) {
525
+            throw new \Exception('Not a valid group');
526
+        }
527
+
528
+        $filterParts = [];
529
+        $filterParts[] = $this->access->getFilterForUserCount();
530
+        if ($search !== '') {
531
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
532
+        }
533
+        $filterParts[] = 'primaryGroupID=' . $groupID;
534
+
535
+        return $this->access->combineFilterWithAnd($filterParts);
536
+    }
537
+
538
+    /**
539
+     * returns a list of users that have the given group as primary group
540
+     *
541
+     * @param string $groupDN
542
+     * @param string $search
543
+     * @param int $limit
544
+     * @param int $offset
545
+     * @return string[]
546
+     */
547
+    public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
548
+        try {
549
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
550
+            $users = $this->access->fetchListOfUsers(
551
+                $filter,
552
+                array($this->access->connection->ldapUserDisplayName, 'dn'),
553
+                $limit,
554
+                $offset
555
+            );
556
+            return $this->access->nextcloudUserNames($users);
557
+        } catch (\Exception $e) {
558
+            return array();
559
+        }
560
+    }
561
+
562
+    /**
563
+     * returns the number of users that have the given group as primary group
564
+     *
565
+     * @param string $groupDN
566
+     * @param string $search
567
+     * @param int $limit
568
+     * @param int $offset
569
+     * @return int
570
+     */
571
+    public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
572
+        try {
573
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
574
+            $users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
575
+            return (int)$users;
576
+        } catch (\Exception $e) {
577
+            return 0;
578
+        }
579
+    }
580
+
581
+    /**
582
+     * gets the primary group of a user
583
+     * @param string $dn
584
+     * @return string
585
+     */
586
+    public function getUserPrimaryGroup($dn) {
587
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
588
+        if($groupID !== false) {
589
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
590
+            if($groupName !== false) {
591
+                return $groupName;
592
+            }
593
+        }
594
+
595
+        return false;
596
+    }
597
+
598
+    /**
599
+     * Get all groups a user belongs to
600
+     * @param string $uid Name of the user
601
+     * @return array with group names
602
+     *
603
+     * This function fetches all groups a user belongs to. It does not check
604
+     * if the user exists at all.
605
+     *
606
+     * This function includes groups based on dynamic group membership.
607
+     */
608
+    public function getUserGroups($uid) {
609
+        if(!$this->enabled) {
610
+            return array();
611
+        }
612
+        $cacheKey = 'getUserGroups'.$uid;
613
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
614
+        if(!is_null($userGroups)) {
615
+            return $userGroups;
616
+        }
617
+        $userDN = $this->access->username2dn($uid);
618
+        if(!$userDN) {
619
+            $this->access->connection->writeToCache($cacheKey, array());
620
+            return array();
621
+        }
622
+
623
+        $groups = [];
624
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
625
+        $gidGroupName = $this->getUserGroupByGid($userDN);
626
+
627
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
628
+
629
+        if (!empty($dynamicGroupMemberURL)) {
630
+            // look through dynamic groups to add them to the result array if needed
631
+            $groupsToMatch = $this->access->fetchListOfGroups(
632
+                $this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
633
+            foreach($groupsToMatch as $dynamicGroup) {
634
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
635
+                    continue;
636
+                }
637
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
638
+                if ($pos !== false) {
639
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
640
+                    // apply filter via ldap search to see if this user is in this
641
+                    // dynamic group
642
+                    $userMatch = $this->access->readAttribute(
643
+                        $userDN,
644
+                        $this->access->connection->ldapUserDisplayName,
645
+                        $memberUrlFilter
646
+                    );
647
+                    if ($userMatch !== false) {
648
+                        // match found so this user is in this group
649
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
650
+                        if(is_string($groupName)) {
651
+                            // be sure to never return false if the dn could not be
652
+                            // resolved to a name, for whatever reason.
653
+                            $groups[] = $groupName;
654
+                        }
655
+                    }
656
+                } else {
657
+                    \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
658
+                        'of group ' . print_r($dynamicGroup, true), \OCP\Util::DEBUG);
659
+                }
660
+            }
661
+        }
662
+
663
+        // if possible, read out membership via memberOf. It's far faster than
664
+        // performing a search, which still is a fallback later.
665
+        // memberof doesn't support memberuid, so skip it here.
666
+        if((int)$this->access->connection->hasMemberOfFilterSupport === 1
667
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
668
+            && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
669
+            ) {
670
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
671
+            if (is_array($groupDNs)) {
672
+                foreach ($groupDNs as $dn) {
673
+                    $groupName = $this->access->dn2groupname($dn);
674
+                    if(is_string($groupName)) {
675
+                        // be sure to never return false if the dn could not be
676
+                        // resolved to a name, for whatever reason.
677
+                        $groups[] = $groupName;
678
+                    }
679
+                }
680
+            }
681
+
682
+            if($primaryGroup !== false) {
683
+                $groups[] = $primaryGroup;
684
+            }
685
+            if($gidGroupName !== false) {
686
+                $groups[] = $gidGroupName;
687
+            }
688
+            $this->access->connection->writeToCache($cacheKey, $groups);
689
+            return $groups;
690
+        }
691
+
692
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
693
+        if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
694
+            || (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
695
+        ) {
696
+            $uid = $userDN;
697
+        } else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
698
+            $result = $this->access->readAttribute($userDN, 'uid');
699
+            if ($result === false) {
700
+                \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
701
+                    $this->access->connection->ldapHost, \OCP\Util::DEBUG);
702
+            }
703
+            $uid = $result[0];
704
+        } else {
705
+            // just in case
706
+            $uid = $userDN;
707
+        }
708
+
709
+        if(isset($this->cachedGroupsByMember[$uid])) {
710
+            $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
711
+        } else {
712
+            $groupsByMember = array_values($this->getGroupsByMember($uid));
713
+            $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
714
+            $this->cachedGroupsByMember[$uid] = $groupsByMember;
715
+            $groups = array_merge($groups, $groupsByMember);
716
+        }
717
+
718
+        if($primaryGroup !== false) {
719
+            $groups[] = $primaryGroup;
720
+        }
721
+        if($gidGroupName !== false) {
722
+            $groups[] = $gidGroupName;
723
+        }
724
+
725
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
726
+        $this->access->connection->writeToCache($cacheKey, $groups);
727
+
728
+        return $groups;
729
+    }
730
+
731
+    /**
732
+     * @param string $dn
733
+     * @param array|null &$seen
734
+     * @return array
735
+     */
736
+    private function getGroupsByMember($dn, &$seen = null) {
737
+        if ($seen === null) {
738
+            $seen = array();
739
+        }
740
+        $allGroups = array();
741
+        if (array_key_exists($dn, $seen)) {
742
+            // avoid loops
743
+            return array();
744
+        }
745
+        $seen[$dn] = true;
746
+        $filter = $this->access->combineFilterWithAnd(array(
747
+            $this->access->connection->ldapGroupFilter,
748
+            $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
749
+        ));
750
+        $groups = $this->access->fetchListOfGroups($filter,
751
+            array($this->access->connection->ldapGroupDisplayName, 'dn'));
752
+        if (is_array($groups)) {
753
+            foreach ($groups as $groupobj) {
754
+                $groupDN = $groupobj['dn'][0];
755
+                $allGroups[$groupDN] = $groupobj;
756
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
757
+                if (!empty($nestedGroups)) {
758
+                    $supergroups = $this->getGroupsByMember($groupDN, $seen);
759
+                    if (is_array($supergroups) && (count($supergroups)>0)) {
760
+                        $allGroups = array_merge($allGroups, $supergroups);
761
+                    }
762
+                }
763
+            }
764
+        }
765
+        return $allGroups;
766
+    }
767
+
768
+    /**
769
+     * get a list of all users in a group
770
+     *
771
+     * @param string $gid
772
+     * @param string $search
773
+     * @param int $limit
774
+     * @param int $offset
775
+     * @return array with user ids
776
+     */
777
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
778
+        if(!$this->enabled) {
779
+            return array();
780
+        }
781
+        if(!$this->groupExists($gid)) {
782
+            return array();
783
+        }
784
+        $search = $this->access->escapeFilterPart($search, true);
785
+        $cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
786
+        // check for cache of the exact query
787
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
788
+        if(!is_null($groupUsers)) {
789
+            return $groupUsers;
790
+        }
791
+
792
+        // check for cache of the query without limit and offset
793
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
794
+        if(!is_null($groupUsers)) {
795
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
796
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
797
+            return $groupUsers;
798
+        }
799
+
800
+        if($limit === -1) {
801
+            $limit = null;
802
+        }
803
+        $groupDN = $this->access->groupname2dn($gid);
804
+        if(!$groupDN) {
805
+            // group couldn't be found, return empty resultset
806
+            $this->access->connection->writeToCache($cacheKey, array());
807
+            return array();
808
+        }
809
+
810
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
811
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
812
+        $members = array_keys($this->_groupMembers($groupDN));
813
+        if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
814
+            //in case users could not be retrieved, return empty result set
815
+            $this->access->connection->writeToCache($cacheKey, []);
816
+            return [];
817
+        }
818
+
819
+        $groupUsers = array();
820
+        $isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
821
+        $attrs = $this->access->userManager->getAttributes(true);
822
+        foreach($members as $member) {
823
+            if($isMemberUid) {
824
+                //we got uids, need to get their DNs to 'translate' them to user names
825
+                $filter = $this->access->combineFilterWithAnd(array(
826
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
827
+                    $this->access->getFilterPartForUserSearch($search)
828
+                ));
829
+                $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
830
+                if(count($ldap_users) < 1) {
831
+                    continue;
832
+                }
833
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
834
+            } else {
835
+                //we got DNs, check if we need to filter by search or we can give back all of them
836
+                if ($search !== '') {
837
+                    if(!$this->access->readAttribute($member,
838
+                        $this->access->connection->ldapUserDisplayName,
839
+                        $this->access->getFilterPartForUserSearch($search))) {
840
+                        continue;
841
+                    }
842
+                }
843
+                // dn2username will also check if the users belong to the allowed base
844
+                if($ocname = $this->access->dn2username($member)) {
845
+                    $groupUsers[] = $ocname;
846
+                }
847
+            }
848
+        }
849
+
850
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
851
+        natsort($groupUsers);
852
+        $this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
853
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
854
+
855
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
856
+
857
+        return $groupUsers;
858
+    }
859
+
860
+    /**
861
+     * returns the number of users in a group, who match the search term
862
+     * @param string $gid the internal group name
863
+     * @param string $search optional, a search string
864
+     * @return int|bool
865
+     */
866
+    public function countUsersInGroup($gid, $search = '') {
867
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
868
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
869
+        }
870
+
871
+        $cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
872
+        if(!$this->enabled || !$this->groupExists($gid)) {
873
+            return false;
874
+        }
875
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
876
+        if(!is_null($groupUsers)) {
877
+            return $groupUsers;
878
+        }
879
+
880
+        $groupDN = $this->access->groupname2dn($gid);
881
+        if(!$groupDN) {
882
+            // group couldn't be found, return empty result set
883
+            $this->access->connection->writeToCache($cacheKey, false);
884
+            return false;
885
+        }
886
+
887
+        $members = array_keys($this->_groupMembers($groupDN));
888
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
889
+        if(!$members && $primaryUserCount === 0) {
890
+            //in case users could not be retrieved, return empty result set
891
+            $this->access->connection->writeToCache($cacheKey, false);
892
+            return false;
893
+        }
894
+
895
+        if ($search === '') {
896
+            $groupUsers = count($members) + $primaryUserCount;
897
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
898
+            return $groupUsers;
899
+        }
900
+        $search = $this->access->escapeFilterPart($search, true);
901
+        $isMemberUid =
902
+            (strtolower($this->access->connection->ldapGroupMemberAssocAttr)
903
+            === 'memberuid');
904
+
905
+        //we need to apply the search filter
906
+        //alternatives that need to be checked:
907
+        //a) get all users by search filter and array_intersect them
908
+        //b) a, but only when less than 1k 10k ?k users like it is
909
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
910
+        //   and let it count.
911
+        //For now this is not important, because the only use of this method
912
+        //does not supply a search string
913
+        $groupUsers = array();
914
+        foreach($members as $member) {
915
+            if($isMemberUid) {
916
+                //we got uids, need to get their DNs to 'translate' them to user names
917
+                $filter = $this->access->combineFilterWithAnd(array(
918
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
919
+                    $this->access->getFilterPartForUserSearch($search)
920
+                ));
921
+                $ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
922
+                if(count($ldap_users) < 1) {
923
+                    continue;
924
+                }
925
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
926
+            } else {
927
+                //we need to apply the search filter now
928
+                if(!$this->access->readAttribute($member,
929
+                    $this->access->connection->ldapUserDisplayName,
930
+                    $this->access->getFilterPartForUserSearch($search))) {
931
+                    continue;
932
+                }
933
+                // dn2username will also check if the users belong to the allowed base
934
+                if($ocname = $this->access->dn2username($member)) {
935
+                    $groupUsers[] = $ocname;
936
+                }
937
+            }
938
+        }
939
+
940
+        //and get users that have the group as primary
941
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
942
+
943
+        return count($groupUsers) + $primaryUsers;
944
+    }
945
+
946
+    /**
947
+     * get a list of all groups
948
+     *
949
+     * @param string $search
950
+     * @param $limit
951
+     * @param int $offset
952
+     * @return array with group names
953
+     *
954
+     * Returns a list with all groups (used by getGroups)
955
+     */
956
+    protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
957
+        if(!$this->enabled) {
958
+            return array();
959
+        }
960
+        $cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
961
+
962
+        //Check cache before driving unnecessary searches
963
+        \OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, \OCP\Util::DEBUG);
964
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
965
+        if(!is_null($ldap_groups)) {
966
+            return $ldap_groups;
967
+        }
968
+
969
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
970
+        // error. With a limit of 0, we get 0 results. So we pass null.
971
+        if($limit <= 0) {
972
+            $limit = null;
973
+        }
974
+        $filter = $this->access->combineFilterWithAnd(array(
975
+            $this->access->connection->ldapGroupFilter,
976
+            $this->access->getFilterPartForGroupSearch($search)
977
+        ));
978
+        \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, \OCP\Util::DEBUG);
979
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
980
+                array($this->access->connection->ldapGroupDisplayName, 'dn'),
981
+                $limit,
982
+                $offset);
983
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
984
+
985
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
986
+        return $ldap_groups;
987
+    }
988
+
989
+    /**
990
+     * get a list of all groups using a paged search
991
+     *
992
+     * @param string $search
993
+     * @param int $limit
994
+     * @param int $offset
995
+     * @return array with group names
996
+     *
997
+     * Returns a list with all groups
998
+     * Uses a paged search if available to override a
999
+     * server side search limit.
1000
+     * (active directory has a limit of 1000 by default)
1001
+     */
1002
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
1003
+        if(!$this->enabled) {
1004
+            return array();
1005
+        }
1006
+        $search = $this->access->escapeFilterPart($search, true);
1007
+        $pagingSize = (int)$this->access->connection->ldapPagingSize;
1008
+        if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
1009
+            return $this->getGroupsChunk($search, $limit, $offset);
1010
+        }
1011
+        $maxGroups = 100000; // limit max results (just for safety reasons)
1012
+        if ($limit > -1) {
1013
+            $overallLimit = min($limit + $offset, $maxGroups);
1014
+        } else {
1015
+            $overallLimit = $maxGroups;
1016
+        }
1017
+        $chunkOffset = $offset;
1018
+        $allGroups = array();
1019
+        while ($chunkOffset < $overallLimit) {
1020
+            $chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1021
+            $ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1022
+            $nread = count($ldapGroups);
1023
+            \OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', \OCP\Util::DEBUG);
1024
+            if ($nread) {
1025
+                $allGroups = array_merge($allGroups, $ldapGroups);
1026
+                $chunkOffset += $nread;
1027
+            }
1028
+            if ($nread < $chunkLimit) {
1029
+                break;
1030
+            }
1031
+        }
1032
+        return $allGroups;
1033
+    }
1034
+
1035
+    /**
1036
+     * @param string $group
1037
+     * @return bool
1038
+     */
1039
+    public function groupMatchesFilter($group) {
1040
+        return (strripos($group, $this->groupSearch) !== false);
1041
+    }
1042
+
1043
+    /**
1044
+     * check if a group exists
1045
+     * @param string $gid
1046
+     * @return bool
1047
+     */
1048
+    public function groupExists($gid) {
1049
+        $groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1050
+        if(!is_null($groupExists)) {
1051
+            return (bool)$groupExists;
1052
+        }
1053
+
1054
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1055
+        //only, requires more checking.
1056
+        $dn = $this->access->groupname2dn($gid);
1057
+        if(!$dn) {
1058
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1059
+            return false;
1060
+        }
1061
+
1062
+        //if group really still exists, we will be able to read its objectclass
1063
+        if(!is_array($this->access->readAttribute($dn, ''))) {
1064
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1065
+            return false;
1066
+        }
1067
+
1068
+        $this->access->connection->writeToCache('groupExists'.$gid, true);
1069
+        return true;
1070
+    }
1071
+
1072
+    /**
1073
+     * Check if backend implements actions
1074
+     * @param int $actions bitwise-or'ed actions
1075
+     * @return boolean
1076
+     *
1077
+     * Returns the supported actions as int to be
1078
+     * compared with GroupInterface::CREATE_GROUP etc.
1079
+     */
1080
+    public function implementsActions($actions) {
1081
+        return (bool)((GroupInterface::COUNT_USERS |
1082
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1083
+    }
1084
+
1085
+    /**
1086
+     * Return access for LDAP interaction.
1087
+     * @return Access instance of Access for LDAP interaction
1088
+     */
1089
+    public function getLDAPAccess($gid) {
1090
+        return $this->access;
1091
+    }
1092
+
1093
+    /**
1094
+     * create a group
1095
+     * @param string $gid
1096
+     * @return bool
1097
+     * @throws \Exception
1098
+     */
1099
+    public function createGroup($gid) {
1100
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1101
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1102
+                //updates group mapping
1103
+                $this->access->dn2ocname($dn, $gid, false);
1104
+                $this->access->connection->writeToCache("groupExists".$gid, true);
1105
+            }
1106
+            return $dn != null;
1107
+        }
1108
+        throw new \Exception('Could not create group in LDAP backend.');
1109
+    }
1110
+
1111
+    /**
1112
+     * delete a group
1113
+     * @param string $gid gid of the group to delete
1114
+     * @return bool
1115
+     * @throws \Exception
1116
+     */
1117
+    public function deleteGroup($gid) {
1118
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1119
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1120
+                #delete group in nextcloud internal db
1121
+                $this->access->getGroupMapper()->unmap($gid);
1122
+                $this->access->connection->writeToCache("groupExists".$gid, false);
1123
+            }
1124
+            return $ret;
1125
+        }
1126
+        throw new \Exception('Could not delete group in LDAP backend.');
1127
+    }
1128
+
1129
+    /**
1130
+     * Add a user to a group
1131
+     * @param string $uid Name of the user to add to group
1132
+     * @param string $gid Name of the group in which add the user
1133
+     * @return bool
1134
+     * @throws \Exception
1135
+     */
1136
+    public function addToGroup($uid, $gid) {
1137
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1138
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1139
+                $this->access->connection->clearCache();
1140
+            }
1141
+            return $ret;
1142
+        }
1143
+        throw new \Exception('Could not add user to group in LDAP backend.');
1144
+    }
1145
+
1146
+    /**
1147
+     * Removes a user from a group
1148
+     * @param string $uid Name of the user to remove from group
1149
+     * @param string $gid Name of the group from which remove the user
1150
+     * @return bool
1151
+     * @throws \Exception
1152
+     */
1153
+    public function removeFromGroup($uid, $gid) {
1154
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1155
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1156
+                $this->access->connection->clearCache();
1157
+            }
1158
+            return $ret;
1159
+        }
1160
+        throw new \Exception('Could not remove user from group in LDAP backend.');
1161
+    }
1162
+
1163
+    /**
1164
+     * Gets group details
1165
+     * @param string $gid Name of the group
1166
+     * @return array | false
1167
+     * @throws \Exception
1168
+     */
1169
+    public function getGroupDetails($gid) {
1170
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1171
+            return $this->groupPluginManager->getGroupDetails($gid);
1172
+        }
1173
+        throw new \Exception('Could not get group details in LDAP backend.');
1174
+    }
1175
+
1176
+    /**
1177
+     * Return LDAP connection resource from a cloned connection.
1178
+     * The cloned connection needs to be closed manually.
1179
+     * of the current access.
1180
+     * @param string $gid
1181
+     * @return resource of the LDAP connection
1182
+     */
1183
+    public function getNewLDAPConnection($gid) {
1184
+        $connection = clone $this->access->getConnection();
1185
+        return $connection->getConnectionResource();
1186
+    }
1187 1187
 
1188 1188
 }
Please login to merge, or discard this patch.
Spacing   +94 added lines, -94 removed lines patch added patch discarded remove patch
@@ -64,7 +64,7 @@  discard block
 block discarded – undo
64 64
 		parent::__construct($access);
65 65
 		$filter = $this->access->connection->ldapGroupFilter;
66 66
 		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
67
-		if(!empty($filter) && !empty($gassoc)) {
67
+		if (!empty($filter) && !empty($gassoc)) {
68 68
 			$this->enabled = true;
69 69
 		}
70 70
 
@@ -82,25 +82,25 @@  discard block
 block discarded – undo
82 82
 	 * Checks whether the user is member of a group or not.
83 83
 	 */
84 84
 	public function inGroup($uid, $gid) {
85
-		if(!$this->enabled) {
85
+		if (!$this->enabled) {
86 86
 			return false;
87 87
 		}
88 88
 		$cacheKey = 'inGroup'.$uid.':'.$gid;
89 89
 		$inGroup = $this->access->connection->getFromCache($cacheKey);
90
-		if(!is_null($inGroup)) {
91
-			return (bool)$inGroup;
90
+		if (!is_null($inGroup)) {
91
+			return (bool) $inGroup;
92 92
 		}
93 93
 
94 94
 		$userDN = $this->access->username2dn($uid);
95 95
 
96
-		if(isset($this->cachedGroupMembers[$gid])) {
96
+		if (isset($this->cachedGroupMembers[$gid])) {
97 97
 			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
98 98
 			return $isInGroup;
99 99
 		}
100 100
 
101 101
 		$cacheKeyMembers = 'inGroup-members:'.$gid;
102 102
 		$members = $this->access->connection->getFromCache($cacheKeyMembers);
103
-		if(!is_null($members)) {
103
+		if (!is_null($members)) {
104 104
 			$this->cachedGroupMembers[$gid] = $members;
105 105
 			$isInGroup = in_array($userDN, $members);
106 106
 			$this->access->connection->writeToCache($cacheKey, $isInGroup);
@@ -109,13 +109,13 @@  discard block
 block discarded – undo
109 109
 
110 110
 		$groupDN = $this->access->groupname2dn($gid);
111 111
 		// just in case
112
-		if(!$groupDN || !$userDN) {
112
+		if (!$groupDN || !$userDN) {
113 113
 			$this->access->connection->writeToCache($cacheKey, false);
114 114
 			return false;
115 115
 		}
116 116
 
117 117
 		//check primary group first
118
-		if($gid === $this->getUserPrimaryGroup($userDN)) {
118
+		if ($gid === $this->getUserPrimaryGroup($userDN)) {
119 119
 			$this->access->connection->writeToCache($cacheKey, true);
120 120
 			return true;
121 121
 		}
@@ -123,21 +123,21 @@  discard block
 block discarded – undo
123 123
 		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
124 124
 		$members = $this->_groupMembers($groupDN);
125 125
 		$members = array_keys($members); // uids are returned as keys
126
-		if(!is_array($members) || count($members) === 0) {
126
+		if (!is_array($members) || count($members) === 0) {
127 127
 			$this->access->connection->writeToCache($cacheKey, false);
128 128
 			return false;
129 129
 		}
130 130
 
131 131
 		//extra work if we don't get back user DNs
132
-		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
132
+		if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
133 133
 			$dns = array();
134 134
 			$filterParts = array();
135 135
 			$bytes = 0;
136
-			foreach($members as $mid) {
136
+			foreach ($members as $mid) {
137 137
 				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
138 138
 				$filterParts[] = $filter;
139 139
 				$bytes += strlen($filter);
140
-				if($bytes >= 9000000) {
140
+				if ($bytes >= 9000000) {
141 141
 					// AD has a default input buffer of 10 MB, we do not want
142 142
 					// to take even the chance to exceed it
143 143
 					$filter = $this->access->combineFilterWithOr($filterParts);
@@ -147,7 +147,7 @@  discard block
 block discarded – undo
147 147
 					$dns = array_merge($dns, $users);
148 148
 				}
149 149
 			}
150
-			if(count($filterParts) > 0) {
150
+			if (count($filterParts) > 0) {
151 151
 				$filter = $this->access->combineFilterWithOr($filterParts);
152 152
 				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
153 153
 				$dns = array_merge($dns, $users);
@@ -190,14 +190,14 @@  discard block
 block discarded – undo
190 190
 			$pos = strpos($memberURLs[0], '(');
191 191
 			if ($pos !== false) {
192 192
 				$memberUrlFilter = substr($memberURLs[0], $pos);
193
-				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
193
+				$foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
194 194
 				$dynamicMembers = array();
195
-				foreach($foundMembers as $value) {
195
+				foreach ($foundMembers as $value) {
196 196
 					$dynamicMembers[$value['dn'][0]] = 1;
197 197
 				}
198 198
 			} else {
199 199
 				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
200
-					'of group ' . $dnGroup, \OCP\Util::DEBUG);
200
+					'of group '.$dnGroup, \OCP\Util::DEBUG);
201 201
 			}
202 202
 		}
203 203
 		return $dynamicMembers;
@@ -220,7 +220,7 @@  discard block
 block discarded – undo
220 220
 		// used extensively in cron job, caching makes sense for nested groups
221 221
 		$cacheKey = '_groupMembers'.$dnGroup;
222 222
 		$groupMembers = $this->access->connection->getFromCache($cacheKey);
223
-		if(!is_null($groupMembers)) {
223
+		if (!is_null($groupMembers)) {
224 224
 			return $groupMembers;
225 225
 		}
226 226
 		$seen[$dnGroup] = 1;
@@ -264,9 +264,9 @@  discard block
 block discarded – undo
264 264
 			return array();
265 265
 		}
266 266
 		$groups = $this->access->groupsMatchFilter($groups);
267
-		$allGroups =  $groups;
267
+		$allGroups = $groups;
268 268
 		$nestedGroups = $this->access->connection->ldapNestedGroups;
269
-		if ((int)$nestedGroups === 1) {
269
+		if ((int) $nestedGroups === 1) {
270 270
 			foreach ($groups as $group) {
271 271
 				$subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
272 272
 				$allGroups = array_merge($allGroups, $subGroups);
@@ -282,9 +282,9 @@  discard block
 block discarded – undo
282 282
 	 * @return string|bool
283 283
 	 */
284 284
 	public function gidNumber2Name($gid, $dn) {
285
-		$cacheKey = 'gidNumberToName' . $gid;
285
+		$cacheKey = 'gidNumberToName'.$gid;
286 286
 		$groupName = $this->access->connection->getFromCache($cacheKey);
287
-		if(!is_null($groupName) && isset($groupName)) {
287
+		if (!is_null($groupName) && isset($groupName)) {
288 288
 			return $groupName;
289 289
 		}
290 290
 
@@ -292,10 +292,10 @@  discard block
 block discarded – undo
292 292
 		$filter = $this->access->combineFilterWithAnd([
293 293
 			$this->access->connection->ldapGroupFilter,
294 294
 			'objectClass=posixGroup',
295
-			$this->access->connection->ldapGidNumber . '=' . $gid
295
+			$this->access->connection->ldapGidNumber.'='.$gid
296 296
 		]);
297 297
 		$result = $this->access->searchGroups($filter, array('dn'), 1);
298
-		if(empty($result)) {
298
+		if (empty($result)) {
299 299
 			return false;
300 300
 		}
301 301
 		$dn = $result[0]['dn'][0];
@@ -318,7 +318,7 @@  discard block
 block discarded – undo
318 318
 	 */
319 319
 	private function getEntryGidNumber($dn, $attribute) {
320 320
 		$value = $this->access->readAttribute($dn, $attribute);
321
-		if(is_array($value) && !empty($value)) {
321
+		if (is_array($value) && !empty($value)) {
322 322
 			return $value[0];
323 323
 		}
324 324
 		return false;
@@ -340,9 +340,9 @@  discard block
 block discarded – undo
340 340
 	 */
341 341
 	public function getUserGidNumber($dn) {
342 342
 		$gidNumber = false;
343
-		if($this->access->connection->hasGidNumber) {
343
+		if ($this->access->connection->hasGidNumber) {
344 344
 			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
345
-			if($gidNumber === false) {
345
+			if ($gidNumber === false) {
346 346
 				$this->access->connection->hasGidNumber = false;
347 347
 			}
348 348
 		}
@@ -359,7 +359,7 @@  discard block
 block discarded – undo
359 359
 	 */
360 360
 	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
361 361
 		$groupID = $this->getGroupGidNumber($groupDN);
362
-		if($groupID === false) {
362
+		if ($groupID === false) {
363 363
 			throw new \Exception('Not a valid group');
364 364
 		}
365 365
 
@@ -368,7 +368,7 @@  discard block
 block discarded – undo
368 368
 		if ($search !== '') {
369 369
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
370 370
 		}
371
-		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
371
+		$filterParts[] = $this->access->connection->ldapGidNumber.'='.$groupID;
372 372
 
373 373
 		return $this->access->combineFilterWithAnd($filterParts);
374 374
 	}
@@ -410,7 +410,7 @@  discard block
 block discarded – undo
410 410
 		try {
411 411
 			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
412 412
 			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
413
-			return (int)$users;
413
+			return (int) $users;
414 414
 		} catch (\Exception $e) {
415 415
 			return 0;
416 416
 		}
@@ -423,9 +423,9 @@  discard block
 block discarded – undo
423 423
 	 */
424 424
 	public function getUserGroupByGid($dn) {
425 425
 		$groupID = $this->getUserGidNumber($dn);
426
-		if($groupID !== false) {
426
+		if ($groupID !== false) {
427 427
 			$groupName = $this->gidNumber2Name($groupID, $dn);
428
-			if($groupName !== false) {
428
+			if ($groupName !== false) {
429 429
 				return $groupName;
430 430
 			}
431 431
 		}
@@ -442,22 +442,22 @@  discard block
 block discarded – undo
442 442
 	public function primaryGroupID2Name($gid, $dn) {
443 443
 		$cacheKey = 'primaryGroupIDtoName';
444 444
 		$groupNames = $this->access->connection->getFromCache($cacheKey);
445
-		if(!is_null($groupNames) && isset($groupNames[$gid])) {
445
+		if (!is_null($groupNames) && isset($groupNames[$gid])) {
446 446
 			return $groupNames[$gid];
447 447
 		}
448 448
 
449 449
 		$domainObjectSid = $this->access->getSID($dn);
450
-		if($domainObjectSid === false) {
450
+		if ($domainObjectSid === false) {
451 451
 			return false;
452 452
 		}
453 453
 
454 454
 		//we need to get the DN from LDAP
455 455
 		$filter = $this->access->combineFilterWithAnd(array(
456 456
 			$this->access->connection->ldapGroupFilter,
457
-			'objectsid=' . $domainObjectSid . '-' . $gid
457
+			'objectsid='.$domainObjectSid.'-'.$gid
458 458
 		));
459 459
 		$result = $this->access->searchGroups($filter, array('dn'), 1);
460
-		if(empty($result)) {
460
+		if (empty($result)) {
461 461
 			return false;
462 462
 		}
463 463
 		$dn = $result[0]['dn'][0];
@@ -480,7 +480,7 @@  discard block
 block discarded – undo
480 480
 	 */
481 481
 	private function getEntryGroupID($dn, $attribute) {
482 482
 		$value = $this->access->readAttribute($dn, $attribute);
483
-		if(is_array($value) && !empty($value)) {
483
+		if (is_array($value) && !empty($value)) {
484 484
 			return $value[0];
485 485
 		}
486 486
 		return false;
@@ -502,9 +502,9 @@  discard block
 block discarded – undo
502 502
 	 */
503 503
 	public function getUserPrimaryGroupIDs($dn) {
504 504
 		$primaryGroupID = false;
505
-		if($this->access->connection->hasPrimaryGroups) {
505
+		if ($this->access->connection->hasPrimaryGroups) {
506 506
 			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
507
-			if($primaryGroupID === false) {
507
+			if ($primaryGroupID === false) {
508 508
 				$this->access->connection->hasPrimaryGroups = false;
509 509
 			}
510 510
 		}
@@ -521,7 +521,7 @@  discard block
 block discarded – undo
521 521
 	 */
522 522
 	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
523 523
 		$groupID = $this->getGroupPrimaryGroupID($groupDN);
524
-		if($groupID === false) {
524
+		if ($groupID === false) {
525 525
 			throw new \Exception('Not a valid group');
526 526
 		}
527 527
 
@@ -530,7 +530,7 @@  discard block
 block discarded – undo
530 530
 		if ($search !== '') {
531 531
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
532 532
 		}
533
-		$filterParts[] = 'primaryGroupID=' . $groupID;
533
+		$filterParts[] = 'primaryGroupID='.$groupID;
534 534
 
535 535
 		return $this->access->combineFilterWithAnd($filterParts);
536 536
 	}
@@ -572,7 +572,7 @@  discard block
 block discarded – undo
572 572
 		try {
573 573
 			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
574 574
 			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
575
-			return (int)$users;
575
+			return (int) $users;
576 576
 		} catch (\Exception $e) {
577 577
 			return 0;
578 578
 		}
@@ -585,9 +585,9 @@  discard block
 block discarded – undo
585 585
 	 */
586 586
 	public function getUserPrimaryGroup($dn) {
587 587
 		$groupID = $this->getUserPrimaryGroupIDs($dn);
588
-		if($groupID !== false) {
588
+		if ($groupID !== false) {
589 589
 			$groupName = $this->primaryGroupID2Name($groupID, $dn);
590
-			if($groupName !== false) {
590
+			if ($groupName !== false) {
591 591
 				return $groupName;
592 592
 			}
593 593
 		}
@@ -606,16 +606,16 @@  discard block
 block discarded – undo
606 606
 	 * This function includes groups based on dynamic group membership.
607 607
 	 */
608 608
 	public function getUserGroups($uid) {
609
-		if(!$this->enabled) {
609
+		if (!$this->enabled) {
610 610
 			return array();
611 611
 		}
612 612
 		$cacheKey = 'getUserGroups'.$uid;
613 613
 		$userGroups = $this->access->connection->getFromCache($cacheKey);
614
-		if(!is_null($userGroups)) {
614
+		if (!is_null($userGroups)) {
615 615
 			return $userGroups;
616 616
 		}
617 617
 		$userDN = $this->access->username2dn($uid);
618
-		if(!$userDN) {
618
+		if (!$userDN) {
619 619
 			$this->access->connection->writeToCache($cacheKey, array());
620 620
 			return array();
621 621
 		}
@@ -629,14 +629,14 @@  discard block
 block discarded – undo
629 629
 		if (!empty($dynamicGroupMemberURL)) {
630 630
 			// look through dynamic groups to add them to the result array if needed
631 631
 			$groupsToMatch = $this->access->fetchListOfGroups(
632
-				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
633
-			foreach($groupsToMatch as $dynamicGroup) {
632
+				$this->access->connection->ldapGroupFilter, array('dn', $dynamicGroupMemberURL));
633
+			foreach ($groupsToMatch as $dynamicGroup) {
634 634
 				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
635 635
 					continue;
636 636
 				}
637 637
 				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
638 638
 				if ($pos !== false) {
639
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
639
+					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
640 640
 					// apply filter via ldap search to see if this user is in this
641 641
 					// dynamic group
642 642
 					$userMatch = $this->access->readAttribute(
@@ -647,7 +647,7 @@  discard block
 block discarded – undo
647 647
 					if ($userMatch !== false) {
648 648
 						// match found so this user is in this group
649 649
 						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
650
-						if(is_string($groupName)) {
650
+						if (is_string($groupName)) {
651 651
 							// be sure to never return false if the dn could not be
652 652
 							// resolved to a name, for whatever reason.
653 653
 							$groups[] = $groupName;
@@ -655,7 +655,7 @@  discard block
 block discarded – undo
655 655
 					}
656 656
 				} else {
657 657
 					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
658
-						'of group ' . print_r($dynamicGroup, true), \OCP\Util::DEBUG);
658
+						'of group '.print_r($dynamicGroup, true), \OCP\Util::DEBUG);
659 659
 				}
660 660
 			}
661 661
 		}
@@ -663,15 +663,15 @@  discard block
 block discarded – undo
663 663
 		// if possible, read out membership via memberOf. It's far faster than
664 664
 		// performing a search, which still is a fallback later.
665 665
 		// memberof doesn't support memberuid, so skip it here.
666
-		if((int)$this->access->connection->hasMemberOfFilterSupport === 1
667
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
666
+		if ((int) $this->access->connection->hasMemberOfFilterSupport === 1
667
+			&& (int) $this->access->connection->useMemberOfToDetectMembership === 1
668 668
 		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
669 669
 		    ) {
670 670
 			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
671 671
 			if (is_array($groupDNs)) {
672 672
 				foreach ($groupDNs as $dn) {
673 673
 					$groupName = $this->access->dn2groupname($dn);
674
-					if(is_string($groupName)) {
674
+					if (is_string($groupName)) {
675 675
 						// be sure to never return false if the dn could not be
676 676
 						// resolved to a name, for whatever reason.
677 677
 						$groups[] = $groupName;
@@ -679,10 +679,10 @@  discard block
 block discarded – undo
679 679
 				}
680 680
 			}
681 681
 
682
-			if($primaryGroup !== false) {
682
+			if ($primaryGroup !== false) {
683 683
 				$groups[] = $primaryGroup;
684 684
 			}
685
-			if($gidGroupName !== false) {
685
+			if ($gidGroupName !== false) {
686 686
 				$groups[] = $gidGroupName;
687 687
 			}
688 688
 			$this->access->connection->writeToCache($cacheKey, $groups);
@@ -690,14 +690,14 @@  discard block
 block discarded – undo
690 690
 		}
691 691
 
692 692
 		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
693
-		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
693
+		if ((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
694 694
 			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
695 695
 		) {
696 696
 			$uid = $userDN;
697
-		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
697
+		} else if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
698 698
 			$result = $this->access->readAttribute($userDN, 'uid');
699 699
 			if ($result === false) {
700
-				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
700
+				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN '.$userDN.' on '.
701 701
 					$this->access->connection->ldapHost, \OCP\Util::DEBUG);
702 702
 			}
703 703
 			$uid = $result[0];
@@ -706,7 +706,7 @@  discard block
 block discarded – undo
706 706
 			$uid = $userDN;
707 707
 		}
708 708
 
709
-		if(isset($this->cachedGroupsByMember[$uid])) {
709
+		if (isset($this->cachedGroupsByMember[$uid])) {
710 710
 			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
711 711
 		} else {
712 712
 			$groupsByMember = array_values($this->getGroupsByMember($uid));
@@ -715,10 +715,10 @@  discard block
 block discarded – undo
715 715
 			$groups = array_merge($groups, $groupsByMember);
716 716
 		}
717 717
 
718
-		if($primaryGroup !== false) {
718
+		if ($primaryGroup !== false) {
719 719
 			$groups[] = $primaryGroup;
720 720
 		}
721
-		if($gidGroupName !== false) {
721
+		if ($gidGroupName !== false) {
722 722
 			$groups[] = $gidGroupName;
723 723
 		}
724 724
 
@@ -756,7 +756,7 @@  discard block
 block discarded – undo
756 756
 				$nestedGroups = $this->access->connection->ldapNestedGroups;
757 757
 				if (!empty($nestedGroups)) {
758 758
 					$supergroups = $this->getGroupsByMember($groupDN, $seen);
759
-					if (is_array($supergroups) && (count($supergroups)>0)) {
759
+					if (is_array($supergroups) && (count($supergroups) > 0)) {
760 760
 						$allGroups = array_merge($allGroups, $supergroups);
761 761
 					}
762 762
 				}
@@ -775,33 +775,33 @@  discard block
 block discarded – undo
775 775
 	 * @return array with user ids
776 776
 	 */
777 777
 	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
778
-		if(!$this->enabled) {
778
+		if (!$this->enabled) {
779 779
 			return array();
780 780
 		}
781
-		if(!$this->groupExists($gid)) {
781
+		if (!$this->groupExists($gid)) {
782 782
 			return array();
783 783
 		}
784 784
 		$search = $this->access->escapeFilterPart($search, true);
785 785
 		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
786 786
 		// check for cache of the exact query
787 787
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
788
-		if(!is_null($groupUsers)) {
788
+		if (!is_null($groupUsers)) {
789 789
 			return $groupUsers;
790 790
 		}
791 791
 
792 792
 		// check for cache of the query without limit and offset
793 793
 		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
794
-		if(!is_null($groupUsers)) {
794
+		if (!is_null($groupUsers)) {
795 795
 			$groupUsers = array_slice($groupUsers, $offset, $limit);
796 796
 			$this->access->connection->writeToCache($cacheKey, $groupUsers);
797 797
 			return $groupUsers;
798 798
 		}
799 799
 
800
-		if($limit === -1) {
800
+		if ($limit === -1) {
801 801
 			$limit = null;
802 802
 		}
803 803
 		$groupDN = $this->access->groupname2dn($gid);
804
-		if(!$groupDN) {
804
+		if (!$groupDN) {
805 805
 			// group couldn't be found, return empty resultset
806 806
 			$this->access->connection->writeToCache($cacheKey, array());
807 807
 			return array();
@@ -810,7 +810,7 @@  discard block
 block discarded – undo
810 810
 		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
811 811
 		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
812 812
 		$members = array_keys($this->_groupMembers($groupDN));
813
-		if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
813
+		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
814 814
 			//in case users could not be retrieved, return empty result set
815 815
 			$this->access->connection->writeToCache($cacheKey, []);
816 816
 			return [];
@@ -819,29 +819,29 @@  discard block
 block discarded – undo
819 819
 		$groupUsers = array();
820 820
 		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
821 821
 		$attrs = $this->access->userManager->getAttributes(true);
822
-		foreach($members as $member) {
823
-			if($isMemberUid) {
822
+		foreach ($members as $member) {
823
+			if ($isMemberUid) {
824 824
 				//we got uids, need to get their DNs to 'translate' them to user names
825 825
 				$filter = $this->access->combineFilterWithAnd(array(
826 826
 					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
827 827
 					$this->access->getFilterPartForUserSearch($search)
828 828
 				));
829 829
 				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
830
-				if(count($ldap_users) < 1) {
830
+				if (count($ldap_users) < 1) {
831 831
 					continue;
832 832
 				}
833 833
 				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
834 834
 			} else {
835 835
 				//we got DNs, check if we need to filter by search or we can give back all of them
836 836
 				if ($search !== '') {
837
-					if(!$this->access->readAttribute($member,
837
+					if (!$this->access->readAttribute($member,
838 838
 						$this->access->connection->ldapUserDisplayName,
839 839
 						$this->access->getFilterPartForUserSearch($search))) {
840 840
 						continue;
841 841
 					}
842 842
 				}
843 843
 				// dn2username will also check if the users belong to the allowed base
844
-				if($ocname = $this->access->dn2username($member)) {
844
+				if ($ocname = $this->access->dn2username($member)) {
845 845
 					$groupUsers[] = $ocname;
846 846
 				}
847 847
 			}
@@ -869,16 +869,16 @@  discard block
 block discarded – undo
869 869
 		}
870 870
 
871 871
 		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
872
-		if(!$this->enabled || !$this->groupExists($gid)) {
872
+		if (!$this->enabled || !$this->groupExists($gid)) {
873 873
 			return false;
874 874
 		}
875 875
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
876
-		if(!is_null($groupUsers)) {
876
+		if (!is_null($groupUsers)) {
877 877
 			return $groupUsers;
878 878
 		}
879 879
 
880 880
 		$groupDN = $this->access->groupname2dn($gid);
881
-		if(!$groupDN) {
881
+		if (!$groupDN) {
882 882
 			// group couldn't be found, return empty result set
883 883
 			$this->access->connection->writeToCache($cacheKey, false);
884 884
 			return false;
@@ -886,7 +886,7 @@  discard block
 block discarded – undo
886 886
 
887 887
 		$members = array_keys($this->_groupMembers($groupDN));
888 888
 		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
889
-		if(!$members && $primaryUserCount === 0) {
889
+		if (!$members && $primaryUserCount === 0) {
890 890
 			//in case users could not be retrieved, return empty result set
891 891
 			$this->access->connection->writeToCache($cacheKey, false);
892 892
 			return false;
@@ -911,27 +911,27 @@  discard block
 block discarded – undo
911 911
 		//For now this is not important, because the only use of this method
912 912
 		//does not supply a search string
913 913
 		$groupUsers = array();
914
-		foreach($members as $member) {
915
-			if($isMemberUid) {
914
+		foreach ($members as $member) {
915
+			if ($isMemberUid) {
916 916
 				//we got uids, need to get their DNs to 'translate' them to user names
917 917
 				$filter = $this->access->combineFilterWithAnd(array(
918 918
 					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
919 919
 					$this->access->getFilterPartForUserSearch($search)
920 920
 				));
921 921
 				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
922
-				if(count($ldap_users) < 1) {
922
+				if (count($ldap_users) < 1) {
923 923
 					continue;
924 924
 				}
925 925
 				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
926 926
 			} else {
927 927
 				//we need to apply the search filter now
928
-				if(!$this->access->readAttribute($member,
928
+				if (!$this->access->readAttribute($member,
929 929
 					$this->access->connection->ldapUserDisplayName,
930 930
 					$this->access->getFilterPartForUserSearch($search))) {
931 931
 					continue;
932 932
 				}
933 933
 				// dn2username will also check if the users belong to the allowed base
934
-				if($ocname = $this->access->dn2username($member)) {
934
+				if ($ocname = $this->access->dn2username($member)) {
935 935
 					$groupUsers[] = $ocname;
936 936
 				}
937 937
 			}
@@ -954,7 +954,7 @@  discard block
 block discarded – undo
954 954
 	 * Returns a list with all groups (used by getGroups)
955 955
 	 */
956 956
 	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
957
-		if(!$this->enabled) {
957
+		if (!$this->enabled) {
958 958
 			return array();
959 959
 		}
960 960
 		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
@@ -962,13 +962,13 @@  discard block
 block discarded – undo
962 962
 		//Check cache before driving unnecessary searches
963 963
 		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, \OCP\Util::DEBUG);
964 964
 		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
965
-		if(!is_null($ldap_groups)) {
965
+		if (!is_null($ldap_groups)) {
966 966
 			return $ldap_groups;
967 967
 		}
968 968
 
969 969
 		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
970 970
 		// error. With a limit of 0, we get 0 results. So we pass null.
971
-		if($limit <= 0) {
971
+		if ($limit <= 0) {
972 972
 			$limit = null;
973 973
 		}
974 974
 		$filter = $this->access->combineFilterWithAnd(array(
@@ -1000,11 +1000,11 @@  discard block
 block discarded – undo
1000 1000
 	 * (active directory has a limit of 1000 by default)
1001 1001
 	 */
1002 1002
 	public function getGroups($search = '', $limit = -1, $offset = 0) {
1003
-		if(!$this->enabled) {
1003
+		if (!$this->enabled) {
1004 1004
 			return array();
1005 1005
 		}
1006 1006
 		$search = $this->access->escapeFilterPart($search, true);
1007
-		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1007
+		$pagingSize = (int) $this->access->connection->ldapPagingSize;
1008 1008
 		if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
1009 1009
 			return $this->getGroupsChunk($search, $limit, $offset);
1010 1010
 		}
@@ -1047,20 +1047,20 @@  discard block
 block discarded – undo
1047 1047
 	 */
1048 1048
 	public function groupExists($gid) {
1049 1049
 		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1050
-		if(!is_null($groupExists)) {
1051
-			return (bool)$groupExists;
1050
+		if (!is_null($groupExists)) {
1051
+			return (bool) $groupExists;
1052 1052
 		}
1053 1053
 
1054 1054
 		//getting dn, if false the group does not exist. If dn, it may be mapped
1055 1055
 		//only, requires more checking.
1056 1056
 		$dn = $this->access->groupname2dn($gid);
1057
-		if(!$dn) {
1057
+		if (!$dn) {
1058 1058
 			$this->access->connection->writeToCache('groupExists'.$gid, false);
1059 1059
 			return false;
1060 1060
 		}
1061 1061
 
1062 1062
 		//if group really still exists, we will be able to read its objectclass
1063
-		if(!is_array($this->access->readAttribute($dn, ''))) {
1063
+		if (!is_array($this->access->readAttribute($dn, ''))) {
1064 1064
 			$this->access->connection->writeToCache('groupExists'.$gid, false);
1065 1065
 			return false;
1066 1066
 		}
@@ -1078,7 +1078,7 @@  discard block
 block discarded – undo
1078 1078
 	* compared with GroupInterface::CREATE_GROUP etc.
1079 1079
 	*/
1080 1080
 	public function implementsActions($actions) {
1081
-		return (bool)((GroupInterface::COUNT_USERS |
1081
+		return (bool) ((GroupInterface::COUNT_USERS |
1082 1082
 				$this->groupPluginManager->getImplementedActions()) & $actions);
1083 1083
 	}
1084 1084
 
Please login to merge, or discard this patch.
apps/user_ldap/lib/Jobs/CleanUp.php 1 patch
Indentation   +190 added lines, -190 removed lines patch added patch discarded remove patch
@@ -43,195 +43,195 @@
 block discarded – undo
43 43
  * @package OCA\User_LDAP\Jobs;
44 44
  */
45 45
 class CleanUp extends TimedJob {
46
-	/** @var int $limit amount of users that should be checked per run */
47
-	protected $limit = 50;
48
-
49
-	/** @var int $defaultIntervalMin default interval in minutes */
50
-	protected $defaultIntervalMin = 51;
51
-
52
-	/** @var User_LDAP|User_Proxy $userBackend */
53
-	protected $userBackend;
54
-
55
-	/** @var \OCP\IConfig $ocConfig */
56
-	protected $ocConfig;
57
-
58
-	/** @var \OCP\IDBConnection $db */
59
-	protected $db;
60
-
61
-	/** @var Helper $ldapHelper */
62
-	protected $ldapHelper;
63
-
64
-	/** @var \OCA\User_LDAP\Mapping\UserMapping */
65
-	protected $mapping;
66
-
67
-	/** @var \OCA\User_LDAP\User\DeletedUsersIndex */
68
-	protected $dui;
69
-
70
-	public function __construct() {
71
-		$minutes = \OC::$server->getConfig()->getSystemValue(
72
-			'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
73
-		$this->setInterval((int)$minutes * 60);
74
-	}
75
-
76
-	/**
77
-	 * assigns the instances passed to run() to the class properties
78
-	 * @param array $arguments
79
-	 */
80
-	public function setArguments($arguments) {
81
-		//Dependency Injection is not possible, because the constructor will
82
-		//only get values that are serialized to JSON. I.e. whatever we would
83
-		//pass in app.php we do add here, except something else is passed e.g.
84
-		//in tests.
85
-
86
-		if(isset($arguments['helper'])) {
87
-			$this->ldapHelper = $arguments['helper'];
88
-		} else {
89
-			$this->ldapHelper = new Helper(\OC::$server->getConfig());
90
-		}
91
-
92
-		if(isset($arguments['ocConfig'])) {
93
-			$this->ocConfig = $arguments['ocConfig'];
94
-		} else {
95
-			$this->ocConfig = \OC::$server->getConfig();
96
-		}
97
-
98
-		if(isset($arguments['userBackend'])) {
99
-			$this->userBackend = $arguments['userBackend'];
100
-		} else {
101
-			$this->userBackend =  new User_Proxy(
102
-				$this->ldapHelper->getServerConfigurationPrefixes(true),
103
-				new LDAP(),
104
-				$this->ocConfig,
105
-				\OC::$server->getNotificationManager(),
106
-				\OC::$server->getUserSession(),
107
-				\OC::$server->query('LDAPUserPluginManager')
108
-			);
109
-		}
110
-
111
-		if(isset($arguments['db'])) {
112
-			$this->db = $arguments['db'];
113
-		} else {
114
-			$this->db = \OC::$server->getDatabaseConnection();
115
-		}
116
-
117
-		if(isset($arguments['mapping'])) {
118
-			$this->mapping = $arguments['mapping'];
119
-		} else {
120
-			$this->mapping = new UserMapping($this->db);
121
-		}
122
-
123
-		if(isset($arguments['deletedUsersIndex'])) {
124
-			$this->dui = $arguments['deletedUsersIndex'];
125
-		} else {
126
-			$this->dui = new DeletedUsersIndex(
127
-				$this->ocConfig, $this->db, $this->mapping);
128
-		}
129
-	}
130
-
131
-	/**
132
-	 * makes the background job do its work
133
-	 * @param array $argument
134
-	 */
135
-	public function run($argument) {
136
-		$this->setArguments($argument);
137
-
138
-		if(!$this->isCleanUpAllowed()) {
139
-			return;
140
-		}
141
-		$users = $this->mapping->getList($this->getOffset(), $this->limit);
142
-		if(!is_array($users)) {
143
-			//something wrong? Let's start from the beginning next time and
144
-			//abort
145
-			$this->setOffset(true);
146
-			return;
147
-		}
148
-		$resetOffset = $this->isOffsetResetNecessary(count($users));
149
-		$this->checkUsers($users);
150
-		$this->setOffset($resetOffset);
151
-	}
152
-
153
-	/**
154
-	 * checks whether next run should start at 0 again
155
-	 * @param int $resultCount
156
-	 * @return bool
157
-	 */
158
-	public function isOffsetResetNecessary($resultCount) {
159
-		return $resultCount < $this->limit;
160
-	}
161
-
162
-	/**
163
-	 * checks whether cleaning up LDAP users is allowed
164
-	 * @return bool
165
-	 */
166
-	public function isCleanUpAllowed() {
167
-		try {
168
-			if($this->ldapHelper->haveDisabledConfigurations()) {
169
-				return false;
170
-			}
171
-		} catch (\Exception $e) {
172
-			return false;
173
-		}
174
-
175
-		return $this->isCleanUpEnabled();
176
-	}
177
-
178
-	/**
179
-	 * checks whether clean up is enabled by configuration
180
-	 * @return bool
181
-	 */
182
-	private function isCleanUpEnabled() {
183
-		return (bool)$this->ocConfig->getSystemValue(
184
-			'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
185
-	}
186
-
187
-	/**
188
-	 * checks users whether they are still existing
189
-	 * @param array $users result from getMappedUsers()
190
-	 */
191
-	private function checkUsers(array $users) {
192
-		foreach($users as $user) {
193
-			$this->checkUser($user);
194
-		}
195
-	}
196
-
197
-	/**
198
-	 * checks whether a user is still existing in LDAP
199
-	 * @param string[] $user
200
-	 */
201
-	private function checkUser(array $user) {
202
-		if($this->userBackend->userExistsOnLDAP($user['name'])) {
203
-			//still available, all good
204
-
205
-			return;
206
-		}
207
-
208
-		$this->dui->markUser($user['name']);
209
-	}
210
-
211
-	/**
212
-	 * gets the offset to fetch users from the mappings table
213
-	 * @return int
214
-	 */
215
-	private function getOffset() {
216
-		return (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobOffset', 0);
217
-	}
218
-
219
-	/**
220
-	 * sets the new offset for the next run
221
-	 * @param bool $reset whether the offset should be set to 0
222
-	 */
223
-	public function setOffset($reset = false) {
224
-		$newOffset = $reset ? 0 :
225
-			$this->getOffset() + $this->limit;
226
-		$this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', $newOffset);
227
-	}
228
-
229
-	/**
230
-	 * returns the chunk size (limit in DB speak)
231
-	 * @return int
232
-	 */
233
-	public function getChunkSize() {
234
-		return $this->limit;
235
-	}
46
+    /** @var int $limit amount of users that should be checked per run */
47
+    protected $limit = 50;
48
+
49
+    /** @var int $defaultIntervalMin default interval in minutes */
50
+    protected $defaultIntervalMin = 51;
51
+
52
+    /** @var User_LDAP|User_Proxy $userBackend */
53
+    protected $userBackend;
54
+
55
+    /** @var \OCP\IConfig $ocConfig */
56
+    protected $ocConfig;
57
+
58
+    /** @var \OCP\IDBConnection $db */
59
+    protected $db;
60
+
61
+    /** @var Helper $ldapHelper */
62
+    protected $ldapHelper;
63
+
64
+    /** @var \OCA\User_LDAP\Mapping\UserMapping */
65
+    protected $mapping;
66
+
67
+    /** @var \OCA\User_LDAP\User\DeletedUsersIndex */
68
+    protected $dui;
69
+
70
+    public function __construct() {
71
+        $minutes = \OC::$server->getConfig()->getSystemValue(
72
+            'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
73
+        $this->setInterval((int)$minutes * 60);
74
+    }
75
+
76
+    /**
77
+     * assigns the instances passed to run() to the class properties
78
+     * @param array $arguments
79
+     */
80
+    public function setArguments($arguments) {
81
+        //Dependency Injection is not possible, because the constructor will
82
+        //only get values that are serialized to JSON. I.e. whatever we would
83
+        //pass in app.php we do add here, except something else is passed e.g.
84
+        //in tests.
85
+
86
+        if(isset($arguments['helper'])) {
87
+            $this->ldapHelper = $arguments['helper'];
88
+        } else {
89
+            $this->ldapHelper = new Helper(\OC::$server->getConfig());
90
+        }
91
+
92
+        if(isset($arguments['ocConfig'])) {
93
+            $this->ocConfig = $arguments['ocConfig'];
94
+        } else {
95
+            $this->ocConfig = \OC::$server->getConfig();
96
+        }
97
+
98
+        if(isset($arguments['userBackend'])) {
99
+            $this->userBackend = $arguments['userBackend'];
100
+        } else {
101
+            $this->userBackend =  new User_Proxy(
102
+                $this->ldapHelper->getServerConfigurationPrefixes(true),
103
+                new LDAP(),
104
+                $this->ocConfig,
105
+                \OC::$server->getNotificationManager(),
106
+                \OC::$server->getUserSession(),
107
+                \OC::$server->query('LDAPUserPluginManager')
108
+            );
109
+        }
110
+
111
+        if(isset($arguments['db'])) {
112
+            $this->db = $arguments['db'];
113
+        } else {
114
+            $this->db = \OC::$server->getDatabaseConnection();
115
+        }
116
+
117
+        if(isset($arguments['mapping'])) {
118
+            $this->mapping = $arguments['mapping'];
119
+        } else {
120
+            $this->mapping = new UserMapping($this->db);
121
+        }
122
+
123
+        if(isset($arguments['deletedUsersIndex'])) {
124
+            $this->dui = $arguments['deletedUsersIndex'];
125
+        } else {
126
+            $this->dui = new DeletedUsersIndex(
127
+                $this->ocConfig, $this->db, $this->mapping);
128
+        }
129
+    }
130
+
131
+    /**
132
+     * makes the background job do its work
133
+     * @param array $argument
134
+     */
135
+    public function run($argument) {
136
+        $this->setArguments($argument);
137
+
138
+        if(!$this->isCleanUpAllowed()) {
139
+            return;
140
+        }
141
+        $users = $this->mapping->getList($this->getOffset(), $this->limit);
142
+        if(!is_array($users)) {
143
+            //something wrong? Let's start from the beginning next time and
144
+            //abort
145
+            $this->setOffset(true);
146
+            return;
147
+        }
148
+        $resetOffset = $this->isOffsetResetNecessary(count($users));
149
+        $this->checkUsers($users);
150
+        $this->setOffset($resetOffset);
151
+    }
152
+
153
+    /**
154
+     * checks whether next run should start at 0 again
155
+     * @param int $resultCount
156
+     * @return bool
157
+     */
158
+    public function isOffsetResetNecessary($resultCount) {
159
+        return $resultCount < $this->limit;
160
+    }
161
+
162
+    /**
163
+     * checks whether cleaning up LDAP users is allowed
164
+     * @return bool
165
+     */
166
+    public function isCleanUpAllowed() {
167
+        try {
168
+            if($this->ldapHelper->haveDisabledConfigurations()) {
169
+                return false;
170
+            }
171
+        } catch (\Exception $e) {
172
+            return false;
173
+        }
174
+
175
+        return $this->isCleanUpEnabled();
176
+    }
177
+
178
+    /**
179
+     * checks whether clean up is enabled by configuration
180
+     * @return bool
181
+     */
182
+    private function isCleanUpEnabled() {
183
+        return (bool)$this->ocConfig->getSystemValue(
184
+            'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
185
+    }
186
+
187
+    /**
188
+     * checks users whether they are still existing
189
+     * @param array $users result from getMappedUsers()
190
+     */
191
+    private function checkUsers(array $users) {
192
+        foreach($users as $user) {
193
+            $this->checkUser($user);
194
+        }
195
+    }
196
+
197
+    /**
198
+     * checks whether a user is still existing in LDAP
199
+     * @param string[] $user
200
+     */
201
+    private function checkUser(array $user) {
202
+        if($this->userBackend->userExistsOnLDAP($user['name'])) {
203
+            //still available, all good
204
+
205
+            return;
206
+        }
207
+
208
+        $this->dui->markUser($user['name']);
209
+    }
210
+
211
+    /**
212
+     * gets the offset to fetch users from the mappings table
213
+     * @return int
214
+     */
215
+    private function getOffset() {
216
+        return (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobOffset', 0);
217
+    }
218
+
219
+    /**
220
+     * sets the new offset for the next run
221
+     * @param bool $reset whether the offset should be set to 0
222
+     */
223
+    public function setOffset($reset = false) {
224
+        $newOffset = $reset ? 0 :
225
+            $this->getOffset() + $this->limit;
226
+        $this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', $newOffset);
227
+    }
228
+
229
+    /**
230
+     * returns the chunk size (limit in DB speak)
231
+     * @return int
232
+     */
233
+    public function getChunkSize() {
234
+        return $this->limit;
235
+    }
236 236
 
237 237
 }
Please login to merge, or discard this patch.
apps/dav/lib/Upload/UploadHome.php 2 patches
Indentation   +52 added lines, -52 removed lines patch added patch discarded remove patch
@@ -30,66 +30,66 @@
 block discarded – undo
30 30
 use Sabre\DAV\ICollection;
31 31
 
32 32
 class UploadHome implements ICollection {
33
-	/**
34
-	 * UploadHome constructor.
35
-	 *
36
-	 * @param array $principalInfo
37
-	 */
38
-	public function __construct($principalInfo) {
39
-		$this->principalInfo = $principalInfo;
40
-	}
33
+    /**
34
+     * UploadHome constructor.
35
+     *
36
+     * @param array $principalInfo
37
+     */
38
+    public function __construct($principalInfo) {
39
+        $this->principalInfo = $principalInfo;
40
+    }
41 41
 
42
-	function createFile($name, $data = null) {
43
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
44
-	}
42
+    function createFile($name, $data = null) {
43
+        throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
44
+    }
45 45
 
46
-	function createDirectory($name) {
47
-		$this->impl()->createDirectory($name);
48
-	}
46
+    function createDirectory($name) {
47
+        $this->impl()->createDirectory($name);
48
+    }
49 49
 
50
-	function getChild($name) {
51
-		return new UploadFolder($this->impl()->getChild($name));
52
-	}
50
+    function getChild($name) {
51
+        return new UploadFolder($this->impl()->getChild($name));
52
+    }
53 53
 
54
-	function getChildren() {
55
-		return array_map(function($node) {
56
-			return new UploadFolder($node);
57
-		}, $this->impl()->getChildren());
58
-	}
54
+    function getChildren() {
55
+        return array_map(function($node) {
56
+            return new UploadFolder($node);
57
+        }, $this->impl()->getChildren());
58
+    }
59 59
 
60
-	function childExists($name) {
61
-		return !is_null($this->getChild($name));
62
-	}
60
+    function childExists($name) {
61
+        return !is_null($this->getChild($name));
62
+    }
63 63
 
64
-	function delete() {
65
-		$this->impl()->delete();
66
-	}
64
+    function delete() {
65
+        $this->impl()->delete();
66
+    }
67 67
 
68
-	function getName() {
69
-		list(,$name) = \Sabre\Uri\split($this->principalInfo['uri']);
70
-		return $name;
71
-	}
68
+    function getName() {
69
+        list(,$name) = \Sabre\Uri\split($this->principalInfo['uri']);
70
+        return $name;
71
+    }
72 72
 
73
-	function setName($name) {
74
-		throw new Forbidden('Permission denied to rename this folder');
75
-	}
73
+    function setName($name) {
74
+        throw new Forbidden('Permission denied to rename this folder');
75
+    }
76 76
 
77
-	function getLastModified() {
78
-		return $this->impl()->getLastModified();
79
-	}
77
+    function getLastModified() {
78
+        return $this->impl()->getLastModified();
79
+    }
80 80
 
81
-	/**
82
-	 * @return Directory
83
-	 */
84
-	private function impl() {
85
-		$rootView = new View();
86
-		$user = \OC::$server->getUserSession()->getUser();
87
-		Filesystem::initMountPoints($user->getUID());
88
-		if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
89
-			$rootView->mkdir('/' . $user->getUID() . '/uploads');
90
-		}
91
-		$view = new View('/' . $user->getUID() . '/uploads');
92
-		$rootInfo = $view->getFileInfo('');
93
-		return new Directory($view, $rootInfo);
94
-	}
81
+    /**
82
+     * @return Directory
83
+     */
84
+    private function impl() {
85
+        $rootView = new View();
86
+        $user = \OC::$server->getUserSession()->getUser();
87
+        Filesystem::initMountPoints($user->getUID());
88
+        if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
89
+            $rootView->mkdir('/' . $user->getUID() . '/uploads');
90
+        }
91
+        $view = new View('/' . $user->getUID() . '/uploads');
92
+        $rootInfo = $view->getFileInfo('');
93
+        return new Directory($view, $rootInfo);
94
+    }
95 95
 }
Please login to merge, or discard this patch.
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -40,7 +40,7 @@  discard block
 block discarded – undo
40 40
 	}
41 41
 
42 42
 	function createFile($name, $data = null) {
43
-		throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
43
+		throw new Forbidden('Permission denied to create file (filename '.$name.')');
44 44
 	}
45 45
 
46 46
 	function createDirectory($name) {
@@ -85,10 +85,10 @@  discard block
 block discarded – undo
85 85
 		$rootView = new View();
86 86
 		$user = \OC::$server->getUserSession()->getUser();
87 87
 		Filesystem::initMountPoints($user->getUID());
88
-		if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
89
-			$rootView->mkdir('/' . $user->getUID() . '/uploads');
88
+		if (!$rootView->file_exists('/'.$user->getUID().'/uploads')) {
89
+			$rootView->mkdir('/'.$user->getUID().'/uploads');
90 90
 		}
91
-		$view = new View('/' . $user->getUID() . '/uploads');
91
+		$view = new View('/'.$user->getUID().'/uploads');
92 92
 		$rootInfo = $view->getFileInfo('');
93 93
 		return new Directory($view, $rootInfo);
94 94
 	}
Please login to merge, or discard this patch.
apps/dav/lib/CardDAV/SyncService.php 1 patch
Indentation   +294 added lines, -294 removed lines patch added patch discarded remove patch
@@ -39,300 +39,300 @@
 block discarded – undo
39 39
 
40 40
 class SyncService {
41 41
 
42
-	/** @var CardDavBackend */
43
-	private $backend;
44
-
45
-	/** @var IUserManager */
46
-	private $userManager;
47
-
48
-	/** @var ILogger */
49
-	private $logger;
50
-
51
-	/** @var array */
52
-	private $localSystemAddressBook;
53
-
54
-	/** @var AccountManager */
55
-	private $accountManager;
56
-
57
-	/** @var string */
58
-	protected $certPath;
59
-
60
-	/**
61
-	 * SyncService constructor.
62
-	 *
63
-	 * @param CardDavBackend $backend
64
-	 * @param IUserManager $userManager
65
-	 * @param ILogger $logger
66
-	 * @param AccountManager $accountManager
67
-	 */
68
-	public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger, AccountManager $accountManager) {
69
-		$this->backend = $backend;
70
-		$this->userManager = $userManager;
71
-		$this->logger = $logger;
72
-		$this->accountManager = $accountManager;
73
-		$this->certPath = '';
74
-	}
75
-
76
-	/**
77
-	 * @param string $url
78
-	 * @param string $userName
79
-	 * @param string $addressBookUrl
80
-	 * @param string $sharedSecret
81
-	 * @param string $syncToken
82
-	 * @param int $targetBookId
83
-	 * @param string $targetPrincipal
84
-	 * @param array $targetProperties
85
-	 * @return string
86
-	 * @throws \Exception
87
-	 */
88
-	public function syncRemoteAddressBook($url, $userName, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) {
89
-		// 1. create addressbook
90
-		$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties);
91
-		$addressBookId = $book['id'];
92
-
93
-		// 2. query changes
94
-		try {
95
-			$response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
96
-		} catch (ClientHttpException $ex) {
97
-			if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
98
-				// remote server revoked access to the address book, remove it
99
-				$this->backend->deleteAddressBook($addressBookId);
100
-				$this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
101
-				throw $ex;
102
-			}
103
-		}
104
-
105
-		// 3. apply changes
106
-		// TODO: use multi-get for download
107
-		foreach ($response['response'] as $resource => $status) {
108
-			$cardUri = basename($resource);
109
-			if (isset($status[200])) {
110
-				$vCard = $this->download($url, $userName, $sharedSecret, $resource);
111
-				$existingCard = $this->backend->getCard($addressBookId, $cardUri);
112
-				if ($existingCard === false) {
113
-					$this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
114
-				} else {
115
-					$this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
116
-				}
117
-			} else {
118
-				$this->backend->deleteCard($addressBookId, $cardUri);
119
-			}
120
-		}
121
-
122
-		return $response['token'];
123
-	}
124
-
125
-	/**
126
-	 * @param string $principal
127
-	 * @param string $id
128
-	 * @param array $properties
129
-	 * @return array|null
130
-	 * @throws \Sabre\DAV\Exception\BadRequest
131
-	 */
132
-	public function ensureSystemAddressBookExists($principal, $id, $properties) {
133
-		$book = $this->backend->getAddressBooksByUri($principal, $id);
134
-		if (!is_null($book)) {
135
-			return $book;
136
-		}
137
-		$this->backend->createAddressBook($principal, $id, $properties);
138
-
139
-		return $this->backend->getAddressBooksByUri($principal, $id);
140
-	}
141
-
142
-	/**
143
-	 * Check if there is a valid certPath we should use
144
-	 *
145
-	 * @return string
146
-	 */
147
-	protected function getCertPath() {
148
-
149
-		// we already have a valid certPath
150
-		if ($this->certPath !== '') {
151
-			return $this->certPath;
152
-		}
153
-
154
-		/** @var ICertificateManager $certManager */
155
-		$certManager = \OC::$server->getCertificateManager(null);
156
-		$certPath = $certManager->getAbsoluteBundlePath();
157
-		if (file_exists($certPath)) {
158
-			$this->certPath = $certPath;
159
-		}
160
-
161
-		return $this->certPath;
162
-	}
163
-
164
-	/**
165
-	 * @param string $url
166
-	 * @param string $userName
167
-	 * @param string $addressBookUrl
168
-	 * @param string $sharedSecret
169
-	 * @return Client
170
-	 */
171
-	protected function getClient($url, $userName, $sharedSecret) {
172
-		$settings = [
173
-			'baseUri' => $url . '/',
174
-			'userName' => $userName,
175
-			'password' => $sharedSecret,
176
-		];
177
-		$client = new Client($settings);
178
-		$certPath = $this->getCertPath();
179
-		$client->setThrowExceptions(true);
180
-
181
-		if ($certPath !== '' && strpos($url, 'http://') !== 0) {
182
-			$client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
183
-		}
184
-
185
-		return $client;
186
-	}
187
-
188
-	/**
189
-	 * @param string $url
190
-	 * @param string $userName
191
-	 * @param string $addressBookUrl
192
-	 * @param string $sharedSecret
193
-	 * @param string $syncToken
194
-	 * @return array
195
-	 */
196
-	 protected function requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken) {
197
-		 $client = $this->getClient($url, $userName, $sharedSecret);
198
-
199
-		 $body = $this->buildSyncCollectionRequestBody($syncToken);
200
-
201
-		 $response = $client->request('REPORT', $addressBookUrl, $body, [
202
-			 'Content-Type' => 'application/xml'
203
-		 ]);
204
-
205
-		 return $this->parseMultiStatus($response['body']);
206
-	 }
207
-
208
-	/**
209
-	 * @param string $url
210
-	 * @param string $userName
211
-	 * @param string $sharedSecret
212
-	 * @param string $resourcePath
213
-	 * @return array
214
-	 */
215
-	protected function download($url, $userName, $sharedSecret, $resourcePath) {
216
-		$client = $this->getClient($url, $userName, $sharedSecret);
217
-		return $client->request('GET', $resourcePath);
218
-	}
219
-
220
-	/**
221
-	 * @param string|null $syncToken
222
-	 * @return string
223
-	 */
224
-	private function buildSyncCollectionRequestBody($syncToken) {
225
-
226
-		$dom = new \DOMDocument('1.0', 'UTF-8');
227
-		$dom->formatOutput = true;
228
-		$root = $dom->createElementNS('DAV:', 'd:sync-collection');
229
-		$sync = $dom->createElement('d:sync-token', $syncToken);
230
-		$prop = $dom->createElement('d:prop');
231
-		$cont = $dom->createElement('d:getcontenttype');
232
-		$etag = $dom->createElement('d:getetag');
233
-
234
-		$prop->appendChild($cont);
235
-		$prop->appendChild($etag);
236
-		$root->appendChild($sync);
237
-		$root->appendChild($prop);
238
-		$dom->appendChild($root);
239
-		return $dom->saveXML();
240
-	}
241
-
242
-	/**
243
-	 * @param string $body
244
-	 * @return array
245
-	 * @throws \Sabre\Xml\ParseException
246
-	 */
247
-	private function parseMultiStatus($body) {
248
-		$xml = new Service();
249
-
250
-		/** @var MultiStatus $multiStatus */
251
-		$multiStatus = $xml->expect('{DAV:}multistatus', $body);
252
-
253
-		$result = [];
254
-		foreach ($multiStatus->getResponses() as $response) {
255
-			$result[$response->getHref()] = $response->getResponseProperties();
256
-		}
257
-
258
-		return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
259
-	}
260
-
261
-	/**
262
-	 * @param IUser $user
263
-	 */
264
-	public function updateUser($user) {
265
-		$systemAddressBook = $this->getLocalSystemAddressBook();
266
-		$addressBookId = $systemAddressBook['id'];
267
-		$converter = new Converter($this->accountManager);
268
-		$name = $user->getBackendClassName();
269
-		$userId = $user->getUID();
270
-
271
-		$cardId = "$name:$userId.vcf";
272
-		$card = $this->backend->getCard($addressBookId, $cardId);
273
-		if ($card === false) {
274
-			$vCard = $converter->createCardFromUser($user);
275
-			if ($vCard !== null) {
276
-				$this->backend->createCard($addressBookId, $cardId, $vCard->serialize());
277
-			}
278
-		} else {
279
-			$vCard = $converter->createCardFromUser($user);
280
-			if (is_null($vCard)) {
281
-				$this->backend->deleteCard($addressBookId, $cardId);
282
-			} else {
283
-				$this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
284
-			}
285
-		}
286
-	}
287
-
288
-	/**
289
-	 * @param IUser|string $userOrCardId
290
-	 */
291
-	public function deleteUser($userOrCardId) {
292
-		$systemAddressBook = $this->getLocalSystemAddressBook();
293
-		if ($userOrCardId instanceof IUser){
294
-			$name = $userOrCardId->getBackendClassName();
295
-			$userId = $userOrCardId->getUID();
296
-
297
-			$userOrCardId = "$name:$userId.vcf";
298
-		}
299
-		$this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
300
-	}
301
-
302
-	/**
303
-	 * @return array|null
304
-	 */
305
-	public function getLocalSystemAddressBook() {
306
-		if (is_null($this->localSystemAddressBook)) {
307
-			$systemPrincipal = "principals/system/system";
308
-			$this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [
309
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
310
-			]);
311
-		}
312
-
313
-		return $this->localSystemAddressBook;
314
-	}
315
-
316
-	public function syncInstance(\Closure $progressCallback = null) {
317
-		$systemAddressBook = $this->getLocalSystemAddressBook();
318
-		$this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $progressCallback) {
319
-			$this->updateUser($user);
320
-			if (!is_null($progressCallback)) {
321
-				$progressCallback();
322
-			}
323
-		});
324
-
325
-		// remove no longer existing
326
-		$allCards = $this->backend->getCards($systemAddressBook['id']);
327
-		foreach($allCards as $card) {
328
-			$vCard = Reader::read($card['carddata']);
329
-			$uid = $vCard->UID->getValue();
330
-			// load backend and see if user exists
331
-			if (!$this->userManager->userExists($uid)) {
332
-				$this->deleteUser($card['uri']);
333
-			}
334
-		}
335
-	}
42
+    /** @var CardDavBackend */
43
+    private $backend;
44
+
45
+    /** @var IUserManager */
46
+    private $userManager;
47
+
48
+    /** @var ILogger */
49
+    private $logger;
50
+
51
+    /** @var array */
52
+    private $localSystemAddressBook;
53
+
54
+    /** @var AccountManager */
55
+    private $accountManager;
56
+
57
+    /** @var string */
58
+    protected $certPath;
59
+
60
+    /**
61
+     * SyncService constructor.
62
+     *
63
+     * @param CardDavBackend $backend
64
+     * @param IUserManager $userManager
65
+     * @param ILogger $logger
66
+     * @param AccountManager $accountManager
67
+     */
68
+    public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger, AccountManager $accountManager) {
69
+        $this->backend = $backend;
70
+        $this->userManager = $userManager;
71
+        $this->logger = $logger;
72
+        $this->accountManager = $accountManager;
73
+        $this->certPath = '';
74
+    }
75
+
76
+    /**
77
+     * @param string $url
78
+     * @param string $userName
79
+     * @param string $addressBookUrl
80
+     * @param string $sharedSecret
81
+     * @param string $syncToken
82
+     * @param int $targetBookId
83
+     * @param string $targetPrincipal
84
+     * @param array $targetProperties
85
+     * @return string
86
+     * @throws \Exception
87
+     */
88
+    public function syncRemoteAddressBook($url, $userName, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) {
89
+        // 1. create addressbook
90
+        $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties);
91
+        $addressBookId = $book['id'];
92
+
93
+        // 2. query changes
94
+        try {
95
+            $response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
96
+        } catch (ClientHttpException $ex) {
97
+            if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
98
+                // remote server revoked access to the address book, remove it
99
+                $this->backend->deleteAddressBook($addressBookId);
100
+                $this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
101
+                throw $ex;
102
+            }
103
+        }
104
+
105
+        // 3. apply changes
106
+        // TODO: use multi-get for download
107
+        foreach ($response['response'] as $resource => $status) {
108
+            $cardUri = basename($resource);
109
+            if (isset($status[200])) {
110
+                $vCard = $this->download($url, $userName, $sharedSecret, $resource);
111
+                $existingCard = $this->backend->getCard($addressBookId, $cardUri);
112
+                if ($existingCard === false) {
113
+                    $this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
114
+                } else {
115
+                    $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
116
+                }
117
+            } else {
118
+                $this->backend->deleteCard($addressBookId, $cardUri);
119
+            }
120
+        }
121
+
122
+        return $response['token'];
123
+    }
124
+
125
+    /**
126
+     * @param string $principal
127
+     * @param string $id
128
+     * @param array $properties
129
+     * @return array|null
130
+     * @throws \Sabre\DAV\Exception\BadRequest
131
+     */
132
+    public function ensureSystemAddressBookExists($principal, $id, $properties) {
133
+        $book = $this->backend->getAddressBooksByUri($principal, $id);
134
+        if (!is_null($book)) {
135
+            return $book;
136
+        }
137
+        $this->backend->createAddressBook($principal, $id, $properties);
138
+
139
+        return $this->backend->getAddressBooksByUri($principal, $id);
140
+    }
141
+
142
+    /**
143
+     * Check if there is a valid certPath we should use
144
+     *
145
+     * @return string
146
+     */
147
+    protected function getCertPath() {
148
+
149
+        // we already have a valid certPath
150
+        if ($this->certPath !== '') {
151
+            return $this->certPath;
152
+        }
153
+
154
+        /** @var ICertificateManager $certManager */
155
+        $certManager = \OC::$server->getCertificateManager(null);
156
+        $certPath = $certManager->getAbsoluteBundlePath();
157
+        if (file_exists($certPath)) {
158
+            $this->certPath = $certPath;
159
+        }
160
+
161
+        return $this->certPath;
162
+    }
163
+
164
+    /**
165
+     * @param string $url
166
+     * @param string $userName
167
+     * @param string $addressBookUrl
168
+     * @param string $sharedSecret
169
+     * @return Client
170
+     */
171
+    protected function getClient($url, $userName, $sharedSecret) {
172
+        $settings = [
173
+            'baseUri' => $url . '/',
174
+            'userName' => $userName,
175
+            'password' => $sharedSecret,
176
+        ];
177
+        $client = new Client($settings);
178
+        $certPath = $this->getCertPath();
179
+        $client->setThrowExceptions(true);
180
+
181
+        if ($certPath !== '' && strpos($url, 'http://') !== 0) {
182
+            $client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
183
+        }
184
+
185
+        return $client;
186
+    }
187
+
188
+    /**
189
+     * @param string $url
190
+     * @param string $userName
191
+     * @param string $addressBookUrl
192
+     * @param string $sharedSecret
193
+     * @param string $syncToken
194
+     * @return array
195
+     */
196
+        protected function requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken) {
197
+            $client = $this->getClient($url, $userName, $sharedSecret);
198
+
199
+            $body = $this->buildSyncCollectionRequestBody($syncToken);
200
+
201
+            $response = $client->request('REPORT', $addressBookUrl, $body, [
202
+                'Content-Type' => 'application/xml'
203
+            ]);
204
+
205
+            return $this->parseMultiStatus($response['body']);
206
+        }
207
+
208
+    /**
209
+     * @param string $url
210
+     * @param string $userName
211
+     * @param string $sharedSecret
212
+     * @param string $resourcePath
213
+     * @return array
214
+     */
215
+    protected function download($url, $userName, $sharedSecret, $resourcePath) {
216
+        $client = $this->getClient($url, $userName, $sharedSecret);
217
+        return $client->request('GET', $resourcePath);
218
+    }
219
+
220
+    /**
221
+     * @param string|null $syncToken
222
+     * @return string
223
+     */
224
+    private function buildSyncCollectionRequestBody($syncToken) {
225
+
226
+        $dom = new \DOMDocument('1.0', 'UTF-8');
227
+        $dom->formatOutput = true;
228
+        $root = $dom->createElementNS('DAV:', 'd:sync-collection');
229
+        $sync = $dom->createElement('d:sync-token', $syncToken);
230
+        $prop = $dom->createElement('d:prop');
231
+        $cont = $dom->createElement('d:getcontenttype');
232
+        $etag = $dom->createElement('d:getetag');
233
+
234
+        $prop->appendChild($cont);
235
+        $prop->appendChild($etag);
236
+        $root->appendChild($sync);
237
+        $root->appendChild($prop);
238
+        $dom->appendChild($root);
239
+        return $dom->saveXML();
240
+    }
241
+
242
+    /**
243
+     * @param string $body
244
+     * @return array
245
+     * @throws \Sabre\Xml\ParseException
246
+     */
247
+    private function parseMultiStatus($body) {
248
+        $xml = new Service();
249
+
250
+        /** @var MultiStatus $multiStatus */
251
+        $multiStatus = $xml->expect('{DAV:}multistatus', $body);
252
+
253
+        $result = [];
254
+        foreach ($multiStatus->getResponses() as $response) {
255
+            $result[$response->getHref()] = $response->getResponseProperties();
256
+        }
257
+
258
+        return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
259
+    }
260
+
261
+    /**
262
+     * @param IUser $user
263
+     */
264
+    public function updateUser($user) {
265
+        $systemAddressBook = $this->getLocalSystemAddressBook();
266
+        $addressBookId = $systemAddressBook['id'];
267
+        $converter = new Converter($this->accountManager);
268
+        $name = $user->getBackendClassName();
269
+        $userId = $user->getUID();
270
+
271
+        $cardId = "$name:$userId.vcf";
272
+        $card = $this->backend->getCard($addressBookId, $cardId);
273
+        if ($card === false) {
274
+            $vCard = $converter->createCardFromUser($user);
275
+            if ($vCard !== null) {
276
+                $this->backend->createCard($addressBookId, $cardId, $vCard->serialize());
277
+            }
278
+        } else {
279
+            $vCard = $converter->createCardFromUser($user);
280
+            if (is_null($vCard)) {
281
+                $this->backend->deleteCard($addressBookId, $cardId);
282
+            } else {
283
+                $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
284
+            }
285
+        }
286
+    }
287
+
288
+    /**
289
+     * @param IUser|string $userOrCardId
290
+     */
291
+    public function deleteUser($userOrCardId) {
292
+        $systemAddressBook = $this->getLocalSystemAddressBook();
293
+        if ($userOrCardId instanceof IUser){
294
+            $name = $userOrCardId->getBackendClassName();
295
+            $userId = $userOrCardId->getUID();
296
+
297
+            $userOrCardId = "$name:$userId.vcf";
298
+        }
299
+        $this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
300
+    }
301
+
302
+    /**
303
+     * @return array|null
304
+     */
305
+    public function getLocalSystemAddressBook() {
306
+        if (is_null($this->localSystemAddressBook)) {
307
+            $systemPrincipal = "principals/system/system";
308
+            $this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [
309
+                '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
310
+            ]);
311
+        }
312
+
313
+        return $this->localSystemAddressBook;
314
+    }
315
+
316
+    public function syncInstance(\Closure $progressCallback = null) {
317
+        $systemAddressBook = $this->getLocalSystemAddressBook();
318
+        $this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $progressCallback) {
319
+            $this->updateUser($user);
320
+            if (!is_null($progressCallback)) {
321
+                $progressCallback();
322
+            }
323
+        });
324
+
325
+        // remove no longer existing
326
+        $allCards = $this->backend->getCards($systemAddressBook['id']);
327
+        foreach($allCards as $card) {
328
+            $vCard = Reader::read($card['carddata']);
329
+            $uid = $vCard->UID->getValue();
330
+            // load backend and see if user exists
331
+            if (!$this->userManager->userExists($uid)) {
332
+                $this->deleteUser($card['uri']);
333
+            }
334
+        }
335
+    }
336 336
 
337 337
 
338 338
 }
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/Auth.php 1 patch
Indentation   +191 added lines, -191 removed lines patch added patch discarded remove patch
@@ -49,212 +49,212 @@
 block discarded – undo
49 49
 class Auth extends AbstractBasic {
50 50
 
51 51
 
52
-	const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
52
+    const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
53 53
 
54
-	/** @var ISession */
55
-	private $session;
56
-	/** @var Session */
57
-	private $userSession;
58
-	/** @var IRequest */
59
-	private $request;
60
-	/** @var string */
61
-	private $currentUser;
62
-	/** @var Manager */
63
-	private $twoFactorManager;
64
-	/** @var Throttler */
65
-	private $throttler;
54
+    /** @var ISession */
55
+    private $session;
56
+    /** @var Session */
57
+    private $userSession;
58
+    /** @var IRequest */
59
+    private $request;
60
+    /** @var string */
61
+    private $currentUser;
62
+    /** @var Manager */
63
+    private $twoFactorManager;
64
+    /** @var Throttler */
65
+    private $throttler;
66 66
 
67
-	/**
68
-	 * @param ISession $session
69
-	 * @param Session $userSession
70
-	 * @param IRequest $request
71
-	 * @param Manager $twoFactorManager
72
-	 * @param Throttler $throttler
73
-	 * @param string $principalPrefix
74
-	 */
75
-	public function __construct(ISession $session,
76
-								Session $userSession,
77
-								IRequest $request,
78
-								Manager $twoFactorManager,
79
-								Throttler $throttler,
80
-								$principalPrefix = 'principals/users/') {
81
-		$this->session = $session;
82
-		$this->userSession = $userSession;
83
-		$this->twoFactorManager = $twoFactorManager;
84
-		$this->request = $request;
85
-		$this->throttler = $throttler;
86
-		$this->principalPrefix = $principalPrefix;
67
+    /**
68
+     * @param ISession $session
69
+     * @param Session $userSession
70
+     * @param IRequest $request
71
+     * @param Manager $twoFactorManager
72
+     * @param Throttler $throttler
73
+     * @param string $principalPrefix
74
+     */
75
+    public function __construct(ISession $session,
76
+                                Session $userSession,
77
+                                IRequest $request,
78
+                                Manager $twoFactorManager,
79
+                                Throttler $throttler,
80
+                                $principalPrefix = 'principals/users/') {
81
+        $this->session = $session;
82
+        $this->userSession = $userSession;
83
+        $this->twoFactorManager = $twoFactorManager;
84
+        $this->request = $request;
85
+        $this->throttler = $throttler;
86
+        $this->principalPrefix = $principalPrefix;
87 87
 
88
-		// setup realm
89
-		$defaults = new \OCP\Defaults();
90
-		$this->realm = $defaults->getName();
91
-	}
88
+        // setup realm
89
+        $defaults = new \OCP\Defaults();
90
+        $this->realm = $defaults->getName();
91
+    }
92 92
 
93
-	/**
94
-	 * Whether the user has initially authenticated via DAV
95
-	 *
96
-	 * This is required for WebDAV clients that resent the cookies even when the
97
-	 * account was changed.
98
-	 *
99
-	 * @see https://github.com/owncloud/core/issues/13245
100
-	 *
101
-	 * @param string $username
102
-	 * @return bool
103
-	 */
104
-	public function isDavAuthenticated($username) {
105
-		return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
106
-		$this->session->get(self::DAV_AUTHENTICATED) === $username;
107
-	}
93
+    /**
94
+     * Whether the user has initially authenticated via DAV
95
+     *
96
+     * This is required for WebDAV clients that resent the cookies even when the
97
+     * account was changed.
98
+     *
99
+     * @see https://github.com/owncloud/core/issues/13245
100
+     *
101
+     * @param string $username
102
+     * @return bool
103
+     */
104
+    public function isDavAuthenticated($username) {
105
+        return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
106
+        $this->session->get(self::DAV_AUTHENTICATED) === $username;
107
+    }
108 108
 
109
-	/**
110
-	 * Validates a username and password
111
-	 *
112
-	 * This method should return true or false depending on if login
113
-	 * succeeded.
114
-	 *
115
-	 * @param string $username
116
-	 * @param string $password
117
-	 * @return bool
118
-	 * @throws PasswordLoginForbidden
119
-	 */
120
-	protected function validateUserPass($username, $password) {
121
-		if ($this->userSession->isLoggedIn() &&
122
-			$this->isDavAuthenticated($this->userSession->getUser()->getUID())
123
-		) {
124
-			\OC_Util::setupFS($this->userSession->getUser()->getUID());
125
-			$this->session->close();
126
-			return true;
127
-		} else {
128
-			\OC_Util::setupFS(); //login hooks may need early access to the filesystem
129
-			try {
130
-				if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) {
131
-					\OC_Util::setupFS($this->userSession->getUser()->getUID());
132
-					$this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID());
133
-					$this->session->close();
134
-					return true;
135
-				} else {
136
-					$this->session->close();
137
-					return false;
138
-				}
139
-			} catch (PasswordLoginForbiddenException $ex) {
140
-				$this->session->close();
141
-				throw new PasswordLoginForbidden();
142
-			}
143
-		}
144
-	}
109
+    /**
110
+     * Validates a username and password
111
+     *
112
+     * This method should return true or false depending on if login
113
+     * succeeded.
114
+     *
115
+     * @param string $username
116
+     * @param string $password
117
+     * @return bool
118
+     * @throws PasswordLoginForbidden
119
+     */
120
+    protected function validateUserPass($username, $password) {
121
+        if ($this->userSession->isLoggedIn() &&
122
+            $this->isDavAuthenticated($this->userSession->getUser()->getUID())
123
+        ) {
124
+            \OC_Util::setupFS($this->userSession->getUser()->getUID());
125
+            $this->session->close();
126
+            return true;
127
+        } else {
128
+            \OC_Util::setupFS(); //login hooks may need early access to the filesystem
129
+            try {
130
+                if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) {
131
+                    \OC_Util::setupFS($this->userSession->getUser()->getUID());
132
+                    $this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID());
133
+                    $this->session->close();
134
+                    return true;
135
+                } else {
136
+                    $this->session->close();
137
+                    return false;
138
+                }
139
+            } catch (PasswordLoginForbiddenException $ex) {
140
+                $this->session->close();
141
+                throw new PasswordLoginForbidden();
142
+            }
143
+        }
144
+    }
145 145
 
146
-	/**
147
-	 * @param RequestInterface $request
148
-	 * @param ResponseInterface $response
149
-	 * @return array
150
-	 * @throws NotAuthenticated
151
-	 * @throws ServiceUnavailable
152
-	 */
153
-	function check(RequestInterface $request, ResponseInterface $response) {
154
-		try {
155
-			return $this->auth($request, $response);
156
-		} catch (NotAuthenticated $e) {
157
-			throw $e;
158
-		} catch (Exception $e) {
159
-			$class = get_class($e);
160
-			$msg = $e->getMessage();
161
-			\OC::$server->getLogger()->logException($e);
162
-			throw new ServiceUnavailable("$class: $msg");
163
-		}
164
-	}
146
+    /**
147
+     * @param RequestInterface $request
148
+     * @param ResponseInterface $response
149
+     * @return array
150
+     * @throws NotAuthenticated
151
+     * @throws ServiceUnavailable
152
+     */
153
+    function check(RequestInterface $request, ResponseInterface $response) {
154
+        try {
155
+            return $this->auth($request, $response);
156
+        } catch (NotAuthenticated $e) {
157
+            throw $e;
158
+        } catch (Exception $e) {
159
+            $class = get_class($e);
160
+            $msg = $e->getMessage();
161
+            \OC::$server->getLogger()->logException($e);
162
+            throw new ServiceUnavailable("$class: $msg");
163
+        }
164
+    }
165 165
 
166
-	/**
167
-	 * Checks whether a CSRF check is required on the request
168
-	 *
169
-	 * @return bool
170
-	 */
171
-	private function requiresCSRFCheck() {
172
-		// GET requires no check at all
173
-		if($this->request->getMethod() === 'GET') {
174
-			return false;
175
-		}
166
+    /**
167
+     * Checks whether a CSRF check is required on the request
168
+     *
169
+     * @return bool
170
+     */
171
+    private function requiresCSRFCheck() {
172
+        // GET requires no check at all
173
+        if($this->request->getMethod() === 'GET') {
174
+            return false;
175
+        }
176 176
 
177
-		// Official Nextcloud clients require no checks
178
-		if($this->request->isUserAgent([
179
-			IRequest::USER_AGENT_CLIENT_DESKTOP,
180
-			IRequest::USER_AGENT_CLIENT_ANDROID,
181
-			IRequest::USER_AGENT_CLIENT_IOS,
182
-		])) {
183
-			return false;
184
-		}
177
+        // Official Nextcloud clients require no checks
178
+        if($this->request->isUserAgent([
179
+            IRequest::USER_AGENT_CLIENT_DESKTOP,
180
+            IRequest::USER_AGENT_CLIENT_ANDROID,
181
+            IRequest::USER_AGENT_CLIENT_IOS,
182
+        ])) {
183
+            return false;
184
+        }
185 185
 
186
-		// If not logged-in no check is required
187
-		if(!$this->userSession->isLoggedIn()) {
188
-			return false;
189
-		}
186
+        // If not logged-in no check is required
187
+        if(!$this->userSession->isLoggedIn()) {
188
+            return false;
189
+        }
190 190
 
191
-		// POST always requires a check
192
-		if($this->request->getMethod() === 'POST') {
193
-			return true;
194
-		}
191
+        // POST always requires a check
192
+        if($this->request->getMethod() === 'POST') {
193
+            return true;
194
+        }
195 195
 
196
-		// If logged-in AND DAV authenticated no check is required
197
-		if($this->userSession->isLoggedIn() &&
198
-			$this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
199
-			return false;
200
-		}
196
+        // If logged-in AND DAV authenticated no check is required
197
+        if($this->userSession->isLoggedIn() &&
198
+            $this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
199
+            return false;
200
+        }
201 201
 
202
-		return true;
203
-	}
202
+        return true;
203
+    }
204 204
 
205
-	/**
206
-	 * @param RequestInterface $request
207
-	 * @param ResponseInterface $response
208
-	 * @return array
209
-	 * @throws NotAuthenticated
210
-	 */
211
-	private function auth(RequestInterface $request, ResponseInterface $response) {
212
-		$forcedLogout = false;
205
+    /**
206
+     * @param RequestInterface $request
207
+     * @param ResponseInterface $response
208
+     * @return array
209
+     * @throws NotAuthenticated
210
+     */
211
+    private function auth(RequestInterface $request, ResponseInterface $response) {
212
+        $forcedLogout = false;
213 213
 
214
-		if(!$this->request->passesCSRFCheck() &&
215
-			$this->requiresCSRFCheck()) {
216
-			// In case of a fail with POST we need to recheck the credentials
217
-			if($this->request->getMethod() === 'POST') {
218
-				$forcedLogout = true;
219
-			} else {
220
-				$response->setStatus(401);
221
-				throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.');
222
-			}
223
-		}
214
+        if(!$this->request->passesCSRFCheck() &&
215
+            $this->requiresCSRFCheck()) {
216
+            // In case of a fail with POST we need to recheck the credentials
217
+            if($this->request->getMethod() === 'POST') {
218
+                $forcedLogout = true;
219
+            } else {
220
+                $response->setStatus(401);
221
+                throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.');
222
+            }
223
+        }
224 224
 
225
-		if($forcedLogout) {
226
-			$this->userSession->logout();
227
-		} else {
228
-			if($this->twoFactorManager->needsSecondFactor($this->userSession->getUser())) {
229
-				throw new \Sabre\DAV\Exception\NotAuthenticated('2FA challenge not passed.');
230
-			}
231
-			if (\OC_User::handleApacheAuth() ||
232
-				//Fix for broken webdav clients
233
-				($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) ||
234
-				//Well behaved clients that only send the cookie are allowed
235
-				($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null)
236
-			) {
237
-				$user = $this->userSession->getUser()->getUID();
238
-				\OC_Util::setupFS($user);
239
-				$this->currentUser = $user;
240
-				$this->session->close();
241
-				return [true, $this->principalPrefix . $user];
242
-			}
243
-		}
225
+        if($forcedLogout) {
226
+            $this->userSession->logout();
227
+        } else {
228
+            if($this->twoFactorManager->needsSecondFactor($this->userSession->getUser())) {
229
+                throw new \Sabre\DAV\Exception\NotAuthenticated('2FA challenge not passed.');
230
+            }
231
+            if (\OC_User::handleApacheAuth() ||
232
+                //Fix for broken webdav clients
233
+                ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) ||
234
+                //Well behaved clients that only send the cookie are allowed
235
+                ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null)
236
+            ) {
237
+                $user = $this->userSession->getUser()->getUID();
238
+                \OC_Util::setupFS($user);
239
+                $this->currentUser = $user;
240
+                $this->session->close();
241
+                return [true, $this->principalPrefix . $user];
242
+            }
243
+        }
244 244
 
245
-		if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) {
246
-			// do not re-authenticate over ajax, use dummy auth name to prevent browser popup
247
-			$response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"');
248
-			$response->setStatus(401);
249
-			throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
250
-		}
245
+        if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) {
246
+            // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
247
+            $response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"');
248
+            $response->setStatus(401);
249
+            throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
250
+        }
251 251
 
252
-		$data = parent::check($request, $response);
253
-		if($data[0] === true) {
254
-			$startPos = strrpos($data[1], '/') + 1;
255
-			$user = $this->userSession->getUser()->getUID();
256
-			$data[1] = substr_replace($data[1], $user, $startPos);
257
-		}
258
-		return $data;
259
-	}
252
+        $data = parent::check($request, $response);
253
+        if($data[0] === true) {
254
+            $startPos = strrpos($data[1], '/') + 1;
255
+            $user = $this->userSession->getUser()->getUID();
256
+            $data[1] = substr_replace($data[1], $user, $startPos);
257
+        }
258
+        return $data;
259
+    }
260 260
 }
Please login to merge, or discard this patch.
apps/files_sharing/lib/Helper.php 1 patch
Indentation   +235 added lines, -235 removed lines patch added patch discarded remove patch
@@ -36,240 +36,240 @@
 block discarded – undo
36 36
 
37 37
 class Helper {
38 38
 
39
-	public static function registerHooks() {
40
-		\OCP\Util::connectHook('OC_Filesystem', 'post_rename', '\OCA\Files_Sharing\Updater', 'renameHook');
41
-		\OCP\Util::connectHook('OC_Filesystem', 'post_delete', '\OCA\Files_Sharing\Hooks', 'unshareChildren');
42
-
43
-		\OCP\Util::connectHook('OC_User', 'post_deleteUser', '\OCA\Files_Sharing\Hooks', 'deleteUser');
44
-	}
45
-
46
-	/**
47
-	 * Sets up the filesystem and user for public sharing
48
-	 * @param string $token string share token
49
-	 * @param string $relativePath optional path relative to the share
50
-	 * @param string $password optional password
51
-	 * @return array
52
-	 */
53
-	public static function setupFromToken($token, $relativePath = null, $password = null) {
54
-		\OC_User::setIncognitoMode(true);
55
-
56
-		$shareManager = \OC::$server->getShareManager();
57
-
58
-		try {
59
-			$share = $shareManager->getShareByToken($token);
60
-		} catch (ShareNotFound $e) {
61
-			\OC_Response::setStatus(404);
62
-			\OCP\Util::writeLog('core-preview', 'Passed token parameter is not valid', \OCP\Util::DEBUG);
63
-			exit;
64
-		}
65
-
66
-		\OCP\JSON::checkUserExists($share->getShareOwner());
67
-		\OC_Util::tearDownFS();
68
-		\OC_Util::setupFS($share->getShareOwner());
69
-
70
-
71
-		try {
72
-			$path = Filesystem::getPath($share->getNodeId());
73
-		} catch (NotFoundException $e) {
74
-			\OCP\Util::writeLog('share', 'could not resolve linkItem', \OCP\Util::DEBUG);
75
-			\OC_Response::setStatus(404);
76
-			\OCP\JSON::error(array('success' => false));
77
-			exit();
78
-		}
79
-
80
-		if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK && $share->getPassword() !== null) {
81
-			if (!self::authenticate($share, $password)) {
82
-				\OC_Response::setStatus(403);
83
-				\OCP\JSON::error(array('success' => false));
84
-				exit();
85
-			}
86
-		}
87
-
88
-		$basePath = $path;
89
-
90
-		if ($relativePath !== null && Filesystem::isReadable($basePath . $relativePath)) {
91
-			$path .= Filesystem::normalizePath($relativePath);
92
-		}
93
-
94
-		return array(
95
-			'share' => $share,
96
-			'basePath' => $basePath,
97
-			'realPath' => $path
98
-		);
99
-	}
100
-
101
-	/**
102
-	 * Authenticate link item with the given password
103
-	 * or with the session if no password was given.
104
-	 * @param \OCP\Share\IShare $share
105
-	 * @param string $password optional password
106
-	 *
107
-	 * @return boolean true if authorized, false otherwise
108
-	 */
109
-	public static function authenticate($share, $password = null) {
110
-		$shareManager = \OC::$server->getShareManager();
111
-
112
-		if ($password !== null) {
113
-			if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) {
114
-				if ($shareManager->checkPassword($share, $password)) {
115
-					\OC::$server->getSession()->set('public_link_authenticated', (string)$share->getId());
116
-					return true;
117
-				}
118
-			}
119
-		} else {
120
-			// not authenticated ?
121
-			if (\OC::$server->getSession()->exists('public_link_authenticated')
122
-				&& \OC::$server->getSession()->get('public_link_authenticated') !== (string)$share->getId()) {
123
-				return true;
124
-			}
125
-		}
126
-		return false;
127
-	}
128
-
129
-	public static function getSharesFromItem($target) {
130
-		$result = array();
131
-		$owner = Filesystem::getOwner($target);
132
-		Filesystem::initMountPoints($owner);
133
-		$info = Filesystem::getFileInfo($target);
134
-		$ownerView = new View('/'.$owner.'/files');
135
-		if ( $owner !== User::getUser() ) {
136
-			$path = $ownerView->getPath($info['fileid']);
137
-		} else {
138
-			$path = $target;
139
-		}
140
-
141
-
142
-		$ids = array();
143
-		while ($path !== dirname($path)) {
144
-			$info = $ownerView->getFileInfo($path);
145
-			if ($info instanceof \OC\Files\FileInfo) {
146
-				$ids[] = $info['fileid'];
147
-			} else {
148
-				\OCP\Util::writeLog('sharing', 'No fileinfo available for: ' . $path, \OCP\Util::WARN);
149
-			}
150
-			$path = dirname($path);
151
-		}
152
-
153
-		if (!empty($ids)) {
154
-
155
-			$idList = array_chunk($ids, 99, true);
156
-
157
-			foreach ($idList as $subList) {
158
-				$statement = "SELECT `share_with`, `share_type`, `file_target` FROM `*PREFIX*share` WHERE `file_source` IN (" . implode(',', $subList) . ") AND `share_type` IN (0, 1, 2)";
159
-				$query = \OCP\DB::prepare($statement);
160
-				$r = $query->execute();
161
-				$result = array_merge($result, $r->fetchAll());
162
-			}
163
-		}
164
-
165
-		return $result;
166
-	}
167
-
168
-	/**
169
-	 * get the UID of the owner of the file and the path to the file relative to
170
-	 * owners files folder
171
-	 *
172
-	 * @param $filename
173
-	 * @return array
174
-	 * @throws \OC\User\NoUserException
175
-	 */
176
-	public static function getUidAndFilename($filename) {
177
-		$uid = Filesystem::getOwner($filename);
178
-		$userManager = \OC::$server->getUserManager();
179
-		// if the user with the UID doesn't exists, e.g. because the UID points
180
-		// to a remote user with a federated cloud ID we use the current logged-in
181
-		// user. We need a valid local user to create the share
182
-		if (!$userManager->userExists($uid)) {
183
-			$uid = User::getUser();
184
-		}
185
-		Filesystem::initMountPoints($uid);
186
-		if ( $uid !== User::getUser() ) {
187
-			$info = Filesystem::getFileInfo($filename);
188
-			$ownerView = new View('/'.$uid.'/files');
189
-			try {
190
-				$filename = $ownerView->getPath($info['fileid']);
191
-			} catch (NotFoundException $e) {
192
-				$filename = null;
193
-			}
194
-		}
195
-		return [$uid, $filename];
196
-	}
197
-
198
-	/**
199
-	 * Format a path to be relative to the /user/files/ directory
200
-	 * @param string $path the absolute path
201
-	 * @return string e.g. turns '/admin/files/test.txt' into 'test.txt'
202
-	 */
203
-	public static function stripUserFilesPath($path) {
204
-		$trimmed = ltrim($path, '/');
205
-		$split = explode('/', $trimmed);
206
-
207
-		// it is not a file relative to data/user/files
208
-		if (count($split) < 3 || $split[1] !== 'files') {
209
-			return false;
210
-		}
211
-
212
-		$sliced = array_slice($split, 2);
213
-		return implode('/', $sliced);
214
-	}
215
-
216
-	/**
217
-	 * check if file name already exists and generate unique target
218
-	 *
219
-	 * @param string $path
220
-	 * @param array $excludeList
221
-	 * @param View $view
222
-	 * @return string $path
223
-	 */
224
-	public static function generateUniqueTarget($path, $excludeList, $view) {
225
-		$pathinfo = pathinfo($path);
226
-		$ext = isset($pathinfo['extension']) ? '.'.$pathinfo['extension'] : '';
227
-		$name = $pathinfo['filename'];
228
-		$dir = $pathinfo['dirname'];
229
-		$i = 2;
230
-		while ($view->file_exists($path) || in_array($path, $excludeList)) {
231
-			$path = Filesystem::normalizePath($dir . '/' . $name . ' ('.$i.')' . $ext);
232
-			$i++;
233
-		}
234
-
235
-		return $path;
236
-	}
237
-
238
-	/**
239
-	 * get default share folder
240
-	 *
241
-	 * @param \OC\Files\View
242
-	 * @return string
243
-	 */
244
-	public static function getShareFolder($view = null) {
245
-		if ($view === null) {
246
-			$view = Filesystem::getView();
247
-		}
248
-		$shareFolder = \OC::$server->getConfig()->getSystemValue('share_folder', '/');
249
-		$shareFolder = Filesystem::normalizePath($shareFolder);
250
-
251
-		if (!$view->file_exists($shareFolder)) {
252
-			$dir = '';
253
-			$subdirs = explode('/', $shareFolder);
254
-			foreach ($subdirs as $subdir) {
255
-				$dir = $dir . '/' . $subdir;
256
-				if (!$view->is_dir($dir)) {
257
-					$view->mkdir($dir);
258
-				}
259
-			}
260
-		}
261
-
262
-		return $shareFolder;
263
-
264
-	}
265
-
266
-	/**
267
-	 * set default share folder
268
-	 *
269
-	 * @param string $shareFolder
270
-	 */
271
-	public static function setShareFolder($shareFolder) {
272
-		\OC::$server->getConfig()->setSystemValue('share_folder', $shareFolder);
273
-	}
39
+    public static function registerHooks() {
40
+        \OCP\Util::connectHook('OC_Filesystem', 'post_rename', '\OCA\Files_Sharing\Updater', 'renameHook');
41
+        \OCP\Util::connectHook('OC_Filesystem', 'post_delete', '\OCA\Files_Sharing\Hooks', 'unshareChildren');
42
+
43
+        \OCP\Util::connectHook('OC_User', 'post_deleteUser', '\OCA\Files_Sharing\Hooks', 'deleteUser');
44
+    }
45
+
46
+    /**
47
+     * Sets up the filesystem and user for public sharing
48
+     * @param string $token string share token
49
+     * @param string $relativePath optional path relative to the share
50
+     * @param string $password optional password
51
+     * @return array
52
+     */
53
+    public static function setupFromToken($token, $relativePath = null, $password = null) {
54
+        \OC_User::setIncognitoMode(true);
55
+
56
+        $shareManager = \OC::$server->getShareManager();
57
+
58
+        try {
59
+            $share = $shareManager->getShareByToken($token);
60
+        } catch (ShareNotFound $e) {
61
+            \OC_Response::setStatus(404);
62
+            \OCP\Util::writeLog('core-preview', 'Passed token parameter is not valid', \OCP\Util::DEBUG);
63
+            exit;
64
+        }
65
+
66
+        \OCP\JSON::checkUserExists($share->getShareOwner());
67
+        \OC_Util::tearDownFS();
68
+        \OC_Util::setupFS($share->getShareOwner());
69
+
70
+
71
+        try {
72
+            $path = Filesystem::getPath($share->getNodeId());
73
+        } catch (NotFoundException $e) {
74
+            \OCP\Util::writeLog('share', 'could not resolve linkItem', \OCP\Util::DEBUG);
75
+            \OC_Response::setStatus(404);
76
+            \OCP\JSON::error(array('success' => false));
77
+            exit();
78
+        }
79
+
80
+        if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK && $share->getPassword() !== null) {
81
+            if (!self::authenticate($share, $password)) {
82
+                \OC_Response::setStatus(403);
83
+                \OCP\JSON::error(array('success' => false));
84
+                exit();
85
+            }
86
+        }
87
+
88
+        $basePath = $path;
89
+
90
+        if ($relativePath !== null && Filesystem::isReadable($basePath . $relativePath)) {
91
+            $path .= Filesystem::normalizePath($relativePath);
92
+        }
93
+
94
+        return array(
95
+            'share' => $share,
96
+            'basePath' => $basePath,
97
+            'realPath' => $path
98
+        );
99
+    }
100
+
101
+    /**
102
+     * Authenticate link item with the given password
103
+     * or with the session if no password was given.
104
+     * @param \OCP\Share\IShare $share
105
+     * @param string $password optional password
106
+     *
107
+     * @return boolean true if authorized, false otherwise
108
+     */
109
+    public static function authenticate($share, $password = null) {
110
+        $shareManager = \OC::$server->getShareManager();
111
+
112
+        if ($password !== null) {
113
+            if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) {
114
+                if ($shareManager->checkPassword($share, $password)) {
115
+                    \OC::$server->getSession()->set('public_link_authenticated', (string)$share->getId());
116
+                    return true;
117
+                }
118
+            }
119
+        } else {
120
+            // not authenticated ?
121
+            if (\OC::$server->getSession()->exists('public_link_authenticated')
122
+                && \OC::$server->getSession()->get('public_link_authenticated') !== (string)$share->getId()) {
123
+                return true;
124
+            }
125
+        }
126
+        return false;
127
+    }
128
+
129
+    public static function getSharesFromItem($target) {
130
+        $result = array();
131
+        $owner = Filesystem::getOwner($target);
132
+        Filesystem::initMountPoints($owner);
133
+        $info = Filesystem::getFileInfo($target);
134
+        $ownerView = new View('/'.$owner.'/files');
135
+        if ( $owner !== User::getUser() ) {
136
+            $path = $ownerView->getPath($info['fileid']);
137
+        } else {
138
+            $path = $target;
139
+        }
140
+
141
+
142
+        $ids = array();
143
+        while ($path !== dirname($path)) {
144
+            $info = $ownerView->getFileInfo($path);
145
+            if ($info instanceof \OC\Files\FileInfo) {
146
+                $ids[] = $info['fileid'];
147
+            } else {
148
+                \OCP\Util::writeLog('sharing', 'No fileinfo available for: ' . $path, \OCP\Util::WARN);
149
+            }
150
+            $path = dirname($path);
151
+        }
152
+
153
+        if (!empty($ids)) {
154
+
155
+            $idList = array_chunk($ids, 99, true);
156
+
157
+            foreach ($idList as $subList) {
158
+                $statement = "SELECT `share_with`, `share_type`, `file_target` FROM `*PREFIX*share` WHERE `file_source` IN (" . implode(',', $subList) . ") AND `share_type` IN (0, 1, 2)";
159
+                $query = \OCP\DB::prepare($statement);
160
+                $r = $query->execute();
161
+                $result = array_merge($result, $r->fetchAll());
162
+            }
163
+        }
164
+
165
+        return $result;
166
+    }
167
+
168
+    /**
169
+     * get the UID of the owner of the file and the path to the file relative to
170
+     * owners files folder
171
+     *
172
+     * @param $filename
173
+     * @return array
174
+     * @throws \OC\User\NoUserException
175
+     */
176
+    public static function getUidAndFilename($filename) {
177
+        $uid = Filesystem::getOwner($filename);
178
+        $userManager = \OC::$server->getUserManager();
179
+        // if the user with the UID doesn't exists, e.g. because the UID points
180
+        // to a remote user with a federated cloud ID we use the current logged-in
181
+        // user. We need a valid local user to create the share
182
+        if (!$userManager->userExists($uid)) {
183
+            $uid = User::getUser();
184
+        }
185
+        Filesystem::initMountPoints($uid);
186
+        if ( $uid !== User::getUser() ) {
187
+            $info = Filesystem::getFileInfo($filename);
188
+            $ownerView = new View('/'.$uid.'/files');
189
+            try {
190
+                $filename = $ownerView->getPath($info['fileid']);
191
+            } catch (NotFoundException $e) {
192
+                $filename = null;
193
+            }
194
+        }
195
+        return [$uid, $filename];
196
+    }
197
+
198
+    /**
199
+     * Format a path to be relative to the /user/files/ directory
200
+     * @param string $path the absolute path
201
+     * @return string e.g. turns '/admin/files/test.txt' into 'test.txt'
202
+     */
203
+    public static function stripUserFilesPath($path) {
204
+        $trimmed = ltrim($path, '/');
205
+        $split = explode('/', $trimmed);
206
+
207
+        // it is not a file relative to data/user/files
208
+        if (count($split) < 3 || $split[1] !== 'files') {
209
+            return false;
210
+        }
211
+
212
+        $sliced = array_slice($split, 2);
213
+        return implode('/', $sliced);
214
+    }
215
+
216
+    /**
217
+     * check if file name already exists and generate unique target
218
+     *
219
+     * @param string $path
220
+     * @param array $excludeList
221
+     * @param View $view
222
+     * @return string $path
223
+     */
224
+    public static function generateUniqueTarget($path, $excludeList, $view) {
225
+        $pathinfo = pathinfo($path);
226
+        $ext = isset($pathinfo['extension']) ? '.'.$pathinfo['extension'] : '';
227
+        $name = $pathinfo['filename'];
228
+        $dir = $pathinfo['dirname'];
229
+        $i = 2;
230
+        while ($view->file_exists($path) || in_array($path, $excludeList)) {
231
+            $path = Filesystem::normalizePath($dir . '/' . $name . ' ('.$i.')' . $ext);
232
+            $i++;
233
+        }
234
+
235
+        return $path;
236
+    }
237
+
238
+    /**
239
+     * get default share folder
240
+     *
241
+     * @param \OC\Files\View
242
+     * @return string
243
+     */
244
+    public static function getShareFolder($view = null) {
245
+        if ($view === null) {
246
+            $view = Filesystem::getView();
247
+        }
248
+        $shareFolder = \OC::$server->getConfig()->getSystemValue('share_folder', '/');
249
+        $shareFolder = Filesystem::normalizePath($shareFolder);
250
+
251
+        if (!$view->file_exists($shareFolder)) {
252
+            $dir = '';
253
+            $subdirs = explode('/', $shareFolder);
254
+            foreach ($subdirs as $subdir) {
255
+                $dir = $dir . '/' . $subdir;
256
+                if (!$view->is_dir($dir)) {
257
+                    $view->mkdir($dir);
258
+                }
259
+            }
260
+        }
261
+
262
+        return $shareFolder;
263
+
264
+    }
265
+
266
+    /**
267
+     * set default share folder
268
+     *
269
+     * @param string $shareFolder
270
+     */
271
+    public static function setShareFolder($shareFolder) {
272
+        \OC::$server->getConfig()->setSystemValue('share_folder', $shareFolder);
273
+    }
274 274
 
275 275
 }
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/Crypt.php 2 patches
Indentation   +633 added lines, -633 removed lines patch added patch discarded remove patch
@@ -55,638 +55,638 @@
 block discarded – undo
55 55
  */
56 56
 class Crypt {
57 57
 
58
-	const DEFAULT_CIPHER = 'AES-256-CTR';
59
-	// default cipher from old Nextcloud versions
60
-	const LEGACY_CIPHER = 'AES-128-CFB';
61
-
62
-	// default key format, old Nextcloud version encrypted the private key directly
63
-	// with the user password
64
-	const LEGACY_KEY_FORMAT = 'password';
65
-
66
-	const HEADER_START = 'HBEGIN';
67
-	const HEADER_END = 'HEND';
68
-
69
-	/** @var ILogger */
70
-	private $logger;
71
-
72
-	/** @var string */
73
-	private $user;
74
-
75
-	/** @var IConfig */
76
-	private $config;
77
-
78
-	/** @var array */
79
-	private $supportedKeyFormats;
80
-
81
-	/** @var IL10N */
82
-	private $l;
83
-
84
-	/** @var array */
85
-	private $supportedCiphersAndKeySize = [
86
-		'AES-256-CTR' => 32,
87
-		'AES-128-CTR' => 16,
88
-		'AES-256-CFB' => 32,
89
-		'AES-128-CFB' => 16,
90
-	];
91
-
92
-	/**
93
-	 * @param ILogger $logger
94
-	 * @param IUserSession $userSession
95
-	 * @param IConfig $config
96
-	 * @param IL10N $l
97
-	 */
98
-	public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
99
-		$this->logger = $logger;
100
-		$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
101
-		$this->config = $config;
102
-		$this->l = $l;
103
-		$this->supportedKeyFormats = ['hash', 'password'];
104
-	}
105
-
106
-	/**
107
-	 * create new private/public key-pair for user
108
-	 *
109
-	 * @return array|bool
110
-	 */
111
-	public function createKeyPair() {
112
-
113
-		$log = $this->logger;
114
-		$res = $this->getOpenSSLPKey();
115
-
116
-		if (!$res) {
117
-			$log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
118
-				['app' => 'encryption']);
119
-
120
-			if (openssl_error_string()) {
121
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
122
-					['app' => 'encryption']);
123
-			}
124
-		} elseif (openssl_pkey_export($res,
125
-			$privateKey,
126
-			null,
127
-			$this->getOpenSSLConfig())) {
128
-			$keyDetails = openssl_pkey_get_details($res);
129
-			$publicKey = $keyDetails['key'];
130
-
131
-			return [
132
-				'publicKey' => $publicKey,
133
-				'privateKey' => $privateKey
134
-			];
135
-		}
136
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
137
-			['app' => 'encryption']);
138
-		if (openssl_error_string()) {
139
-			$log->error('Encryption Library:' . openssl_error_string(),
140
-				['app' => 'encryption']);
141
-		}
142
-
143
-		return false;
144
-	}
145
-
146
-	/**
147
-	 * Generates a new private key
148
-	 *
149
-	 * @return resource
150
-	 */
151
-	public function getOpenSSLPKey() {
152
-		$config = $this->getOpenSSLConfig();
153
-		return openssl_pkey_new($config);
154
-	}
155
-
156
-	/**
157
-	 * get openSSL Config
158
-	 *
159
-	 * @return array
160
-	 */
161
-	private function getOpenSSLConfig() {
162
-		$config = ['private_key_bits' => 4096];
163
-		$config = array_merge(
164
-			$config,
165
-			$this->config->getSystemValue('openssl', [])
166
-		);
167
-		return $config;
168
-	}
169
-
170
-	/**
171
-	 * @param string $plainContent
172
-	 * @param string $passPhrase
173
-	 * @param int $version
174
-	 * @param int $position
175
-	 * @return false|string
176
-	 * @throws EncryptionFailedException
177
-	 */
178
-	public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
179
-
180
-		if (!$plainContent) {
181
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
182
-				['app' => 'encryption']);
183
-			return false;
184
-		}
185
-
186
-		$iv = $this->generateIv();
187
-
188
-		$encryptedContent = $this->encrypt($plainContent,
189
-			$iv,
190
-			$passPhrase,
191
-			$this->getCipher());
192
-
193
-		// Create a signature based on the key as well as the current version
194
-		$sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position);
195
-
196
-		// combine content to encrypt the IV identifier and actual IV
197
-		$catFile = $this->concatIV($encryptedContent, $iv);
198
-		$catFile = $this->concatSig($catFile, $sig);
199
-		return $this->addPadding($catFile);
200
-	}
201
-
202
-	/**
203
-	 * generate header for encrypted file
204
-	 *
205
-	 * @param string $keyFormat (can be 'hash' or 'password')
206
-	 * @return string
207
-	 * @throws \InvalidArgumentException
208
-	 */
209
-	public function generateHeader($keyFormat = 'hash') {
210
-
211
-		if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
212
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
213
-		}
214
-
215
-		$cipher = $this->getCipher();
216
-
217
-		$header = self::HEADER_START
218
-			. ':cipher:' . $cipher
219
-			. ':keyFormat:' . $keyFormat
220
-			. ':' . self::HEADER_END;
221
-
222
-		return $header;
223
-	}
224
-
225
-	/**
226
-	 * @param string $plainContent
227
-	 * @param string $iv
228
-	 * @param string $passPhrase
229
-	 * @param string $cipher
230
-	 * @return string
231
-	 * @throws EncryptionFailedException
232
-	 */
233
-	private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
234
-		$encryptedContent = openssl_encrypt($plainContent,
235
-			$cipher,
236
-			$passPhrase,
237
-			false,
238
-			$iv);
239
-
240
-		if (!$encryptedContent) {
241
-			$error = 'Encryption (symmetric) of content failed';
242
-			$this->logger->error($error . openssl_error_string(),
243
-				['app' => 'encryption']);
244
-			throw new EncryptionFailedException($error);
245
-		}
246
-
247
-		return $encryptedContent;
248
-	}
249
-
250
-	/**
251
-	 * return Cipher either from config.php or the default cipher defined in
252
-	 * this class
253
-	 *
254
-	 * @return string
255
-	 */
256
-	public function getCipher() {
257
-		$cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
258
-		if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
259
-			$this->logger->warning(
260
-					sprintf(
261
-							'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
262
-							$cipher,
263
-							self::DEFAULT_CIPHER
264
-					),
265
-				['app' => 'encryption']);
266
-			$cipher = self::DEFAULT_CIPHER;
267
-		}
268
-
269
-		// Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
270
-		if(OPENSSL_VERSION_NUMBER < 0x1000101f) {
271
-			if($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
272
-				$cipher = self::LEGACY_CIPHER;
273
-			}
274
-		}
275
-
276
-		return $cipher;
277
-	}
278
-
279
-	/**
280
-	 * get key size depending on the cipher
281
-	 *
282
-	 * @param string $cipher
283
-	 * @return int
284
-	 * @throws \InvalidArgumentException
285
-	 */
286
-	protected function getKeySize($cipher) {
287
-		if(isset($this->supportedCiphersAndKeySize[$cipher])) {
288
-			return $this->supportedCiphersAndKeySize[$cipher];
289
-		}
290
-
291
-		throw new \InvalidArgumentException(
292
-			sprintf(
293
-					'Unsupported cipher (%s) defined.',
294
-					$cipher
295
-			)
296
-		);
297
-	}
298
-
299
-	/**
300
-	 * get legacy cipher
301
-	 *
302
-	 * @return string
303
-	 */
304
-	public function getLegacyCipher() {
305
-		return self::LEGACY_CIPHER;
306
-	}
307
-
308
-	/**
309
-	 * @param string $encryptedContent
310
-	 * @param string $iv
311
-	 * @return string
312
-	 */
313
-	private function concatIV($encryptedContent, $iv) {
314
-		return $encryptedContent . '00iv00' . $iv;
315
-	}
316
-
317
-	/**
318
-	 * @param string $encryptedContent
319
-	 * @param string $signature
320
-	 * @return string
321
-	 */
322
-	private function concatSig($encryptedContent, $signature) {
323
-		return $encryptedContent . '00sig00' . $signature;
324
-	}
325
-
326
-	/**
327
-	 * Note: This is _NOT_ a padding used for encryption purposes. It is solely
328
-	 * used to achieve the PHP stream size. It has _NOTHING_ to do with the
329
-	 * encrypted content and is not used in any crypto primitive.
330
-	 *
331
-	 * @param string $data
332
-	 * @return string
333
-	 */
334
-	private function addPadding($data) {
335
-		return $data . 'xxx';
336
-	}
337
-
338
-	/**
339
-	 * generate password hash used to encrypt the users private key
340
-	 *
341
-	 * @param string $password
342
-	 * @param string $cipher
343
-	 * @param string $uid only used for user keys
344
-	 * @return string
345
-	 */
346
-	protected function generatePasswordHash($password, $cipher, $uid = '') {
347
-		$instanceId = $this->config->getSystemValue('instanceid');
348
-		$instanceSecret = $this->config->getSystemValue('secret');
349
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
350
-		$keySize = $this->getKeySize($cipher);
351
-
352
-		$hash = hash_pbkdf2(
353
-			'sha256',
354
-			$password,
355
-			$salt,
356
-			100000,
357
-			$keySize,
358
-			true
359
-		);
360
-
361
-		return $hash;
362
-	}
363
-
364
-	/**
365
-	 * encrypt private key
366
-	 *
367
-	 * @param string $privateKey
368
-	 * @param string $password
369
-	 * @param string $uid for regular users, empty for system keys
370
-	 * @return false|string
371
-	 */
372
-	public function encryptPrivateKey($privateKey, $password, $uid = '') {
373
-		$cipher = $this->getCipher();
374
-		$hash = $this->generatePasswordHash($password, $cipher, $uid);
375
-		$encryptedKey = $this->symmetricEncryptFileContent(
376
-			$privateKey,
377
-			$hash,
378
-			0,
379
-			0
380
-		);
381
-
382
-		return $encryptedKey;
383
-	}
384
-
385
-	/**
386
-	 * @param string $privateKey
387
-	 * @param string $password
388
-	 * @param string $uid for regular users, empty for system keys
389
-	 * @return false|string
390
-	 */
391
-	public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
392
-
393
-		$header = $this->parseHeader($privateKey);
394
-
395
-		if (isset($header['cipher'])) {
396
-			$cipher = $header['cipher'];
397
-		} else {
398
-			$cipher = self::LEGACY_CIPHER;
399
-		}
400
-
401
-		if (isset($header['keyFormat'])) {
402
-			$keyFormat = $header['keyFormat'];
403
-		} else {
404
-			$keyFormat = self::LEGACY_KEY_FORMAT;
405
-		}
406
-
407
-		if ($keyFormat === 'hash') {
408
-			$password = $this->generatePasswordHash($password, $cipher, $uid);
409
-		}
410
-
411
-		// If we found a header we need to remove it from the key we want to decrypt
412
-		if (!empty($header)) {
413
-			$privateKey = substr($privateKey,
414
-				strpos($privateKey,
415
-					self::HEADER_END) + strlen(self::HEADER_END));
416
-		}
417
-
418
-		$plainKey = $this->symmetricDecryptFileContent(
419
-			$privateKey,
420
-			$password,
421
-			$cipher,
422
-			0
423
-		);
424
-
425
-		if ($this->isValidPrivateKey($plainKey) === false) {
426
-			return false;
427
-		}
428
-
429
-		return $plainKey;
430
-	}
431
-
432
-	/**
433
-	 * check if it is a valid private key
434
-	 *
435
-	 * @param string $plainKey
436
-	 * @return bool
437
-	 */
438
-	protected function isValidPrivateKey($plainKey) {
439
-		$res = openssl_get_privatekey($plainKey);
440
-		if (is_resource($res)) {
441
-			$sslInfo = openssl_pkey_get_details($res);
442
-			if (isset($sslInfo['key'])) {
443
-				return true;
444
-			}
445
-		}
446
-
447
-		return false;
448
-	}
449
-
450
-	/**
451
-	 * @param string $keyFileContents
452
-	 * @param string $passPhrase
453
-	 * @param string $cipher
454
-	 * @param int $version
455
-	 * @param int $position
456
-	 * @return string
457
-	 * @throws DecryptionFailedException
458
-	 */
459
-	public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
460
-		$catFile = $this->splitMetaData($keyFileContents, $cipher);
461
-
462
-		if ($catFile['signature'] !== false) {
463
-			$this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']);
464
-		}
465
-
466
-		return $this->decrypt($catFile['encrypted'],
467
-			$catFile['iv'],
468
-			$passPhrase,
469
-			$cipher);
470
-	}
471
-
472
-	/**
473
-	 * check for valid signature
474
-	 *
475
-	 * @param string $data
476
-	 * @param string $passPhrase
477
-	 * @param string $expectedSignature
478
-	 * @throws GenericEncryptionException
479
-	 */
480
-	private function checkSignature($data, $passPhrase, $expectedSignature) {
481
-		$signature = $this->createSignature($data, $passPhrase);
482
-		if (!hash_equals($expectedSignature, $signature)) {
483
-			throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
484
-		}
485
-	}
486
-
487
-	/**
488
-	 * create signature
489
-	 *
490
-	 * @param string $data
491
-	 * @param string $passPhrase
492
-	 * @return string
493
-	 */
494
-	private function createSignature($data, $passPhrase) {
495
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
496
-		return hash_hmac('sha256', $data, $passPhrase);
497
-	}
498
-
499
-
500
-	/**
501
-	 * remove padding
502
-	 *
503
-	 * @param string $padded
504
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
505
-	 * @return string|false
506
-	 */
507
-	private function removePadding($padded, $hasSignature = false) {
508
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
509
-			return substr($padded, 0, -2);
510
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
511
-			return substr($padded, 0, -3);
512
-		}
513
-		return false;
514
-	}
515
-
516
-	/**
517
-	 * split meta data from encrypted file
518
-	 * Note: for now, we assume that the meta data always start with the iv
519
-	 *       followed by the signature, if available
520
-	 *
521
-	 * @param string $catFile
522
-	 * @param string $cipher
523
-	 * @return array
524
-	 */
525
-	private function splitMetaData($catFile, $cipher) {
526
-		if ($this->hasSignature($catFile, $cipher)) {
527
-			$catFile = $this->removePadding($catFile, true);
528
-			$meta = substr($catFile, -93);
529
-			$iv = substr($meta, strlen('00iv00'), 16);
530
-			$sig = substr($meta, 22 + strlen('00sig00'));
531
-			$encrypted = substr($catFile, 0, -93);
532
-		} else {
533
-			$catFile = $this->removePadding($catFile);
534
-			$meta = substr($catFile, -22);
535
-			$iv = substr($meta, -16);
536
-			$sig = false;
537
-			$encrypted = substr($catFile, 0, -22);
538
-		}
539
-
540
-		return [
541
-			'encrypted' => $encrypted,
542
-			'iv' => $iv,
543
-			'signature' => $sig
544
-		];
545
-	}
546
-
547
-	/**
548
-	 * check if encrypted block is signed
549
-	 *
550
-	 * @param string $catFile
551
-	 * @param string $cipher
552
-	 * @return bool
553
-	 * @throws GenericEncryptionException
554
-	 */
555
-	private function hasSignature($catFile, $cipher) {
556
-		$meta = substr($catFile, -93);
557
-		$signaturePosition = strpos($meta, '00sig00');
558
-
559
-		// enforce signature for the new 'CTR' ciphers
560
-		if ($signaturePosition === false && stripos($cipher, 'ctr') !== false) {
561
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
562
-		}
563
-
564
-		return ($signaturePosition !== false);
565
-	}
566
-
567
-
568
-	/**
569
-	 * @param string $encryptedContent
570
-	 * @param string $iv
571
-	 * @param string $passPhrase
572
-	 * @param string $cipher
573
-	 * @return string
574
-	 * @throws DecryptionFailedException
575
-	 */
576
-	private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
577
-		$plainContent = openssl_decrypt($encryptedContent,
578
-			$cipher,
579
-			$passPhrase,
580
-			false,
581
-			$iv);
582
-
583
-		if ($plainContent) {
584
-			return $plainContent;
585
-		} else {
586
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
587
-		}
588
-	}
589
-
590
-	/**
591
-	 * @param string $data
592
-	 * @return array
593
-	 */
594
-	protected function parseHeader($data) {
595
-		$result = [];
596
-
597
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
598
-			$endAt = strpos($data, self::HEADER_END);
599
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
600
-
601
-			// +1 not to start with an ':' which would result in empty element at the beginning
602
-			$exploded = explode(':',
603
-				substr($header, strlen(self::HEADER_START) + 1));
604
-
605
-			$element = array_shift($exploded);
606
-
607
-			while ($element !== self::HEADER_END) {
608
-				$result[$element] = array_shift($exploded);
609
-				$element = array_shift($exploded);
610
-			}
611
-		}
612
-
613
-		return $result;
614
-	}
615
-
616
-	/**
617
-	 * generate initialization vector
618
-	 *
619
-	 * @return string
620
-	 * @throws GenericEncryptionException
621
-	 */
622
-	private function generateIv() {
623
-		return random_bytes(16);
624
-	}
625
-
626
-	/**
627
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
628
-	 * as file key
629
-	 *
630
-	 * @return string
631
-	 * @throws \Exception
632
-	 */
633
-	public function generateFileKey() {
634
-		return random_bytes(32);
635
-	}
636
-
637
-	/**
638
-	 * @param $encKeyFile
639
-	 * @param $shareKey
640
-	 * @param $privateKey
641
-	 * @return string
642
-	 * @throws MultiKeyDecryptException
643
-	 */
644
-	public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
645
-		if (!$encKeyFile) {
646
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
647
-		}
648
-
649
-		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
650
-			return $plainContent;
651
-		} else {
652
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
653
-		}
654
-	}
655
-
656
-	/**
657
-	 * @param string $plainContent
658
-	 * @param array $keyFiles
659
-	 * @return array
660
-	 * @throws MultiKeyEncryptException
661
-	 */
662
-	public function multiKeyEncrypt($plainContent, array $keyFiles) {
663
-		// openssl_seal returns false without errors if plaincontent is empty
664
-		// so trigger our own error
665
-		if (empty($plainContent)) {
666
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
667
-		}
668
-
669
-		// Set empty vars to be set by openssl by reference
670
-		$sealed = '';
671
-		$shareKeys = [];
672
-		$mappedShareKeys = [];
673
-
674
-		if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
675
-			$i = 0;
676
-
677
-			// Ensure each shareKey is labelled with its corresponding key id
678
-			foreach ($keyFiles as $userId => $publicKey) {
679
-				$mappedShareKeys[$userId] = $shareKeys[$i];
680
-				$i++;
681
-			}
682
-
683
-			return [
684
-				'keys' => $mappedShareKeys,
685
-				'data' => $sealed
686
-			];
687
-		} else {
688
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
689
-		}
690
-	}
58
+    const DEFAULT_CIPHER = 'AES-256-CTR';
59
+    // default cipher from old Nextcloud versions
60
+    const LEGACY_CIPHER = 'AES-128-CFB';
61
+
62
+    // default key format, old Nextcloud version encrypted the private key directly
63
+    // with the user password
64
+    const LEGACY_KEY_FORMAT = 'password';
65
+
66
+    const HEADER_START = 'HBEGIN';
67
+    const HEADER_END = 'HEND';
68
+
69
+    /** @var ILogger */
70
+    private $logger;
71
+
72
+    /** @var string */
73
+    private $user;
74
+
75
+    /** @var IConfig */
76
+    private $config;
77
+
78
+    /** @var array */
79
+    private $supportedKeyFormats;
80
+
81
+    /** @var IL10N */
82
+    private $l;
83
+
84
+    /** @var array */
85
+    private $supportedCiphersAndKeySize = [
86
+        'AES-256-CTR' => 32,
87
+        'AES-128-CTR' => 16,
88
+        'AES-256-CFB' => 32,
89
+        'AES-128-CFB' => 16,
90
+    ];
91
+
92
+    /**
93
+     * @param ILogger $logger
94
+     * @param IUserSession $userSession
95
+     * @param IConfig $config
96
+     * @param IL10N $l
97
+     */
98
+    public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
99
+        $this->logger = $logger;
100
+        $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
101
+        $this->config = $config;
102
+        $this->l = $l;
103
+        $this->supportedKeyFormats = ['hash', 'password'];
104
+    }
105
+
106
+    /**
107
+     * create new private/public key-pair for user
108
+     *
109
+     * @return array|bool
110
+     */
111
+    public function createKeyPair() {
112
+
113
+        $log = $this->logger;
114
+        $res = $this->getOpenSSLPKey();
115
+
116
+        if (!$res) {
117
+            $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
118
+                ['app' => 'encryption']);
119
+
120
+            if (openssl_error_string()) {
121
+                $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
122
+                    ['app' => 'encryption']);
123
+            }
124
+        } elseif (openssl_pkey_export($res,
125
+            $privateKey,
126
+            null,
127
+            $this->getOpenSSLConfig())) {
128
+            $keyDetails = openssl_pkey_get_details($res);
129
+            $publicKey = $keyDetails['key'];
130
+
131
+            return [
132
+                'publicKey' => $publicKey,
133
+                'privateKey' => $privateKey
134
+            ];
135
+        }
136
+        $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
137
+            ['app' => 'encryption']);
138
+        if (openssl_error_string()) {
139
+            $log->error('Encryption Library:' . openssl_error_string(),
140
+                ['app' => 'encryption']);
141
+        }
142
+
143
+        return false;
144
+    }
145
+
146
+    /**
147
+     * Generates a new private key
148
+     *
149
+     * @return resource
150
+     */
151
+    public function getOpenSSLPKey() {
152
+        $config = $this->getOpenSSLConfig();
153
+        return openssl_pkey_new($config);
154
+    }
155
+
156
+    /**
157
+     * get openSSL Config
158
+     *
159
+     * @return array
160
+     */
161
+    private function getOpenSSLConfig() {
162
+        $config = ['private_key_bits' => 4096];
163
+        $config = array_merge(
164
+            $config,
165
+            $this->config->getSystemValue('openssl', [])
166
+        );
167
+        return $config;
168
+    }
169
+
170
+    /**
171
+     * @param string $plainContent
172
+     * @param string $passPhrase
173
+     * @param int $version
174
+     * @param int $position
175
+     * @return false|string
176
+     * @throws EncryptionFailedException
177
+     */
178
+    public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
179
+
180
+        if (!$plainContent) {
181
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
182
+                ['app' => 'encryption']);
183
+            return false;
184
+        }
185
+
186
+        $iv = $this->generateIv();
187
+
188
+        $encryptedContent = $this->encrypt($plainContent,
189
+            $iv,
190
+            $passPhrase,
191
+            $this->getCipher());
192
+
193
+        // Create a signature based on the key as well as the current version
194
+        $sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position);
195
+
196
+        // combine content to encrypt the IV identifier and actual IV
197
+        $catFile = $this->concatIV($encryptedContent, $iv);
198
+        $catFile = $this->concatSig($catFile, $sig);
199
+        return $this->addPadding($catFile);
200
+    }
201
+
202
+    /**
203
+     * generate header for encrypted file
204
+     *
205
+     * @param string $keyFormat (can be 'hash' or 'password')
206
+     * @return string
207
+     * @throws \InvalidArgumentException
208
+     */
209
+    public function generateHeader($keyFormat = 'hash') {
210
+
211
+        if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
212
+            throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
213
+        }
214
+
215
+        $cipher = $this->getCipher();
216
+
217
+        $header = self::HEADER_START
218
+            . ':cipher:' . $cipher
219
+            . ':keyFormat:' . $keyFormat
220
+            . ':' . self::HEADER_END;
221
+
222
+        return $header;
223
+    }
224
+
225
+    /**
226
+     * @param string $plainContent
227
+     * @param string $iv
228
+     * @param string $passPhrase
229
+     * @param string $cipher
230
+     * @return string
231
+     * @throws EncryptionFailedException
232
+     */
233
+    private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
234
+        $encryptedContent = openssl_encrypt($plainContent,
235
+            $cipher,
236
+            $passPhrase,
237
+            false,
238
+            $iv);
239
+
240
+        if (!$encryptedContent) {
241
+            $error = 'Encryption (symmetric) of content failed';
242
+            $this->logger->error($error . openssl_error_string(),
243
+                ['app' => 'encryption']);
244
+            throw new EncryptionFailedException($error);
245
+        }
246
+
247
+        return $encryptedContent;
248
+    }
249
+
250
+    /**
251
+     * return Cipher either from config.php or the default cipher defined in
252
+     * this class
253
+     *
254
+     * @return string
255
+     */
256
+    public function getCipher() {
257
+        $cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
258
+        if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
259
+            $this->logger->warning(
260
+                    sprintf(
261
+                            'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
262
+                            $cipher,
263
+                            self::DEFAULT_CIPHER
264
+                    ),
265
+                ['app' => 'encryption']);
266
+            $cipher = self::DEFAULT_CIPHER;
267
+        }
268
+
269
+        // Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
270
+        if(OPENSSL_VERSION_NUMBER < 0x1000101f) {
271
+            if($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
272
+                $cipher = self::LEGACY_CIPHER;
273
+            }
274
+        }
275
+
276
+        return $cipher;
277
+    }
278
+
279
+    /**
280
+     * get key size depending on the cipher
281
+     *
282
+     * @param string $cipher
283
+     * @return int
284
+     * @throws \InvalidArgumentException
285
+     */
286
+    protected function getKeySize($cipher) {
287
+        if(isset($this->supportedCiphersAndKeySize[$cipher])) {
288
+            return $this->supportedCiphersAndKeySize[$cipher];
289
+        }
290
+
291
+        throw new \InvalidArgumentException(
292
+            sprintf(
293
+                    'Unsupported cipher (%s) defined.',
294
+                    $cipher
295
+            )
296
+        );
297
+    }
298
+
299
+    /**
300
+     * get legacy cipher
301
+     *
302
+     * @return string
303
+     */
304
+    public function getLegacyCipher() {
305
+        return self::LEGACY_CIPHER;
306
+    }
307
+
308
+    /**
309
+     * @param string $encryptedContent
310
+     * @param string $iv
311
+     * @return string
312
+     */
313
+    private function concatIV($encryptedContent, $iv) {
314
+        return $encryptedContent . '00iv00' . $iv;
315
+    }
316
+
317
+    /**
318
+     * @param string $encryptedContent
319
+     * @param string $signature
320
+     * @return string
321
+     */
322
+    private function concatSig($encryptedContent, $signature) {
323
+        return $encryptedContent . '00sig00' . $signature;
324
+    }
325
+
326
+    /**
327
+     * Note: This is _NOT_ a padding used for encryption purposes. It is solely
328
+     * used to achieve the PHP stream size. It has _NOTHING_ to do with the
329
+     * encrypted content and is not used in any crypto primitive.
330
+     *
331
+     * @param string $data
332
+     * @return string
333
+     */
334
+    private function addPadding($data) {
335
+        return $data . 'xxx';
336
+    }
337
+
338
+    /**
339
+     * generate password hash used to encrypt the users private key
340
+     *
341
+     * @param string $password
342
+     * @param string $cipher
343
+     * @param string $uid only used for user keys
344
+     * @return string
345
+     */
346
+    protected function generatePasswordHash($password, $cipher, $uid = '') {
347
+        $instanceId = $this->config->getSystemValue('instanceid');
348
+        $instanceSecret = $this->config->getSystemValue('secret');
349
+        $salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
350
+        $keySize = $this->getKeySize($cipher);
351
+
352
+        $hash = hash_pbkdf2(
353
+            'sha256',
354
+            $password,
355
+            $salt,
356
+            100000,
357
+            $keySize,
358
+            true
359
+        );
360
+
361
+        return $hash;
362
+    }
363
+
364
+    /**
365
+     * encrypt private key
366
+     *
367
+     * @param string $privateKey
368
+     * @param string $password
369
+     * @param string $uid for regular users, empty for system keys
370
+     * @return false|string
371
+     */
372
+    public function encryptPrivateKey($privateKey, $password, $uid = '') {
373
+        $cipher = $this->getCipher();
374
+        $hash = $this->generatePasswordHash($password, $cipher, $uid);
375
+        $encryptedKey = $this->symmetricEncryptFileContent(
376
+            $privateKey,
377
+            $hash,
378
+            0,
379
+            0
380
+        );
381
+
382
+        return $encryptedKey;
383
+    }
384
+
385
+    /**
386
+     * @param string $privateKey
387
+     * @param string $password
388
+     * @param string $uid for regular users, empty for system keys
389
+     * @return false|string
390
+     */
391
+    public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
392
+
393
+        $header = $this->parseHeader($privateKey);
394
+
395
+        if (isset($header['cipher'])) {
396
+            $cipher = $header['cipher'];
397
+        } else {
398
+            $cipher = self::LEGACY_CIPHER;
399
+        }
400
+
401
+        if (isset($header['keyFormat'])) {
402
+            $keyFormat = $header['keyFormat'];
403
+        } else {
404
+            $keyFormat = self::LEGACY_KEY_FORMAT;
405
+        }
406
+
407
+        if ($keyFormat === 'hash') {
408
+            $password = $this->generatePasswordHash($password, $cipher, $uid);
409
+        }
410
+
411
+        // If we found a header we need to remove it from the key we want to decrypt
412
+        if (!empty($header)) {
413
+            $privateKey = substr($privateKey,
414
+                strpos($privateKey,
415
+                    self::HEADER_END) + strlen(self::HEADER_END));
416
+        }
417
+
418
+        $plainKey = $this->symmetricDecryptFileContent(
419
+            $privateKey,
420
+            $password,
421
+            $cipher,
422
+            0
423
+        );
424
+
425
+        if ($this->isValidPrivateKey($plainKey) === false) {
426
+            return false;
427
+        }
428
+
429
+        return $plainKey;
430
+    }
431
+
432
+    /**
433
+     * check if it is a valid private key
434
+     *
435
+     * @param string $plainKey
436
+     * @return bool
437
+     */
438
+    protected function isValidPrivateKey($plainKey) {
439
+        $res = openssl_get_privatekey($plainKey);
440
+        if (is_resource($res)) {
441
+            $sslInfo = openssl_pkey_get_details($res);
442
+            if (isset($sslInfo['key'])) {
443
+                return true;
444
+            }
445
+        }
446
+
447
+        return false;
448
+    }
449
+
450
+    /**
451
+     * @param string $keyFileContents
452
+     * @param string $passPhrase
453
+     * @param string $cipher
454
+     * @param int $version
455
+     * @param int $position
456
+     * @return string
457
+     * @throws DecryptionFailedException
458
+     */
459
+    public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
460
+        $catFile = $this->splitMetaData($keyFileContents, $cipher);
461
+
462
+        if ($catFile['signature'] !== false) {
463
+            $this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']);
464
+        }
465
+
466
+        return $this->decrypt($catFile['encrypted'],
467
+            $catFile['iv'],
468
+            $passPhrase,
469
+            $cipher);
470
+    }
471
+
472
+    /**
473
+     * check for valid signature
474
+     *
475
+     * @param string $data
476
+     * @param string $passPhrase
477
+     * @param string $expectedSignature
478
+     * @throws GenericEncryptionException
479
+     */
480
+    private function checkSignature($data, $passPhrase, $expectedSignature) {
481
+        $signature = $this->createSignature($data, $passPhrase);
482
+        if (!hash_equals($expectedSignature, $signature)) {
483
+            throw new GenericEncryptionException('Bad Signature', $this->l->t('Bad Signature'));
484
+        }
485
+    }
486
+
487
+    /**
488
+     * create signature
489
+     *
490
+     * @param string $data
491
+     * @param string $passPhrase
492
+     * @return string
493
+     */
494
+    private function createSignature($data, $passPhrase) {
495
+        $passPhrase = hash('sha512', $passPhrase . 'a', true);
496
+        return hash_hmac('sha256', $data, $passPhrase);
497
+    }
498
+
499
+
500
+    /**
501
+     * remove padding
502
+     *
503
+     * @param string $padded
504
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
505
+     * @return string|false
506
+     */
507
+    private function removePadding($padded, $hasSignature = false) {
508
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
509
+            return substr($padded, 0, -2);
510
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
511
+            return substr($padded, 0, -3);
512
+        }
513
+        return false;
514
+    }
515
+
516
+    /**
517
+     * split meta data from encrypted file
518
+     * Note: for now, we assume that the meta data always start with the iv
519
+     *       followed by the signature, if available
520
+     *
521
+     * @param string $catFile
522
+     * @param string $cipher
523
+     * @return array
524
+     */
525
+    private function splitMetaData($catFile, $cipher) {
526
+        if ($this->hasSignature($catFile, $cipher)) {
527
+            $catFile = $this->removePadding($catFile, true);
528
+            $meta = substr($catFile, -93);
529
+            $iv = substr($meta, strlen('00iv00'), 16);
530
+            $sig = substr($meta, 22 + strlen('00sig00'));
531
+            $encrypted = substr($catFile, 0, -93);
532
+        } else {
533
+            $catFile = $this->removePadding($catFile);
534
+            $meta = substr($catFile, -22);
535
+            $iv = substr($meta, -16);
536
+            $sig = false;
537
+            $encrypted = substr($catFile, 0, -22);
538
+        }
539
+
540
+        return [
541
+            'encrypted' => $encrypted,
542
+            'iv' => $iv,
543
+            'signature' => $sig
544
+        ];
545
+    }
546
+
547
+    /**
548
+     * check if encrypted block is signed
549
+     *
550
+     * @param string $catFile
551
+     * @param string $cipher
552
+     * @return bool
553
+     * @throws GenericEncryptionException
554
+     */
555
+    private function hasSignature($catFile, $cipher) {
556
+        $meta = substr($catFile, -93);
557
+        $signaturePosition = strpos($meta, '00sig00');
558
+
559
+        // enforce signature for the new 'CTR' ciphers
560
+        if ($signaturePosition === false && stripos($cipher, 'ctr') !== false) {
561
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
562
+        }
563
+
564
+        return ($signaturePosition !== false);
565
+    }
566
+
567
+
568
+    /**
569
+     * @param string $encryptedContent
570
+     * @param string $iv
571
+     * @param string $passPhrase
572
+     * @param string $cipher
573
+     * @return string
574
+     * @throws DecryptionFailedException
575
+     */
576
+    private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
577
+        $plainContent = openssl_decrypt($encryptedContent,
578
+            $cipher,
579
+            $passPhrase,
580
+            false,
581
+            $iv);
582
+
583
+        if ($plainContent) {
584
+            return $plainContent;
585
+        } else {
586
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
587
+        }
588
+    }
589
+
590
+    /**
591
+     * @param string $data
592
+     * @return array
593
+     */
594
+    protected function parseHeader($data) {
595
+        $result = [];
596
+
597
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
598
+            $endAt = strpos($data, self::HEADER_END);
599
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
600
+
601
+            // +1 not to start with an ':' which would result in empty element at the beginning
602
+            $exploded = explode(':',
603
+                substr($header, strlen(self::HEADER_START) + 1));
604
+
605
+            $element = array_shift($exploded);
606
+
607
+            while ($element !== self::HEADER_END) {
608
+                $result[$element] = array_shift($exploded);
609
+                $element = array_shift($exploded);
610
+            }
611
+        }
612
+
613
+        return $result;
614
+    }
615
+
616
+    /**
617
+     * generate initialization vector
618
+     *
619
+     * @return string
620
+     * @throws GenericEncryptionException
621
+     */
622
+    private function generateIv() {
623
+        return random_bytes(16);
624
+    }
625
+
626
+    /**
627
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
628
+     * as file key
629
+     *
630
+     * @return string
631
+     * @throws \Exception
632
+     */
633
+    public function generateFileKey() {
634
+        return random_bytes(32);
635
+    }
636
+
637
+    /**
638
+     * @param $encKeyFile
639
+     * @param $shareKey
640
+     * @param $privateKey
641
+     * @return string
642
+     * @throws MultiKeyDecryptException
643
+     */
644
+    public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
645
+        if (!$encKeyFile) {
646
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
647
+        }
648
+
649
+        if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
650
+            return $plainContent;
651
+        } else {
652
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
653
+        }
654
+    }
655
+
656
+    /**
657
+     * @param string $plainContent
658
+     * @param array $keyFiles
659
+     * @return array
660
+     * @throws MultiKeyEncryptException
661
+     */
662
+    public function multiKeyEncrypt($plainContent, array $keyFiles) {
663
+        // openssl_seal returns false without errors if plaincontent is empty
664
+        // so trigger our own error
665
+        if (empty($plainContent)) {
666
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
667
+        }
668
+
669
+        // Set empty vars to be set by openssl by reference
670
+        $sealed = '';
671
+        $shareKeys = [];
672
+        $mappedShareKeys = [];
673
+
674
+        if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
675
+            $i = 0;
676
+
677
+            // Ensure each shareKey is labelled with its corresponding key id
678
+            foreach ($keyFiles as $userId => $publicKey) {
679
+                $mappedShareKeys[$userId] = $shareKeys[$i];
680
+                $i++;
681
+            }
682
+
683
+            return [
684
+                'keys' => $mappedShareKeys,
685
+                'data' => $sealed
686
+            ];
687
+        } else {
688
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
689
+        }
690
+    }
691 691
 }
692 692
 
Please login to merge, or discard this patch.
Spacing   +19 added lines, -19 removed lines patch added patch discarded remove patch
@@ -118,7 +118,7 @@  discard block
 block discarded – undo
118 118
 				['app' => 'encryption']);
119 119
 
120 120
 			if (openssl_error_string()) {
121
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
121
+				$log->error('Encryption library openssl_pkey_new() fails: '.openssl_error_string(),
122 122
 					['app' => 'encryption']);
123 123
 			}
124 124
 		} elseif (openssl_pkey_export($res,
@@ -133,10 +133,10 @@  discard block
 block discarded – undo
133 133
 				'privateKey' => $privateKey
134 134
 			];
135 135
 		}
136
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
136
+		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.'.$this->user,
137 137
 			['app' => 'encryption']);
138 138
 		if (openssl_error_string()) {
139
-			$log->error('Encryption Library:' . openssl_error_string(),
139
+			$log->error('Encryption Library:'.openssl_error_string(),
140 140
 				['app' => 'encryption']);
141 141
 		}
142 142
 
@@ -209,15 +209,15 @@  discard block
 block discarded – undo
209 209
 	public function generateHeader($keyFormat = 'hash') {
210 210
 
211 211
 		if (in_array($keyFormat, $this->supportedKeyFormats, true) === false) {
212
-			throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
212
+			throw new \InvalidArgumentException('key format "'.$keyFormat.'" is not supported');
213 213
 		}
214 214
 
215 215
 		$cipher = $this->getCipher();
216 216
 
217 217
 		$header = self::HEADER_START
218
-			. ':cipher:' . $cipher
219
-			. ':keyFormat:' . $keyFormat
220
-			. ':' . self::HEADER_END;
218
+			. ':cipher:'.$cipher
219
+			. ':keyFormat:'.$keyFormat
220
+			. ':'.self::HEADER_END;
221 221
 
222 222
 		return $header;
223 223
 	}
@@ -239,7 +239,7 @@  discard block
 block discarded – undo
239 239
 
240 240
 		if (!$encryptedContent) {
241 241
 			$error = 'Encryption (symmetric) of content failed';
242
-			$this->logger->error($error . openssl_error_string(),
242
+			$this->logger->error($error.openssl_error_string(),
243 243
 				['app' => 'encryption']);
244 244
 			throw new EncryptionFailedException($error);
245 245
 		}
@@ -267,8 +267,8 @@  discard block
 block discarded – undo
267 267
 		}
268 268
 
269 269
 		// Workaround for OpenSSL 0.9.8. Fallback to an old cipher that should work.
270
-		if(OPENSSL_VERSION_NUMBER < 0x1000101f) {
271
-			if($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
270
+		if (OPENSSL_VERSION_NUMBER < 0x1000101f) {
271
+			if ($cipher === 'AES-256-CTR' || $cipher === 'AES-128-CTR') {
272 272
 				$cipher = self::LEGACY_CIPHER;
273 273
 			}
274 274
 		}
@@ -284,7 +284,7 @@  discard block
 block discarded – undo
284 284
 	 * @throws \InvalidArgumentException
285 285
 	 */
286 286
 	protected function getKeySize($cipher) {
287
-		if(isset($this->supportedCiphersAndKeySize[$cipher])) {
287
+		if (isset($this->supportedCiphersAndKeySize[$cipher])) {
288 288
 			return $this->supportedCiphersAndKeySize[$cipher];
289 289
 		}
290 290
 
@@ -311,7 +311,7 @@  discard block
 block discarded – undo
311 311
 	 * @return string
312 312
 	 */
313 313
 	private function concatIV($encryptedContent, $iv) {
314
-		return $encryptedContent . '00iv00' . $iv;
314
+		return $encryptedContent.'00iv00'.$iv;
315 315
 	}
316 316
 
317 317
 	/**
@@ -320,7 +320,7 @@  discard block
 block discarded – undo
320 320
 	 * @return string
321 321
 	 */
322 322
 	private function concatSig($encryptedContent, $signature) {
323
-		return $encryptedContent . '00sig00' . $signature;
323
+		return $encryptedContent.'00sig00'.$signature;
324 324
 	}
325 325
 
326 326
 	/**
@@ -332,7 +332,7 @@  discard block
 block discarded – undo
332 332
 	 * @return string
333 333
 	 */
334 334
 	private function addPadding($data) {
335
-		return $data . 'xxx';
335
+		return $data.'xxx';
336 336
 	}
337 337
 
338 338
 	/**
@@ -346,7 +346,7 @@  discard block
 block discarded – undo
346 346
 	protected function generatePasswordHash($password, $cipher, $uid = '') {
347 347
 		$instanceId = $this->config->getSystemValue('instanceid');
348 348
 		$instanceSecret = $this->config->getSystemValue('secret');
349
-		$salt = hash('sha256', $uid . $instanceId . $instanceSecret, true);
349
+		$salt = hash('sha256', $uid.$instanceId.$instanceSecret, true);
350 350
 		$keySize = $this->getKeySize($cipher);
351 351
 
352 352
 		$hash = hash_pbkdf2(
@@ -492,7 +492,7 @@  discard block
 block discarded – undo
492 492
 	 * @return string
493 493
 	 */
494 494
 	private function createSignature($data, $passPhrase) {
495
-		$passPhrase = hash('sha512', $passPhrase . 'a', true);
495
+		$passPhrase = hash('sha512', $passPhrase.'a', true);
496 496
 		return hash_hmac('sha256', $data, $passPhrase);
497 497
 	}
498 498
 
@@ -583,7 +583,7 @@  discard block
 block discarded – undo
583 583
 		if ($plainContent) {
584 584
 			return $plainContent;
585 585
 		} else {
586
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
586
+			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: '.openssl_error_string());
587 587
 		}
588 588
 	}
589 589
 
@@ -649,7 +649,7 @@  discard block
 block discarded – undo
649 649
 		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
650 650
 			return $plainContent;
651 651
 		} else {
652
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
652
+			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:'.openssl_error_string());
653 653
 		}
654 654
 	}
655 655
 
@@ -685,7 +685,7 @@  discard block
 block discarded – undo
685 685
 				'data' => $sealed
686 686
 			];
687 687
 		} else {
688
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
688
+			throw new MultiKeyEncryptException('multikeyencryption failed '.openssl_error_string());
689 689
 		}
690 690
 	}
691 691
 }
Please login to merge, or discard this patch.