Completed
Push — master ( 39468f...49d871 )
by Björn
14:59
created
apps/files_external/lib/Lib/Storage/Google.php 1 patch
Indentation   +675 added lines, -675 removed lines patch added patch discarded remove patch
@@ -41,684 +41,684 @@
 block discarded – undo
41 41
 use Icewind\Streams\RetryWrapper;
42 42
 
43 43
 set_include_path(get_include_path().PATH_SEPARATOR.
44
-	\OC_App::getAppPath('files_external').'/3rdparty/google-api-php-client/src');
44
+    \OC_App::getAppPath('files_external').'/3rdparty/google-api-php-client/src');
45 45
 require_once 'Google/autoload.php';
46 46
 
47 47
 class Google extends \OC\Files\Storage\Common {
48 48
 
49
-	private $client;
50
-	private $id;
51
-	private $service;
52
-	private $driveFiles;
53
-
54
-	// Google Doc mimetypes
55
-	const FOLDER = 'application/vnd.google-apps.folder';
56
-	const DOCUMENT = 'application/vnd.google-apps.document';
57
-	const SPREADSHEET = 'application/vnd.google-apps.spreadsheet';
58
-	const DRAWING = 'application/vnd.google-apps.drawing';
59
-	const PRESENTATION = 'application/vnd.google-apps.presentation';
60
-	const MAP = 'application/vnd.google-apps.map';
61
-
62
-	public function __construct($params) {
63
-		if (isset($params['configured']) && $params['configured'] === 'true'
64
-			&& isset($params['client_id']) && isset($params['client_secret'])
65
-			&& isset($params['token'])
66
-		) {
67
-			$this->client = new \Google_Client();
68
-			$this->client->setClientId($params['client_id']);
69
-			$this->client->setClientSecret($params['client_secret']);
70
-			$this->client->setScopes(array('https://www.googleapis.com/auth/drive'));
71
-			$this->client->setAccessToken($params['token']);
72
-			// if curl isn't available we're likely to run into
73
-			// https://github.com/google/google-api-php-client/issues/59
74
-			// - disable gzip to avoid it.
75
-			if (!function_exists('curl_version') || !function_exists('curl_exec')) {
76
-				$this->client->setClassConfig("Google_Http_Request", "disable_gzip", true);
77
-			}
78
-			// note: API connection is lazy
79
-			$this->service = new \Google_Service_Drive($this->client);
80
-			$token = json_decode($params['token'], true);
81
-			$this->id = 'google::'.substr($params['client_id'], 0, 30).$token['created'];
82
-		} else {
83
-			throw new \Exception('Creating Google storage failed');
84
-		}
85
-	}
86
-
87
-	public function getId() {
88
-		return $this->id;
89
-	}
90
-
91
-	/**
92
-	 * Get the Google_Service_Drive_DriveFile object for the specified path.
93
-	 * Returns false on failure.
94
-	 * @param string $path
95
-	 * @return \Google_Service_Drive_DriveFile|false
96
-	 */
97
-	private function getDriveFile($path) {
98
-		// Remove leading and trailing slashes
99
-		$path = trim($path, '/');
100
-		if ($path === '.') {
101
-			$path = '';
102
-		}
103
-		if (isset($this->driveFiles[$path])) {
104
-			return $this->driveFiles[$path];
105
-		} else if ($path === '') {
106
-			$root = $this->service->files->get('root');
107
-			$this->driveFiles[$path] = $root;
108
-			return $root;
109
-		} else {
110
-			// Google Drive SDK does not have methods for retrieving files by path
111
-			// Instead we must find the id of the parent folder of the file
112
-			$parentId = $this->getDriveFile('')->getId();
113
-			$folderNames = explode('/', $path);
114
-			$path = '';
115
-			// Loop through each folder of this path to get to the file
116
-			foreach ($folderNames as $name) {
117
-				// Reconstruct path from beginning
118
-				if ($path === '') {
119
-					$path .= $name;
120
-				} else {
121
-					$path .= '/'.$name;
122
-				}
123
-				if (isset($this->driveFiles[$path])) {
124
-					$parentId = $this->driveFiles[$path]->getId();
125
-				} else {
126
-					$q = "title='" . str_replace("'","\\'", $name) . "' and '" . str_replace("'","\\'", $parentId) . "' in parents and trashed = false";
127
-					$result = $this->service->files->listFiles(array('q' => $q))->getItems();
128
-					if (!empty($result)) {
129
-						// Google Drive allows files with the same name, Nextcloud doesn't
130
-						if (count($result) > 1) {
131
-							$this->onDuplicateFileDetected($path);
132
-							return false;
133
-						} else {
134
-							$file = current($result);
135
-							$this->driveFiles[$path] = $file;
136
-							$parentId = $file->getId();
137
-						}
138
-					} else {
139
-						// Google Docs have no extension in their title, so try without extension
140
-						$pos = strrpos($path, '.');
141
-						if ($pos !== false) {
142
-							$pathWithoutExt = substr($path, 0, $pos);
143
-							$file = $this->getDriveFile($pathWithoutExt);
144
-							if ($file && $this->isGoogleDocFile($file)) {
145
-								// Switch cached Google_Service_Drive_DriveFile to the correct index
146
-								unset($this->driveFiles[$pathWithoutExt]);
147
-								$this->driveFiles[$path] = $file;
148
-								$parentId = $file->getId();
149
-							} else {
150
-								return false;
151
-							}
152
-						} else {
153
-							return false;
154
-						}
155
-					}
156
-				}
157
-			}
158
-			return $this->driveFiles[$path];
159
-		}
160
-	}
161
-
162
-	/**
163
-	 * Set the Google_Service_Drive_DriveFile object in the cache
164
-	 * @param string $path
165
-	 * @param \Google_Service_Drive_DriveFile|false $file
166
-	 */
167
-	private function setDriveFile($path, $file) {
168
-		$path = trim($path, '/');
169
-		$this->driveFiles[$path] = $file;
170
-		if ($file === false) {
171
-			// Remove all children
172
-			$len = strlen($path);
173
-			foreach ($this->driveFiles as $key => $file) {
174
-				if (substr($key, 0, $len) === $path) {
175
-					unset($this->driveFiles[$key]);
176
-				}
177
-			}
178
-		}
179
-	}
180
-
181
-	/**
182
-	 * Write a log message to inform about duplicate file names
183
-	 * @param string $path
184
-	 */
185
-	private function onDuplicateFileDetected($path) {
186
-		$about = $this->service->about->get();
187
-		$user = $about->getName();
188
-		\OCP\Util::writeLog('files_external',
189
-			'Ignoring duplicate file name: '.$path.' on Google Drive for Google user: '.$user,
190
-			\OCP\Util::INFO
191
-		);
192
-	}
193
-
194
-	/**
195
-	 * Generate file extension for a Google Doc, choosing Open Document formats for download
196
-	 * @param string $mimetype
197
-	 * @return string
198
-	 */
199
-	private function getGoogleDocExtension($mimetype) {
200
-		if ($mimetype === self::DOCUMENT) {
201
-			return 'odt';
202
-		} else if ($mimetype === self::SPREADSHEET) {
203
-			return 'ods';
204
-		} else if ($mimetype === self::DRAWING) {
205
-			return 'jpg';
206
-		} else if ($mimetype === self::PRESENTATION) {
207
-			// Download as .odp is not available
208
-			return 'pdf';
209
-		} else {
210
-			return '';
211
-		}
212
-	}
213
-
214
-	/**
215
-	 * Returns whether the given drive file is a Google Doc file
216
-	 *
217
-	 * @param \Google_Service_Drive_DriveFile
218
-	 *
219
-	 * @return true if the file is a Google Doc file, false otherwise
220
-	 */
221
-	private function isGoogleDocFile($file) {
222
-		return $this->getGoogleDocExtension($file->getMimeType()) !== '';
223
-	}
224
-
225
-	public function mkdir($path) {
226
-		if (!$this->is_dir($path)) {
227
-			$parentFolder = $this->getDriveFile(dirname($path));
228
-			if ($parentFolder) {
229
-				$folder = new \Google_Service_Drive_DriveFile();
230
-				$folder->setTitle(basename($path));
231
-				$folder->setMimeType(self::FOLDER);
232
-				$parent = new \Google_Service_Drive_ParentReference();
233
-				$parent->setId($parentFolder->getId());
234
-				$folder->setParents(array($parent));
235
-				$result = $this->service->files->insert($folder);
236
-				if ($result) {
237
-					$this->setDriveFile($path, $result);
238
-				}
239
-				return (bool)$result;
240
-			}
241
-		}
242
-		return false;
243
-	}
244
-
245
-	public function rmdir($path) {
246
-		if (!$this->isDeletable($path)) {
247
-			return false;
248
-		}
249
-		if (trim($path, '/') === '') {
250
-			$dir = $this->opendir($path);
251
-			if(is_resource($dir)) {
252
-				while (($file = readdir($dir)) !== false) {
253
-					if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
254
-						if (!$this->unlink($path.'/'.$file)) {
255
-							return false;
256
-						}
257
-					}
258
-				}
259
-				closedir($dir);
260
-			}
261
-			$this->driveFiles = array();
262
-			return true;
263
-		} else {
264
-			return $this->unlink($path);
265
-		}
266
-	}
267
-
268
-	public function opendir($path) {
269
-		$folder = $this->getDriveFile($path);
270
-		if ($folder) {
271
-			$files = array();
272
-			$duplicates = array();
273
-			$pageToken = true;
274
-			while ($pageToken) {
275
-				$params = array();
276
-				if ($pageToken !== true) {
277
-					$params['pageToken'] = $pageToken;
278
-				}
279
-				$params['q'] = "'" . str_replace("'","\\'", $folder->getId()) . "' in parents and trashed = false";
280
-				$children = $this->service->files->listFiles($params);
281
-				foreach ($children->getItems() as $child) {
282
-					$name = $child->getTitle();
283
-					// Check if this is a Google Doc i.e. no extension in name
284
-					$extension = $child->getFileExtension();
285
-					if (empty($extension)) {
286
-						if ($child->getMimeType() === self::MAP) {
287
-							continue; // No method known to transfer map files, ignore it
288
-						} else if ($child->getMimeType() !== self::FOLDER) {
289
-							$name .= '.'.$this->getGoogleDocExtension($child->getMimeType());
290
-						}
291
-					}
292
-					if ($path === '') {
293
-						$filepath = $name;
294
-					} else {
295
-						$filepath = $path.'/'.$name;
296
-					}
297
-					// Google Drive allows files with the same name, Nextcloud doesn't
298
-					// Prevent opendir() from returning any duplicate files
299
-					$key = array_search($name, $files);
300
-					if ($key !== false || isset($duplicates[$filepath])) {
301
-						if (!isset($duplicates[$filepath])) {
302
-							$duplicates[$filepath] = true;
303
-							$this->setDriveFile($filepath, false);
304
-							unset($files[$key]);
305
-							$this->onDuplicateFileDetected($filepath);
306
-						}
307
-					} else {
308
-						// Cache the Google_Service_Drive_DriveFile for future use
309
-						$this->setDriveFile($filepath, $child);
310
-						$files[] = $name;
311
-					}
312
-				}
313
-				$pageToken = $children->getNextPageToken();
314
-			}
315
-			return IteratorDirectory::wrap($files);
316
-		} else {
317
-			return false;
318
-		}
319
-	}
320
-
321
-	public function stat($path) {
322
-		$file = $this->getDriveFile($path);
323
-		if ($file) {
324
-			$stat = array();
325
-			if ($this->filetype($path) === 'dir') {
326
-				$stat['size'] = 0;
327
-			} else {
328
-				// Check if this is a Google Doc
329
-				if ($this->isGoogleDocFile($file)) {
330
-					// Return unknown file size
331
-					$stat['size'] = \OCP\Files\FileInfo::SPACE_UNKNOWN;
332
-				} else {
333
-					$stat['size'] = $file->getFileSize();
334
-				}
335
-			}
336
-			$stat['atime'] = strtotime($file->getLastViewedByMeDate());
337
-			$stat['mtime'] = strtotime($file->getModifiedDate());
338
-			$stat['ctime'] = strtotime($file->getCreatedDate());
339
-			return $stat;
340
-		} else {
341
-			return false;
342
-		}
343
-	}
344
-
345
-	public function filetype($path) {
346
-		if ($path === '') {
347
-			return 'dir';
348
-		} else {
349
-			$file = $this->getDriveFile($path);
350
-			if ($file) {
351
-				if ($file->getMimeType() === self::FOLDER) {
352
-					return 'dir';
353
-				} else {
354
-					return 'file';
355
-				}
356
-			} else {
357
-				return false;
358
-			}
359
-		}
360
-	}
361
-
362
-	public function isUpdatable($path) {
363
-		$file = $this->getDriveFile($path);
364
-		if ($file) {
365
-			return $file->getEditable();
366
-		} else {
367
-			return false;
368
-		}
369
-	}
370
-
371
-	public function file_exists($path) {
372
-		return (bool)$this->getDriveFile($path);
373
-	}
374
-
375
-	public function unlink($path) {
376
-		$file = $this->getDriveFile($path);
377
-		if ($file) {
378
-			$result = $this->service->files->trash($file->getId());
379
-			if ($result) {
380
-				$this->setDriveFile($path, false);
381
-			}
382
-			return (bool)$result;
383
-		} else {
384
-			return false;
385
-		}
386
-	}
387
-
388
-	public function rename($path1, $path2) {
389
-		$file = $this->getDriveFile($path1);
390
-		if ($file) {
391
-			$newFile = $this->getDriveFile($path2);
392
-			if (dirname($path1) === dirname($path2)) {
393
-				if ($newFile) {
394
-					// rename to the name of the target file, could be an office file without extension
395
-					$file->setTitle($newFile->getTitle());
396
-				} else {
397
-					$file->setTitle(basename(($path2)));
398
-				}
399
-			} else {
400
-				// Change file parent
401
-				$parentFolder2 = $this->getDriveFile(dirname($path2));
402
-				if ($parentFolder2) {
403
-					$parent = new \Google_Service_Drive_ParentReference();
404
-					$parent->setId($parentFolder2->getId());
405
-					$file->setParents(array($parent));
406
-				} else {
407
-					return false;
408
-				}
409
-			}
410
-			// We need to get the object for the existing file with the same
411
-			// name (if there is one) before we do the patch. If oldfile
412
-			// exists and is a directory we have to delete it before we
413
-			// do the rename too.
414
-			$oldfile = $this->getDriveFile($path2);
415
-			if ($oldfile && $this->is_dir($path2)) {
416
-				$this->rmdir($path2);
417
-				$oldfile = false;
418
-			}
419
-			$result = $this->service->files->patch($file->getId(), $file);
420
-			if ($result) {
421
-				$this->setDriveFile($path1, false);
422
-				$this->setDriveFile($path2, $result);
423
-				if ($oldfile && $newFile) {
424
-					// only delete if they have a different id (same id can happen for part files)
425
-					if ($newFile->getId() !== $oldfile->getId()) {
426
-						$this->service->files->delete($oldfile->getId());
427
-					}
428
-				}
429
-			}
430
-			return (bool)$result;
431
-		} else {
432
-			return false;
433
-		}
434
-	}
435
-
436
-	public function fopen($path, $mode) {
437
-		$pos = strrpos($path, '.');
438
-		if ($pos !== false) {
439
-			$ext = substr($path, $pos);
440
-		} else {
441
-			$ext = '';
442
-		}
443
-		switch ($mode) {
444
-			case 'r':
445
-			case 'rb':
446
-				$file = $this->getDriveFile($path);
447
-				if ($file) {
448
-					$exportLinks = $file->getExportLinks();
449
-					$mimetype = $this->getMimeType($path);
450
-					$downloadUrl = null;
451
-					if ($exportLinks && isset($exportLinks[$mimetype])) {
452
-						$downloadUrl = $exportLinks[$mimetype];
453
-					} else {
454
-						$downloadUrl = $file->getDownloadUrl();
455
-					}
456
-					if (isset($downloadUrl)) {
457
-						$request = new \Google_Http_Request($downloadUrl, 'GET', null, null);
458
-						$httpRequest = $this->client->getAuth()->sign($request);
459
-						// the library's service doesn't support streaming, so we use Guzzle instead
460
-						$client = \OC::$server->getHTTPClientService()->newClient();
461
-						try {
462
-							$response = $client->get($downloadUrl, [
463
-								'headers' => $httpRequest->getRequestHeaders(),
464
-								'stream' => true,
465
-								'verify' => realpath(__DIR__ . '/../../../3rdparty/google-api-php-client/src/Google/IO/cacerts.pem'),
466
-							]);
467
-						} catch (RequestException $e) {
468
-							if(!is_null($e->getResponse())) {
469
-								if ($e->getResponse()->getStatusCode() === 404) {
470
-									return false;
471
-								} else {
472
-									throw $e;
473
-								}
474
-							} else {
475
-								throw $e;
476
-							}
477
-						}
478
-
479
-						$handle = $response->getBody();
480
-						return RetryWrapper::wrap($handle);
481
-					}
482
-				}
483
-				return false;
484
-			case 'w':
485
-			case 'wb':
486
-			case 'a':
487
-			case 'ab':
488
-			case 'r+':
489
-			case 'w+':
490
-			case 'wb+':
491
-			case 'a+':
492
-			case 'x':
493
-			case 'x+':
494
-			case 'c':
495
-			case 'c+':
496
-				$tmpFile = \OCP\Files::tmpFile($ext);
497
-				if ($this->file_exists($path)) {
498
-					$source = $this->fopen($path, 'rb');
499
-					file_put_contents($tmpFile, $source);
500
-				}
501
-				$handle = fopen($tmpFile, $mode);
502
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
503
-					$this->writeBack($tmpFile, $path);
504
-				});
505
-		}
506
-	}
507
-
508
-	public function writeBack($tmpFile, $path) {
509
-		$parentFolder = $this->getDriveFile(dirname($path));
510
-		if ($parentFolder) {
511
-			$mimetype = \OC::$server->getMimeTypeDetector()->detect($tmpFile);
512
-			$params = array(
513
-				'mimeType' => $mimetype,
514
-				'uploadType' => 'media'
515
-			);
516
-			$result = false;
517
-
518
-			$chunkSizeBytes = 10 * 1024 * 1024;
519
-
520
-			$useChunking = false;
521
-			$size = filesize($tmpFile);
522
-			if ($size > $chunkSizeBytes) {
523
-				$useChunking = true;
524
-			} else {
525
-				$params['data'] = file_get_contents($tmpFile);
526
-			}
527
-
528
-			if ($this->file_exists($path)) {
529
-				$file = $this->getDriveFile($path);
530
-				$this->client->setDefer($useChunking);
531
-				$request = $this->service->files->update($file->getId(), $file, $params);
532
-			} else {
533
-				$file = new \Google_Service_Drive_DriveFile();
534
-				$file->setTitle(basename($path));
535
-				$file->setMimeType($mimetype);
536
-				$parent = new \Google_Service_Drive_ParentReference();
537
-				$parent->setId($parentFolder->getId());
538
-				$file->setParents(array($parent));
539
-				$this->client->setDefer($useChunking);
540
-				$request = $this->service->files->insert($file, $params);
541
-			}
542
-
543
-			if ($useChunking) {
544
-				// Create a media file upload to represent our upload process.
545
-				$media = new \Google_Http_MediaFileUpload(
546
-					$this->client,
547
-					$request,
548
-					'text/plain',
549
-					null,
550
-					true,
551
-					$chunkSizeBytes
552
-				);
553
-				$media->setFileSize($size);
554
-
555
-				// Upload the various chunks. $status will be false until the process is
556
-				// complete.
557
-				$status = false;
558
-				$handle = fopen($tmpFile, 'rb');
559
-				while (!$status && !feof($handle)) {
560
-					$chunk = fread($handle, $chunkSizeBytes);
561
-					$status = $media->nextChunk($chunk);
562
-				}
563
-
564
-				// The final value of $status will be the data from the API for the object
565
-				// that has been uploaded.
566
-				$result = false;
567
-				if ($status !== false) {
568
-					$result = $status;
569
-				}
570
-
571
-				fclose($handle);
572
-			} else {
573
-				$result = $request;
574
-			}
575
-
576
-			// Reset to the client to execute requests immediately in the future.
577
-			$this->client->setDefer(false);
578
-
579
-			if ($result) {
580
-				$this->setDriveFile($path, $result);
581
-			}
582
-		}
583
-	}
584
-
585
-	public function getMimeType($path) {
586
-		$file = $this->getDriveFile($path);
587
-		if ($file) {
588
-			$mimetype = $file->getMimeType();
589
-			// Convert Google Doc mimetypes, choosing Open Document formats for download
590
-			if ($mimetype === self::FOLDER) {
591
-				return 'httpd/unix-directory';
592
-			} else if ($mimetype === self::DOCUMENT) {
593
-				return 'application/vnd.oasis.opendocument.text';
594
-			} else if ($mimetype === self::SPREADSHEET) {
595
-				return 'application/x-vnd.oasis.opendocument.spreadsheet';
596
-			} else if ($mimetype === self::DRAWING) {
597
-				return 'image/jpeg';
598
-			} else if ($mimetype === self::PRESENTATION) {
599
-				// Download as .odp is not available
600
-				return 'application/pdf';
601
-			} else {
602
-				// use extension-based detection, could be an encrypted file
603
-				return parent::getMimeType($path);
604
-			}
605
-		} else {
606
-			return false;
607
-		}
608
-	}
609
-
610
-	public function free_space($path) {
611
-		$about = $this->service->about->get();
612
-		return $about->getQuotaBytesTotal() - $about->getQuotaBytesUsed();
613
-	}
614
-
615
-	public function touch($path, $mtime = null) {
616
-		$file = $this->getDriveFile($path);
617
-		$result = false;
618
-		if ($file) {
619
-			if (isset($mtime)) {
620
-				// This is just RFC3339, but frustratingly, GDrive's API *requires*
621
-				// the fractions portion be present, while no handy PHP constant
622
-				// for RFC3339 or ISO8601 includes it. So we do it ourselves.
623
-				$file->setModifiedDate(date('Y-m-d\TH:i:s.uP', $mtime));
624
-				$result = $this->service->files->patch($file->getId(), $file, array(
625
-					'setModifiedDate' => true,
626
-				));
627
-			} else {
628
-				$result = $this->service->files->touch($file->getId());
629
-			}
630
-		} else {
631
-			$parentFolder = $this->getDriveFile(dirname($path));
632
-			if ($parentFolder) {
633
-				$file = new \Google_Service_Drive_DriveFile();
634
-				$file->setTitle(basename($path));
635
-				$parent = new \Google_Service_Drive_ParentReference();
636
-				$parent->setId($parentFolder->getId());
637
-				$file->setParents(array($parent));
638
-				$result = $this->service->files->insert($file);
639
-			}
640
-		}
641
-		if ($result) {
642
-			$this->setDriveFile($path, $result);
643
-		}
644
-		return (bool)$result;
645
-	}
646
-
647
-	public function test() {
648
-		if ($this->free_space('')) {
649
-			return true;
650
-		}
651
-		return false;
652
-	}
653
-
654
-	public function hasUpdated($path, $time) {
655
-		$appConfig = \OC::$server->getAppConfig();
656
-		if ($this->is_file($path)) {
657
-			return parent::hasUpdated($path, $time);
658
-		} else {
659
-			// Google Drive doesn't change modified times of folders when files inside are updated
660
-			// Instead we use the Changes API to see if folders have been updated, and it's a pain
661
-			$folder = $this->getDriveFile($path);
662
-			if ($folder) {
663
-				$result = false;
664
-				$folderId = $folder->getId();
665
-				$startChangeId = $appConfig->getValue('files_external', $this->getId().'cId');
666
-				$params = array(
667
-					'includeDeleted' => true,
668
-					'includeSubscribed' => true,
669
-				);
670
-				if (isset($startChangeId)) {
671
-					$startChangeId = (int)$startChangeId;
672
-					$largestChangeId = $startChangeId;
673
-					$params['startChangeId'] = $startChangeId + 1;
674
-				} else {
675
-					$largestChangeId = 0;
676
-				}
677
-				$pageToken = true;
678
-				while ($pageToken) {
679
-					if ($pageToken !== true) {
680
-						$params['pageToken'] = $pageToken;
681
-					}
682
-					$changes = $this->service->changes->listChanges($params);
683
-					if ($largestChangeId === 0 || $largestChangeId === $startChangeId) {
684
-						$largestChangeId = $changes->getLargestChangeId();
685
-					}
686
-					if (isset($startChangeId)) {
687
-						// Check if a file in this folder has been updated
688
-						// There is no way to filter by folder at the API level...
689
-						foreach ($changes->getItems() as $change) {
690
-							$file = $change->getFile();
691
-							if ($file) {
692
-								foreach ($file->getParents() as $parent) {
693
-									if ($parent->getId() === $folderId) {
694
-										$result = true;
695
-									// Check if there are changes in different folders
696
-									} else if ($change->getId() <= $largestChangeId) {
697
-										// Decrement id so this change is fetched when called again
698
-										$largestChangeId = $change->getId();
699
-										$largestChangeId--;
700
-									}
701
-								}
702
-							}
703
-						}
704
-						$pageToken = $changes->getNextPageToken();
705
-					} else {
706
-						// Assuming the initial scan just occurred and changes are negligible
707
-						break;
708
-					}
709
-				}
710
-				$appConfig->setValue('files_external', $this->getId().'cId', $largestChangeId);
711
-				return $result;
712
-			}
713
-		}
714
-		return false;
715
-	}
716
-
717
-	/**
718
-	 * check if curl is installed
719
-	 */
720
-	public static function checkDependencies() {
721
-		return true;
722
-	}
49
+    private $client;
50
+    private $id;
51
+    private $service;
52
+    private $driveFiles;
53
+
54
+    // Google Doc mimetypes
55
+    const FOLDER = 'application/vnd.google-apps.folder';
56
+    const DOCUMENT = 'application/vnd.google-apps.document';
57
+    const SPREADSHEET = 'application/vnd.google-apps.spreadsheet';
58
+    const DRAWING = 'application/vnd.google-apps.drawing';
59
+    const PRESENTATION = 'application/vnd.google-apps.presentation';
60
+    const MAP = 'application/vnd.google-apps.map';
61
+
62
+    public function __construct($params) {
63
+        if (isset($params['configured']) && $params['configured'] === 'true'
64
+            && isset($params['client_id']) && isset($params['client_secret'])
65
+            && isset($params['token'])
66
+        ) {
67
+            $this->client = new \Google_Client();
68
+            $this->client->setClientId($params['client_id']);
69
+            $this->client->setClientSecret($params['client_secret']);
70
+            $this->client->setScopes(array('https://www.googleapis.com/auth/drive'));
71
+            $this->client->setAccessToken($params['token']);
72
+            // if curl isn't available we're likely to run into
73
+            // https://github.com/google/google-api-php-client/issues/59
74
+            // - disable gzip to avoid it.
75
+            if (!function_exists('curl_version') || !function_exists('curl_exec')) {
76
+                $this->client->setClassConfig("Google_Http_Request", "disable_gzip", true);
77
+            }
78
+            // note: API connection is lazy
79
+            $this->service = new \Google_Service_Drive($this->client);
80
+            $token = json_decode($params['token'], true);
81
+            $this->id = 'google::'.substr($params['client_id'], 0, 30).$token['created'];
82
+        } else {
83
+            throw new \Exception('Creating Google storage failed');
84
+        }
85
+    }
86
+
87
+    public function getId() {
88
+        return $this->id;
89
+    }
90
+
91
+    /**
92
+     * Get the Google_Service_Drive_DriveFile object for the specified path.
93
+     * Returns false on failure.
94
+     * @param string $path
95
+     * @return \Google_Service_Drive_DriveFile|false
96
+     */
97
+    private function getDriveFile($path) {
98
+        // Remove leading and trailing slashes
99
+        $path = trim($path, '/');
100
+        if ($path === '.') {
101
+            $path = '';
102
+        }
103
+        if (isset($this->driveFiles[$path])) {
104
+            return $this->driveFiles[$path];
105
+        } else if ($path === '') {
106
+            $root = $this->service->files->get('root');
107
+            $this->driveFiles[$path] = $root;
108
+            return $root;
109
+        } else {
110
+            // Google Drive SDK does not have methods for retrieving files by path
111
+            // Instead we must find the id of the parent folder of the file
112
+            $parentId = $this->getDriveFile('')->getId();
113
+            $folderNames = explode('/', $path);
114
+            $path = '';
115
+            // Loop through each folder of this path to get to the file
116
+            foreach ($folderNames as $name) {
117
+                // Reconstruct path from beginning
118
+                if ($path === '') {
119
+                    $path .= $name;
120
+                } else {
121
+                    $path .= '/'.$name;
122
+                }
123
+                if (isset($this->driveFiles[$path])) {
124
+                    $parentId = $this->driveFiles[$path]->getId();
125
+                } else {
126
+                    $q = "title='" . str_replace("'","\\'", $name) . "' and '" . str_replace("'","\\'", $parentId) . "' in parents and trashed = false";
127
+                    $result = $this->service->files->listFiles(array('q' => $q))->getItems();
128
+                    if (!empty($result)) {
129
+                        // Google Drive allows files with the same name, Nextcloud doesn't
130
+                        if (count($result) > 1) {
131
+                            $this->onDuplicateFileDetected($path);
132
+                            return false;
133
+                        } else {
134
+                            $file = current($result);
135
+                            $this->driveFiles[$path] = $file;
136
+                            $parentId = $file->getId();
137
+                        }
138
+                    } else {
139
+                        // Google Docs have no extension in their title, so try without extension
140
+                        $pos = strrpos($path, '.');
141
+                        if ($pos !== false) {
142
+                            $pathWithoutExt = substr($path, 0, $pos);
143
+                            $file = $this->getDriveFile($pathWithoutExt);
144
+                            if ($file && $this->isGoogleDocFile($file)) {
145
+                                // Switch cached Google_Service_Drive_DriveFile to the correct index
146
+                                unset($this->driveFiles[$pathWithoutExt]);
147
+                                $this->driveFiles[$path] = $file;
148
+                                $parentId = $file->getId();
149
+                            } else {
150
+                                return false;
151
+                            }
152
+                        } else {
153
+                            return false;
154
+                        }
155
+                    }
156
+                }
157
+            }
158
+            return $this->driveFiles[$path];
159
+        }
160
+    }
161
+
162
+    /**
163
+     * Set the Google_Service_Drive_DriveFile object in the cache
164
+     * @param string $path
165
+     * @param \Google_Service_Drive_DriveFile|false $file
166
+     */
167
+    private function setDriveFile($path, $file) {
168
+        $path = trim($path, '/');
169
+        $this->driveFiles[$path] = $file;
170
+        if ($file === false) {
171
+            // Remove all children
172
+            $len = strlen($path);
173
+            foreach ($this->driveFiles as $key => $file) {
174
+                if (substr($key, 0, $len) === $path) {
175
+                    unset($this->driveFiles[$key]);
176
+                }
177
+            }
178
+        }
179
+    }
180
+
181
+    /**
182
+     * Write a log message to inform about duplicate file names
183
+     * @param string $path
184
+     */
185
+    private function onDuplicateFileDetected($path) {
186
+        $about = $this->service->about->get();
187
+        $user = $about->getName();
188
+        \OCP\Util::writeLog('files_external',
189
+            'Ignoring duplicate file name: '.$path.' on Google Drive for Google user: '.$user,
190
+            \OCP\Util::INFO
191
+        );
192
+    }
193
+
194
+    /**
195
+     * Generate file extension for a Google Doc, choosing Open Document formats for download
196
+     * @param string $mimetype
197
+     * @return string
198
+     */
199
+    private function getGoogleDocExtension($mimetype) {
200
+        if ($mimetype === self::DOCUMENT) {
201
+            return 'odt';
202
+        } else if ($mimetype === self::SPREADSHEET) {
203
+            return 'ods';
204
+        } else if ($mimetype === self::DRAWING) {
205
+            return 'jpg';
206
+        } else if ($mimetype === self::PRESENTATION) {
207
+            // Download as .odp is not available
208
+            return 'pdf';
209
+        } else {
210
+            return '';
211
+        }
212
+    }
213
+
214
+    /**
215
+     * Returns whether the given drive file is a Google Doc file
216
+     *
217
+     * @param \Google_Service_Drive_DriveFile
218
+     *
219
+     * @return true if the file is a Google Doc file, false otherwise
220
+     */
221
+    private function isGoogleDocFile($file) {
222
+        return $this->getGoogleDocExtension($file->getMimeType()) !== '';
223
+    }
224
+
225
+    public function mkdir($path) {
226
+        if (!$this->is_dir($path)) {
227
+            $parentFolder = $this->getDriveFile(dirname($path));
228
+            if ($parentFolder) {
229
+                $folder = new \Google_Service_Drive_DriveFile();
230
+                $folder->setTitle(basename($path));
231
+                $folder->setMimeType(self::FOLDER);
232
+                $parent = new \Google_Service_Drive_ParentReference();
233
+                $parent->setId($parentFolder->getId());
234
+                $folder->setParents(array($parent));
235
+                $result = $this->service->files->insert($folder);
236
+                if ($result) {
237
+                    $this->setDriveFile($path, $result);
238
+                }
239
+                return (bool)$result;
240
+            }
241
+        }
242
+        return false;
243
+    }
244
+
245
+    public function rmdir($path) {
246
+        if (!$this->isDeletable($path)) {
247
+            return false;
248
+        }
249
+        if (trim($path, '/') === '') {
250
+            $dir = $this->opendir($path);
251
+            if(is_resource($dir)) {
252
+                while (($file = readdir($dir)) !== false) {
253
+                    if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
254
+                        if (!$this->unlink($path.'/'.$file)) {
255
+                            return false;
256
+                        }
257
+                    }
258
+                }
259
+                closedir($dir);
260
+            }
261
+            $this->driveFiles = array();
262
+            return true;
263
+        } else {
264
+            return $this->unlink($path);
265
+        }
266
+    }
267
+
268
+    public function opendir($path) {
269
+        $folder = $this->getDriveFile($path);
270
+        if ($folder) {
271
+            $files = array();
272
+            $duplicates = array();
273
+            $pageToken = true;
274
+            while ($pageToken) {
275
+                $params = array();
276
+                if ($pageToken !== true) {
277
+                    $params['pageToken'] = $pageToken;
278
+                }
279
+                $params['q'] = "'" . str_replace("'","\\'", $folder->getId()) . "' in parents and trashed = false";
280
+                $children = $this->service->files->listFiles($params);
281
+                foreach ($children->getItems() as $child) {
282
+                    $name = $child->getTitle();
283
+                    // Check if this is a Google Doc i.e. no extension in name
284
+                    $extension = $child->getFileExtension();
285
+                    if (empty($extension)) {
286
+                        if ($child->getMimeType() === self::MAP) {
287
+                            continue; // No method known to transfer map files, ignore it
288
+                        } else if ($child->getMimeType() !== self::FOLDER) {
289
+                            $name .= '.'.$this->getGoogleDocExtension($child->getMimeType());
290
+                        }
291
+                    }
292
+                    if ($path === '') {
293
+                        $filepath = $name;
294
+                    } else {
295
+                        $filepath = $path.'/'.$name;
296
+                    }
297
+                    // Google Drive allows files with the same name, Nextcloud doesn't
298
+                    // Prevent opendir() from returning any duplicate files
299
+                    $key = array_search($name, $files);
300
+                    if ($key !== false || isset($duplicates[$filepath])) {
301
+                        if (!isset($duplicates[$filepath])) {
302
+                            $duplicates[$filepath] = true;
303
+                            $this->setDriveFile($filepath, false);
304
+                            unset($files[$key]);
305
+                            $this->onDuplicateFileDetected($filepath);
306
+                        }
307
+                    } else {
308
+                        // Cache the Google_Service_Drive_DriveFile for future use
309
+                        $this->setDriveFile($filepath, $child);
310
+                        $files[] = $name;
311
+                    }
312
+                }
313
+                $pageToken = $children->getNextPageToken();
314
+            }
315
+            return IteratorDirectory::wrap($files);
316
+        } else {
317
+            return false;
318
+        }
319
+    }
320
+
321
+    public function stat($path) {
322
+        $file = $this->getDriveFile($path);
323
+        if ($file) {
324
+            $stat = array();
325
+            if ($this->filetype($path) === 'dir') {
326
+                $stat['size'] = 0;
327
+            } else {
328
+                // Check if this is a Google Doc
329
+                if ($this->isGoogleDocFile($file)) {
330
+                    // Return unknown file size
331
+                    $stat['size'] = \OCP\Files\FileInfo::SPACE_UNKNOWN;
332
+                } else {
333
+                    $stat['size'] = $file->getFileSize();
334
+                }
335
+            }
336
+            $stat['atime'] = strtotime($file->getLastViewedByMeDate());
337
+            $stat['mtime'] = strtotime($file->getModifiedDate());
338
+            $stat['ctime'] = strtotime($file->getCreatedDate());
339
+            return $stat;
340
+        } else {
341
+            return false;
342
+        }
343
+    }
344
+
345
+    public function filetype($path) {
346
+        if ($path === '') {
347
+            return 'dir';
348
+        } else {
349
+            $file = $this->getDriveFile($path);
350
+            if ($file) {
351
+                if ($file->getMimeType() === self::FOLDER) {
352
+                    return 'dir';
353
+                } else {
354
+                    return 'file';
355
+                }
356
+            } else {
357
+                return false;
358
+            }
359
+        }
360
+    }
361
+
362
+    public function isUpdatable($path) {
363
+        $file = $this->getDriveFile($path);
364
+        if ($file) {
365
+            return $file->getEditable();
366
+        } else {
367
+            return false;
368
+        }
369
+    }
370
+
371
+    public function file_exists($path) {
372
+        return (bool)$this->getDriveFile($path);
373
+    }
374
+
375
+    public function unlink($path) {
376
+        $file = $this->getDriveFile($path);
377
+        if ($file) {
378
+            $result = $this->service->files->trash($file->getId());
379
+            if ($result) {
380
+                $this->setDriveFile($path, false);
381
+            }
382
+            return (bool)$result;
383
+        } else {
384
+            return false;
385
+        }
386
+    }
387
+
388
+    public function rename($path1, $path2) {
389
+        $file = $this->getDriveFile($path1);
390
+        if ($file) {
391
+            $newFile = $this->getDriveFile($path2);
392
+            if (dirname($path1) === dirname($path2)) {
393
+                if ($newFile) {
394
+                    // rename to the name of the target file, could be an office file without extension
395
+                    $file->setTitle($newFile->getTitle());
396
+                } else {
397
+                    $file->setTitle(basename(($path2)));
398
+                }
399
+            } else {
400
+                // Change file parent
401
+                $parentFolder2 = $this->getDriveFile(dirname($path2));
402
+                if ($parentFolder2) {
403
+                    $parent = new \Google_Service_Drive_ParentReference();
404
+                    $parent->setId($parentFolder2->getId());
405
+                    $file->setParents(array($parent));
406
+                } else {
407
+                    return false;
408
+                }
409
+            }
410
+            // We need to get the object for the existing file with the same
411
+            // name (if there is one) before we do the patch. If oldfile
412
+            // exists and is a directory we have to delete it before we
413
+            // do the rename too.
414
+            $oldfile = $this->getDriveFile($path2);
415
+            if ($oldfile && $this->is_dir($path2)) {
416
+                $this->rmdir($path2);
417
+                $oldfile = false;
418
+            }
419
+            $result = $this->service->files->patch($file->getId(), $file);
420
+            if ($result) {
421
+                $this->setDriveFile($path1, false);
422
+                $this->setDriveFile($path2, $result);
423
+                if ($oldfile && $newFile) {
424
+                    // only delete if they have a different id (same id can happen for part files)
425
+                    if ($newFile->getId() !== $oldfile->getId()) {
426
+                        $this->service->files->delete($oldfile->getId());
427
+                    }
428
+                }
429
+            }
430
+            return (bool)$result;
431
+        } else {
432
+            return false;
433
+        }
434
+    }
435
+
436
+    public function fopen($path, $mode) {
437
+        $pos = strrpos($path, '.');
438
+        if ($pos !== false) {
439
+            $ext = substr($path, $pos);
440
+        } else {
441
+            $ext = '';
442
+        }
443
+        switch ($mode) {
444
+            case 'r':
445
+            case 'rb':
446
+                $file = $this->getDriveFile($path);
447
+                if ($file) {
448
+                    $exportLinks = $file->getExportLinks();
449
+                    $mimetype = $this->getMimeType($path);
450
+                    $downloadUrl = null;
451
+                    if ($exportLinks && isset($exportLinks[$mimetype])) {
452
+                        $downloadUrl = $exportLinks[$mimetype];
453
+                    } else {
454
+                        $downloadUrl = $file->getDownloadUrl();
455
+                    }
456
+                    if (isset($downloadUrl)) {
457
+                        $request = new \Google_Http_Request($downloadUrl, 'GET', null, null);
458
+                        $httpRequest = $this->client->getAuth()->sign($request);
459
+                        // the library's service doesn't support streaming, so we use Guzzle instead
460
+                        $client = \OC::$server->getHTTPClientService()->newClient();
461
+                        try {
462
+                            $response = $client->get($downloadUrl, [
463
+                                'headers' => $httpRequest->getRequestHeaders(),
464
+                                'stream' => true,
465
+                                'verify' => realpath(__DIR__ . '/../../../3rdparty/google-api-php-client/src/Google/IO/cacerts.pem'),
466
+                            ]);
467
+                        } catch (RequestException $e) {
468
+                            if(!is_null($e->getResponse())) {
469
+                                if ($e->getResponse()->getStatusCode() === 404) {
470
+                                    return false;
471
+                                } else {
472
+                                    throw $e;
473
+                                }
474
+                            } else {
475
+                                throw $e;
476
+                            }
477
+                        }
478
+
479
+                        $handle = $response->getBody();
480
+                        return RetryWrapper::wrap($handle);
481
+                    }
482
+                }
483
+                return false;
484
+            case 'w':
485
+            case 'wb':
486
+            case 'a':
487
+            case 'ab':
488
+            case 'r+':
489
+            case 'w+':
490
+            case 'wb+':
491
+            case 'a+':
492
+            case 'x':
493
+            case 'x+':
494
+            case 'c':
495
+            case 'c+':
496
+                $tmpFile = \OCP\Files::tmpFile($ext);
497
+                if ($this->file_exists($path)) {
498
+                    $source = $this->fopen($path, 'rb');
499
+                    file_put_contents($tmpFile, $source);
500
+                }
501
+                $handle = fopen($tmpFile, $mode);
502
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
503
+                    $this->writeBack($tmpFile, $path);
504
+                });
505
+        }
506
+    }
507
+
508
+    public function writeBack($tmpFile, $path) {
509
+        $parentFolder = $this->getDriveFile(dirname($path));
510
+        if ($parentFolder) {
511
+            $mimetype = \OC::$server->getMimeTypeDetector()->detect($tmpFile);
512
+            $params = array(
513
+                'mimeType' => $mimetype,
514
+                'uploadType' => 'media'
515
+            );
516
+            $result = false;
517
+
518
+            $chunkSizeBytes = 10 * 1024 * 1024;
519
+
520
+            $useChunking = false;
521
+            $size = filesize($tmpFile);
522
+            if ($size > $chunkSizeBytes) {
523
+                $useChunking = true;
524
+            } else {
525
+                $params['data'] = file_get_contents($tmpFile);
526
+            }
527
+
528
+            if ($this->file_exists($path)) {
529
+                $file = $this->getDriveFile($path);
530
+                $this->client->setDefer($useChunking);
531
+                $request = $this->service->files->update($file->getId(), $file, $params);
532
+            } else {
533
+                $file = new \Google_Service_Drive_DriveFile();
534
+                $file->setTitle(basename($path));
535
+                $file->setMimeType($mimetype);
536
+                $parent = new \Google_Service_Drive_ParentReference();
537
+                $parent->setId($parentFolder->getId());
538
+                $file->setParents(array($parent));
539
+                $this->client->setDefer($useChunking);
540
+                $request = $this->service->files->insert($file, $params);
541
+            }
542
+
543
+            if ($useChunking) {
544
+                // Create a media file upload to represent our upload process.
545
+                $media = new \Google_Http_MediaFileUpload(
546
+                    $this->client,
547
+                    $request,
548
+                    'text/plain',
549
+                    null,
550
+                    true,
551
+                    $chunkSizeBytes
552
+                );
553
+                $media->setFileSize($size);
554
+
555
+                // Upload the various chunks. $status will be false until the process is
556
+                // complete.
557
+                $status = false;
558
+                $handle = fopen($tmpFile, 'rb');
559
+                while (!$status && !feof($handle)) {
560
+                    $chunk = fread($handle, $chunkSizeBytes);
561
+                    $status = $media->nextChunk($chunk);
562
+                }
563
+
564
+                // The final value of $status will be the data from the API for the object
565
+                // that has been uploaded.
566
+                $result = false;
567
+                if ($status !== false) {
568
+                    $result = $status;
569
+                }
570
+
571
+                fclose($handle);
572
+            } else {
573
+                $result = $request;
574
+            }
575
+
576
+            // Reset to the client to execute requests immediately in the future.
577
+            $this->client->setDefer(false);
578
+
579
+            if ($result) {
580
+                $this->setDriveFile($path, $result);
581
+            }
582
+        }
583
+    }
584
+
585
+    public function getMimeType($path) {
586
+        $file = $this->getDriveFile($path);
587
+        if ($file) {
588
+            $mimetype = $file->getMimeType();
589
+            // Convert Google Doc mimetypes, choosing Open Document formats for download
590
+            if ($mimetype === self::FOLDER) {
591
+                return 'httpd/unix-directory';
592
+            } else if ($mimetype === self::DOCUMENT) {
593
+                return 'application/vnd.oasis.opendocument.text';
594
+            } else if ($mimetype === self::SPREADSHEET) {
595
+                return 'application/x-vnd.oasis.opendocument.spreadsheet';
596
+            } else if ($mimetype === self::DRAWING) {
597
+                return 'image/jpeg';
598
+            } else if ($mimetype === self::PRESENTATION) {
599
+                // Download as .odp is not available
600
+                return 'application/pdf';
601
+            } else {
602
+                // use extension-based detection, could be an encrypted file
603
+                return parent::getMimeType($path);
604
+            }
605
+        } else {
606
+            return false;
607
+        }
608
+    }
609
+
610
+    public function free_space($path) {
611
+        $about = $this->service->about->get();
612
+        return $about->getQuotaBytesTotal() - $about->getQuotaBytesUsed();
613
+    }
614
+
615
+    public function touch($path, $mtime = null) {
616
+        $file = $this->getDriveFile($path);
617
+        $result = false;
618
+        if ($file) {
619
+            if (isset($mtime)) {
620
+                // This is just RFC3339, but frustratingly, GDrive's API *requires*
621
+                // the fractions portion be present, while no handy PHP constant
622
+                // for RFC3339 or ISO8601 includes it. So we do it ourselves.
623
+                $file->setModifiedDate(date('Y-m-d\TH:i:s.uP', $mtime));
624
+                $result = $this->service->files->patch($file->getId(), $file, array(
625
+                    'setModifiedDate' => true,
626
+                ));
627
+            } else {
628
+                $result = $this->service->files->touch($file->getId());
629
+            }
630
+        } else {
631
+            $parentFolder = $this->getDriveFile(dirname($path));
632
+            if ($parentFolder) {
633
+                $file = new \Google_Service_Drive_DriveFile();
634
+                $file->setTitle(basename($path));
635
+                $parent = new \Google_Service_Drive_ParentReference();
636
+                $parent->setId($parentFolder->getId());
637
+                $file->setParents(array($parent));
638
+                $result = $this->service->files->insert($file);
639
+            }
640
+        }
641
+        if ($result) {
642
+            $this->setDriveFile($path, $result);
643
+        }
644
+        return (bool)$result;
645
+    }
646
+
647
+    public function test() {
648
+        if ($this->free_space('')) {
649
+            return true;
650
+        }
651
+        return false;
652
+    }
653
+
654
+    public function hasUpdated($path, $time) {
655
+        $appConfig = \OC::$server->getAppConfig();
656
+        if ($this->is_file($path)) {
657
+            return parent::hasUpdated($path, $time);
658
+        } else {
659
+            // Google Drive doesn't change modified times of folders when files inside are updated
660
+            // Instead we use the Changes API to see if folders have been updated, and it's a pain
661
+            $folder = $this->getDriveFile($path);
662
+            if ($folder) {
663
+                $result = false;
664
+                $folderId = $folder->getId();
665
+                $startChangeId = $appConfig->getValue('files_external', $this->getId().'cId');
666
+                $params = array(
667
+                    'includeDeleted' => true,
668
+                    'includeSubscribed' => true,
669
+                );
670
+                if (isset($startChangeId)) {
671
+                    $startChangeId = (int)$startChangeId;
672
+                    $largestChangeId = $startChangeId;
673
+                    $params['startChangeId'] = $startChangeId + 1;
674
+                } else {
675
+                    $largestChangeId = 0;
676
+                }
677
+                $pageToken = true;
678
+                while ($pageToken) {
679
+                    if ($pageToken !== true) {
680
+                        $params['pageToken'] = $pageToken;
681
+                    }
682
+                    $changes = $this->service->changes->listChanges($params);
683
+                    if ($largestChangeId === 0 || $largestChangeId === $startChangeId) {
684
+                        $largestChangeId = $changes->getLargestChangeId();
685
+                    }
686
+                    if (isset($startChangeId)) {
687
+                        // Check if a file in this folder has been updated
688
+                        // There is no way to filter by folder at the API level...
689
+                        foreach ($changes->getItems() as $change) {
690
+                            $file = $change->getFile();
691
+                            if ($file) {
692
+                                foreach ($file->getParents() as $parent) {
693
+                                    if ($parent->getId() === $folderId) {
694
+                                        $result = true;
695
+                                    // Check if there are changes in different folders
696
+                                    } else if ($change->getId() <= $largestChangeId) {
697
+                                        // Decrement id so this change is fetched when called again
698
+                                        $largestChangeId = $change->getId();
699
+                                        $largestChangeId--;
700
+                                    }
701
+                                }
702
+                            }
703
+                        }
704
+                        $pageToken = $changes->getNextPageToken();
705
+                    } else {
706
+                        // Assuming the initial scan just occurred and changes are negligible
707
+                        break;
708
+                    }
709
+                }
710
+                $appConfig->setValue('files_external', $this->getId().'cId', $largestChangeId);
711
+                return $result;
712
+            }
713
+        }
714
+        return false;
715
+    }
716
+
717
+    /**
718
+     * check if curl is installed
719
+     */
720
+    public static function checkDependencies() {
721
+        return true;
722
+    }
723 723
 
724 724
 }
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/Crypt.php 1 patch
Indentation   +636 added lines, -636 removed lines patch added patch discarded remove patch
@@ -53,641 +53,641 @@
 block discarded – undo
53 53
  */
54 54
 class Crypt {
55 55
 
56
-	const DEFAULT_CIPHER = 'AES-256-CTR';
57
-	// default cipher from old Nextcloud versions
58
-	const LEGACY_CIPHER = 'AES-128-CFB';
59
-
60
-	// default key format, old Nextcloud version encrypted the private key directly
61
-	// with the user password
62
-	const LEGACY_KEY_FORMAT = 'password';
63
-
64
-	const HEADER_START = 'HBEGIN';
65
-	const HEADER_END = 'HEND';
66
-
67
-	/** @var ILogger */
68
-	private $logger;
69
-
70
-	/** @var string */
71
-	private $user;
72
-
73
-	/** @var IConfig */
74
-	private $config;
75
-
76
-	/** @var array */
77
-	private $supportedKeyFormats;
78
-
79
-	/** @var IL10N */
80
-	private $l;
81
-
82
-	/** @var array */
83
-	private $supportedCiphersAndKeySize = [
84
-		'AES-256-CTR' => 32,
85
-		'AES-128-CTR' => 16,
86
-		'AES-256-CFB' => 32,
87
-		'AES-128-CFB' => 16,
88
-	];
89
-
90
-	/**
91
-	 * @param ILogger $logger
92
-	 * @param IUserSession $userSession
93
-	 * @param IConfig $config
94
-	 * @param IL10N $l
95
-	 */
96
-	public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
97
-		$this->logger = $logger;
98
-		$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
99
-		$this->config = $config;
100
-		$this->l = $l;
101
-		$this->supportedKeyFormats = ['hash', 'password'];
102
-	}
103
-
104
-	/**
105
-	 * create new private/public key-pair for user
106
-	 *
107
-	 * @return array|bool
108
-	 */
109
-	public function createKeyPair() {
110
-
111
-		$log = $this->logger;
112
-		$res = $this->getOpenSSLPKey();
113
-
114
-		if (!$res) {
115
-			$log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
116
-				['app' => 'encryption']);
117
-
118
-			if (openssl_error_string()) {
119
-				$log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
120
-					['app' => 'encryption']);
121
-			}
122
-		} elseif (openssl_pkey_export($res,
123
-			$privateKey,
124
-			null,
125
-			$this->getOpenSSLConfig())) {
126
-			$keyDetails = openssl_pkey_get_details($res);
127
-			$publicKey = $keyDetails['key'];
128
-
129
-			return [
130
-				'publicKey' => $publicKey,
131
-				'privateKey' => $privateKey
132
-			];
133
-		}
134
-		$log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
135
-			['app' => 'encryption']);
136
-		if (openssl_error_string()) {
137
-			$log->error('Encryption Library:' . openssl_error_string(),
138
-				['app' => 'encryption']);
139
-		}
140
-
141
-		return false;
142
-	}
143
-
144
-	/**
145
-	 * Generates a new private key
146
-	 *
147
-	 * @return resource
148
-	 */
149
-	public function getOpenSSLPKey() {
150
-		$config = $this->getOpenSSLConfig();
151
-		return openssl_pkey_new($config);
152
-	}
153
-
154
-	/**
155
-	 * get openSSL Config
156
-	 *
157
-	 * @return array
158
-	 */
159
-	private function getOpenSSLConfig() {
160
-		$config = ['private_key_bits' => 4096];
161
-		$config = array_merge(
162
-			$config,
163
-			$this->config->getSystemValue('openssl', [])
164
-		);
165
-		return $config;
166
-	}
167
-
168
-	/**
169
-	 * @param string $plainContent
170
-	 * @param string $passPhrase
171
-	 * @param int $version
172
-	 * @param int $position
173
-	 * @return false|string
174
-	 * @throws EncryptionFailedException
175
-	 */
176
-	public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
177
-
178
-		if (!$plainContent) {
179
-			$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
180
-				['app' => 'encryption']);
181
-			return false;
182
-		}
183
-
184
-		$iv = $this->generateIv();
185
-
186
-		$encryptedContent = $this->encrypt($plainContent,
187
-			$iv,
188
-			$passPhrase,
189
-			$this->getCipher());
190
-
191
-		// Create a signature based on the key as well as the current version
192
-		$sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position);
193
-
194
-		// combine content to encrypt the IV identifier and actual IV
195
-		$catFile = $this->concatIV($encryptedContent, $iv);
196
-		$catFile = $this->concatSig($catFile, $sig);
197
-		$padded = $this->addPadding($catFile);
198
-
199
-		return $padded;
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
-		$signature = hash_hmac('sha256', $data, $passPhrase);
497
-		return $signature;
498
-	}
499
-
500
-
501
-	/**
502
-	 * remove padding
503
-	 *
504
-	 * @param string $padded
505
-	 * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
506
-	 * @return string|false
507
-	 */
508
-	private function removePadding($padded, $hasSignature = false) {
509
-		if ($hasSignature === false && substr($padded, -2) === 'xx') {
510
-			return substr($padded, 0, -2);
511
-		} elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
512
-			return substr($padded, 0, -3);
513
-		}
514
-		return false;
515
-	}
516
-
517
-	/**
518
-	 * split meta data from encrypted file
519
-	 * Note: for now, we assume that the meta data always start with the iv
520
-	 *       followed by the signature, if available
521
-	 *
522
-	 * @param string $catFile
523
-	 * @param string $cipher
524
-	 * @return array
525
-	 */
526
-	private function splitMetaData($catFile, $cipher) {
527
-		if ($this->hasSignature($catFile, $cipher)) {
528
-			$catFile = $this->removePadding($catFile, true);
529
-			$meta = substr($catFile, -93);
530
-			$iv = substr($meta, strlen('00iv00'), 16);
531
-			$sig = substr($meta, 22 + strlen('00sig00'));
532
-			$encrypted = substr($catFile, 0, -93);
533
-		} else {
534
-			$catFile = $this->removePadding($catFile);
535
-			$meta = substr($catFile, -22);
536
-			$iv = substr($meta, -16);
537
-			$sig = false;
538
-			$encrypted = substr($catFile, 0, -22);
539
-		}
540
-
541
-		return [
542
-			'encrypted' => $encrypted,
543
-			'iv' => $iv,
544
-			'signature' => $sig
545
-		];
546
-	}
547
-
548
-	/**
549
-	 * check if encrypted block is signed
550
-	 *
551
-	 * @param string $catFile
552
-	 * @param string $cipher
553
-	 * @return bool
554
-	 * @throws GenericEncryptionException
555
-	 */
556
-	private function hasSignature($catFile, $cipher) {
557
-		$meta = substr($catFile, -93);
558
-		$signaturePosition = strpos($meta, '00sig00');
559
-
560
-		// enforce signature for the new 'CTR' ciphers
561
-		if ($signaturePosition === false && strpos(strtolower($cipher), 'ctr') !== false) {
562
-			throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
563
-		}
564
-
565
-		return ($signaturePosition !== false);
566
-	}
567
-
568
-
569
-	/**
570
-	 * @param string $encryptedContent
571
-	 * @param string $iv
572
-	 * @param string $passPhrase
573
-	 * @param string $cipher
574
-	 * @return string
575
-	 * @throws DecryptionFailedException
576
-	 */
577
-	private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
578
-		$plainContent = openssl_decrypt($encryptedContent,
579
-			$cipher,
580
-			$passPhrase,
581
-			false,
582
-			$iv);
583
-
584
-		if ($plainContent) {
585
-			return $plainContent;
586
-		} else {
587
-			throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
588
-		}
589
-	}
590
-
591
-	/**
592
-	 * @param string $data
593
-	 * @return array
594
-	 */
595
-	protected function parseHeader($data) {
596
-		$result = [];
597
-
598
-		if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
599
-			$endAt = strpos($data, self::HEADER_END);
600
-			$header = substr($data, 0, $endAt + strlen(self::HEADER_END));
601
-
602
-			// +1 not to start with an ':' which would result in empty element at the beginning
603
-			$exploded = explode(':',
604
-				substr($header, strlen(self::HEADER_START) + 1));
605
-
606
-			$element = array_shift($exploded);
607
-
608
-			while ($element != self::HEADER_END) {
609
-				$result[$element] = array_shift($exploded);
610
-				$element = array_shift($exploded);
611
-			}
612
-		}
613
-
614
-		return $result;
615
-	}
616
-
617
-	/**
618
-	 * generate initialization vector
619
-	 *
620
-	 * @return string
621
-	 * @throws GenericEncryptionException
622
-	 */
623
-	private function generateIv() {
624
-		return random_bytes(16);
625
-	}
626
-
627
-	/**
628
-	 * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
629
-	 * as file key
630
-	 *
631
-	 * @return string
632
-	 * @throws \Exception
633
-	 */
634
-	public function generateFileKey() {
635
-		return random_bytes(32);
636
-	}
637
-
638
-	/**
639
-	 * @param $encKeyFile
640
-	 * @param $shareKey
641
-	 * @param $privateKey
642
-	 * @return string
643
-	 * @throws MultiKeyDecryptException
644
-	 */
645
-	public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
646
-		if (!$encKeyFile) {
647
-			throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
648
-		}
649
-
650
-		if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
651
-			return $plainContent;
652
-		} else {
653
-			throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
654
-		}
655
-	}
656
-
657
-	/**
658
-	 * @param string $plainContent
659
-	 * @param array $keyFiles
660
-	 * @return array
661
-	 * @throws MultiKeyEncryptException
662
-	 */
663
-	public function multiKeyEncrypt($plainContent, array $keyFiles) {
664
-		// openssl_seal returns false without errors if plaincontent is empty
665
-		// so trigger our own error
666
-		if (empty($plainContent)) {
667
-			throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
668
-		}
669
-
670
-		// Set empty vars to be set by openssl by reference
671
-		$sealed = '';
672
-		$shareKeys = [];
673
-		$mappedShareKeys = [];
674
-
675
-		if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
676
-			$i = 0;
677
-
678
-			// Ensure each shareKey is labelled with its corresponding key id
679
-			foreach ($keyFiles as $userId => $publicKey) {
680
-				$mappedShareKeys[$userId] = $shareKeys[$i];
681
-				$i++;
682
-			}
683
-
684
-			return [
685
-				'keys' => $mappedShareKeys,
686
-				'data' => $sealed
687
-			];
688
-		} else {
689
-			throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
690
-		}
691
-	}
56
+    const DEFAULT_CIPHER = 'AES-256-CTR';
57
+    // default cipher from old Nextcloud versions
58
+    const LEGACY_CIPHER = 'AES-128-CFB';
59
+
60
+    // default key format, old Nextcloud version encrypted the private key directly
61
+    // with the user password
62
+    const LEGACY_KEY_FORMAT = 'password';
63
+
64
+    const HEADER_START = 'HBEGIN';
65
+    const HEADER_END = 'HEND';
66
+
67
+    /** @var ILogger */
68
+    private $logger;
69
+
70
+    /** @var string */
71
+    private $user;
72
+
73
+    /** @var IConfig */
74
+    private $config;
75
+
76
+    /** @var array */
77
+    private $supportedKeyFormats;
78
+
79
+    /** @var IL10N */
80
+    private $l;
81
+
82
+    /** @var array */
83
+    private $supportedCiphersAndKeySize = [
84
+        'AES-256-CTR' => 32,
85
+        'AES-128-CTR' => 16,
86
+        'AES-256-CFB' => 32,
87
+        'AES-128-CFB' => 16,
88
+    ];
89
+
90
+    /**
91
+     * @param ILogger $logger
92
+     * @param IUserSession $userSession
93
+     * @param IConfig $config
94
+     * @param IL10N $l
95
+     */
96
+    public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
97
+        $this->logger = $logger;
98
+        $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
99
+        $this->config = $config;
100
+        $this->l = $l;
101
+        $this->supportedKeyFormats = ['hash', 'password'];
102
+    }
103
+
104
+    /**
105
+     * create new private/public key-pair for user
106
+     *
107
+     * @return array|bool
108
+     */
109
+    public function createKeyPair() {
110
+
111
+        $log = $this->logger;
112
+        $res = $this->getOpenSSLPKey();
113
+
114
+        if (!$res) {
115
+            $log->error("Encryption Library couldn't generate users key-pair for {$this->user}",
116
+                ['app' => 'encryption']);
117
+
118
+            if (openssl_error_string()) {
119
+                $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(),
120
+                    ['app' => 'encryption']);
121
+            }
122
+        } elseif (openssl_pkey_export($res,
123
+            $privateKey,
124
+            null,
125
+            $this->getOpenSSLConfig())) {
126
+            $keyDetails = openssl_pkey_get_details($res);
127
+            $publicKey = $keyDetails['key'];
128
+
129
+            return [
130
+                'publicKey' => $publicKey,
131
+                'privateKey' => $privateKey
132
+            ];
133
+        }
134
+        $log->error('Encryption library couldn\'t export users private key, please check your servers OpenSSL configuration.' . $this->user,
135
+            ['app' => 'encryption']);
136
+        if (openssl_error_string()) {
137
+            $log->error('Encryption Library:' . openssl_error_string(),
138
+                ['app' => 'encryption']);
139
+        }
140
+
141
+        return false;
142
+    }
143
+
144
+    /**
145
+     * Generates a new private key
146
+     *
147
+     * @return resource
148
+     */
149
+    public function getOpenSSLPKey() {
150
+        $config = $this->getOpenSSLConfig();
151
+        return openssl_pkey_new($config);
152
+    }
153
+
154
+    /**
155
+     * get openSSL Config
156
+     *
157
+     * @return array
158
+     */
159
+    private function getOpenSSLConfig() {
160
+        $config = ['private_key_bits' => 4096];
161
+        $config = array_merge(
162
+            $config,
163
+            $this->config->getSystemValue('openssl', [])
164
+        );
165
+        return $config;
166
+    }
167
+
168
+    /**
169
+     * @param string $plainContent
170
+     * @param string $passPhrase
171
+     * @param int $version
172
+     * @param int $position
173
+     * @return false|string
174
+     * @throws EncryptionFailedException
175
+     */
176
+    public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
177
+
178
+        if (!$plainContent) {
179
+            $this->logger->error('Encryption Library, symmetrical encryption failed no content given',
180
+                ['app' => 'encryption']);
181
+            return false;
182
+        }
183
+
184
+        $iv = $this->generateIv();
185
+
186
+        $encryptedContent = $this->encrypt($plainContent,
187
+            $iv,
188
+            $passPhrase,
189
+            $this->getCipher());
190
+
191
+        // Create a signature based on the key as well as the current version
192
+        $sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position);
193
+
194
+        // combine content to encrypt the IV identifier and actual IV
195
+        $catFile = $this->concatIV($encryptedContent, $iv);
196
+        $catFile = $this->concatSig($catFile, $sig);
197
+        $padded = $this->addPadding($catFile);
198
+
199
+        return $padded;
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
+        $signature = hash_hmac('sha256', $data, $passPhrase);
497
+        return $signature;
498
+    }
499
+
500
+
501
+    /**
502
+     * remove padding
503
+     *
504
+     * @param string $padded
505
+     * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
506
+     * @return string|false
507
+     */
508
+    private function removePadding($padded, $hasSignature = false) {
509
+        if ($hasSignature === false && substr($padded, -2) === 'xx') {
510
+            return substr($padded, 0, -2);
511
+        } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
512
+            return substr($padded, 0, -3);
513
+        }
514
+        return false;
515
+    }
516
+
517
+    /**
518
+     * split meta data from encrypted file
519
+     * Note: for now, we assume that the meta data always start with the iv
520
+     *       followed by the signature, if available
521
+     *
522
+     * @param string $catFile
523
+     * @param string $cipher
524
+     * @return array
525
+     */
526
+    private function splitMetaData($catFile, $cipher) {
527
+        if ($this->hasSignature($catFile, $cipher)) {
528
+            $catFile = $this->removePadding($catFile, true);
529
+            $meta = substr($catFile, -93);
530
+            $iv = substr($meta, strlen('00iv00'), 16);
531
+            $sig = substr($meta, 22 + strlen('00sig00'));
532
+            $encrypted = substr($catFile, 0, -93);
533
+        } else {
534
+            $catFile = $this->removePadding($catFile);
535
+            $meta = substr($catFile, -22);
536
+            $iv = substr($meta, -16);
537
+            $sig = false;
538
+            $encrypted = substr($catFile, 0, -22);
539
+        }
540
+
541
+        return [
542
+            'encrypted' => $encrypted,
543
+            'iv' => $iv,
544
+            'signature' => $sig
545
+        ];
546
+    }
547
+
548
+    /**
549
+     * check if encrypted block is signed
550
+     *
551
+     * @param string $catFile
552
+     * @param string $cipher
553
+     * @return bool
554
+     * @throws GenericEncryptionException
555
+     */
556
+    private function hasSignature($catFile, $cipher) {
557
+        $meta = substr($catFile, -93);
558
+        $signaturePosition = strpos($meta, '00sig00');
559
+
560
+        // enforce signature for the new 'CTR' ciphers
561
+        if ($signaturePosition === false && strpos(strtolower($cipher), 'ctr') !== false) {
562
+            throw new GenericEncryptionException('Missing Signature', $this->l->t('Missing Signature'));
563
+        }
564
+
565
+        return ($signaturePosition !== false);
566
+    }
567
+
568
+
569
+    /**
570
+     * @param string $encryptedContent
571
+     * @param string $iv
572
+     * @param string $passPhrase
573
+     * @param string $cipher
574
+     * @return string
575
+     * @throws DecryptionFailedException
576
+     */
577
+    private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
578
+        $plainContent = openssl_decrypt($encryptedContent,
579
+            $cipher,
580
+            $passPhrase,
581
+            false,
582
+            $iv);
583
+
584
+        if ($plainContent) {
585
+            return $plainContent;
586
+        } else {
587
+            throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
588
+        }
589
+    }
590
+
591
+    /**
592
+     * @param string $data
593
+     * @return array
594
+     */
595
+    protected function parseHeader($data) {
596
+        $result = [];
597
+
598
+        if (substr($data, 0, strlen(self::HEADER_START)) === self::HEADER_START) {
599
+            $endAt = strpos($data, self::HEADER_END);
600
+            $header = substr($data, 0, $endAt + strlen(self::HEADER_END));
601
+
602
+            // +1 not to start with an ':' which would result in empty element at the beginning
603
+            $exploded = explode(':',
604
+                substr($header, strlen(self::HEADER_START) + 1));
605
+
606
+            $element = array_shift($exploded);
607
+
608
+            while ($element != self::HEADER_END) {
609
+                $result[$element] = array_shift($exploded);
610
+                $element = array_shift($exploded);
611
+            }
612
+        }
613
+
614
+        return $result;
615
+    }
616
+
617
+    /**
618
+     * generate initialization vector
619
+     *
620
+     * @return string
621
+     * @throws GenericEncryptionException
622
+     */
623
+    private function generateIv() {
624
+        return random_bytes(16);
625
+    }
626
+
627
+    /**
628
+     * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
629
+     * as file key
630
+     *
631
+     * @return string
632
+     * @throws \Exception
633
+     */
634
+    public function generateFileKey() {
635
+        return random_bytes(32);
636
+    }
637
+
638
+    /**
639
+     * @param $encKeyFile
640
+     * @param $shareKey
641
+     * @param $privateKey
642
+     * @return string
643
+     * @throws MultiKeyDecryptException
644
+     */
645
+    public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
646
+        if (!$encKeyFile) {
647
+            throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
648
+        }
649
+
650
+        if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) {
651
+            return $plainContent;
652
+        } else {
653
+            throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string());
654
+        }
655
+    }
656
+
657
+    /**
658
+     * @param string $plainContent
659
+     * @param array $keyFiles
660
+     * @return array
661
+     * @throws MultiKeyEncryptException
662
+     */
663
+    public function multiKeyEncrypt($plainContent, array $keyFiles) {
664
+        // openssl_seal returns false without errors if plaincontent is empty
665
+        // so trigger our own error
666
+        if (empty($plainContent)) {
667
+            throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content');
668
+        }
669
+
670
+        // Set empty vars to be set by openssl by reference
671
+        $sealed = '';
672
+        $shareKeys = [];
673
+        $mappedShareKeys = [];
674
+
675
+        if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) {
676
+            $i = 0;
677
+
678
+            // Ensure each shareKey is labelled with its corresponding key id
679
+            foreach ($keyFiles as $userId => $publicKey) {
680
+                $mappedShareKeys[$userId] = $shareKeys[$i];
681
+                $i++;
682
+            }
683
+
684
+            return [
685
+                'keys' => $mappedShareKeys,
686
+                'data' => $sealed
687
+            ];
688
+        } else {
689
+            throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
690
+        }
691
+    }
692 692
 }
693 693
 
Please login to merge, or discard this patch.
apps/encryption/lib/Crypto/Encryption.php 1 patch
Indentation   +526 added lines, -526 removed lines patch added patch discarded remove patch
@@ -43,530 +43,530 @@
 block discarded – undo
43 43
 
44 44
 class Encryption implements IEncryptionModule {
45 45
 
46
-	const ID = 'OC_DEFAULT_MODULE';
47
-	const DISPLAY_NAME = 'Default encryption module';
48
-
49
-	/**
50
-	 * @var Crypt
51
-	 */
52
-	private $crypt;
53
-
54
-	/** @var string */
55
-	private $cipher;
56
-
57
-	/** @var string */
58
-	private $path;
59
-
60
-	/** @var string */
61
-	private $user;
62
-
63
-	/** @var string */
64
-	private $fileKey;
65
-
66
-	/** @var string */
67
-	private $writeCache;
68
-
69
-	/** @var KeyManager */
70
-	private $keyManager;
71
-
72
-	/** @var array */
73
-	private $accessList;
74
-
75
-	/** @var boolean */
76
-	private $isWriteOperation;
77
-
78
-	/** @var Util */
79
-	private $util;
80
-
81
-	/** @var  Session */
82
-	private $session;
83
-
84
-	/** @var  ILogger */
85
-	private $logger;
86
-
87
-	/** @var IL10N */
88
-	private $l;
89
-
90
-	/** @var EncryptAll */
91
-	private $encryptAll;
92
-
93
-	/** @var  bool */
94
-	private $useMasterPassword;
95
-
96
-	/** @var DecryptAll  */
97
-	private $decryptAll;
98
-
99
-	/** @var int unencrypted block size if block contains signature */
100
-	private $unencryptedBlockSizeSigned = 6072;
101
-
102
-	/** @var int unencrypted block size */
103
-	private $unencryptedBlockSize = 6126;
104
-
105
-	/** @var int Current version of the file */
106
-	private $version = 0;
107
-
108
-	/** @var array remember encryption signature version */
109
-	private static $rememberVersion = [];
110
-
111
-
112
-	/**
113
-	 *
114
-	 * @param Crypt $crypt
115
-	 * @param KeyManager $keyManager
116
-	 * @param Util $util
117
-	 * @param Session $session
118
-	 * @param EncryptAll $encryptAll
119
-	 * @param DecryptAll $decryptAll
120
-	 * @param ILogger $logger
121
-	 * @param IL10N $il10n
122
-	 */
123
-	public function __construct(Crypt $crypt,
124
-								KeyManager $keyManager,
125
-								Util $util,
126
-								Session $session,
127
-								EncryptAll $encryptAll,
128
-								DecryptAll $decryptAll,
129
-								ILogger $logger,
130
-								IL10N $il10n) {
131
-		$this->crypt = $crypt;
132
-		$this->keyManager = $keyManager;
133
-		$this->util = $util;
134
-		$this->session = $session;
135
-		$this->encryptAll = $encryptAll;
136
-		$this->decryptAll = $decryptAll;
137
-		$this->logger = $logger;
138
-		$this->l = $il10n;
139
-		$this->useMasterPassword = $util->isMasterKeyEnabled();
140
-	}
141
-
142
-	/**
143
-	 * @return string defining the technical unique id
144
-	 */
145
-	public function getId() {
146
-		return self::ID;
147
-	}
148
-
149
-	/**
150
-	 * In comparison to getKey() this function returns a human readable (maybe translated) name
151
-	 *
152
-	 * @return string
153
-	 */
154
-	public function getDisplayName() {
155
-		return self::DISPLAY_NAME;
156
-	}
157
-
158
-	/**
159
-	 * start receiving chunks from a file. This is the place where you can
160
-	 * perform some initial step before starting encrypting/decrypting the
161
-	 * chunks
162
-	 *
163
-	 * @param string $path to the file
164
-	 * @param string $user who read/write the file
165
-	 * @param string $mode php stream open mode
166
-	 * @param array $header contains the header data read from the file
167
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
168
-	 *
169
-	 * @return array $header contain data as key-value pairs which should be
170
-	 *                       written to the header, in case of a write operation
171
-	 *                       or if no additional data is needed return a empty array
172
-	 */
173
-	public function begin($path, $user, $mode, array $header, array $accessList) {
174
-		$this->path = $this->getPathToRealFile($path);
175
-		$this->accessList = $accessList;
176
-		$this->user = $user;
177
-		$this->isWriteOperation = false;
178
-		$this->writeCache = '';
179
-
180
-		if($this->session->isReady() === false) {
181
-			// if the master key is enabled we can initialize encryption
182
-			// with a empty password and user name
183
-			if ($this->util->isMasterKeyEnabled()) {
184
-				$this->keyManager->init('', '');
185
-			}
186
-		}
187
-
188
-		if ($this->session->decryptAllModeActivated()) {
189
-			$encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
190
-			$shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
191
-			$this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
192
-				$shareKey,
193
-				$this->session->getDecryptAllKey());
194
-		} else {
195
-			$this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
196
-		}
197
-
198
-		// always use the version from the original file, also part files
199
-		// need to have a correct version number if they get moved over to the
200
-		// final location
201
-		$this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
202
-
203
-		if (
204
-			$mode === 'w'
205
-			|| $mode === 'w+'
206
-			|| $mode === 'wb'
207
-			|| $mode === 'wb+'
208
-		) {
209
-			$this->isWriteOperation = true;
210
-			if (empty($this->fileKey)) {
211
-				$this->fileKey = $this->crypt->generateFileKey();
212
-			}
213
-		} else {
214
-			// if we read a part file we need to increase the version by 1
215
-			// because the version number was also increased by writing
216
-			// the part file
217
-			if(Scanner::isPartialFile($path)) {
218
-				$this->version = $this->version + 1;
219
-			}
220
-		}
221
-
222
-		if ($this->isWriteOperation) {
223
-			$this->cipher = $this->crypt->getCipher();
224
-		} elseif (isset($header['cipher'])) {
225
-			$this->cipher = $header['cipher'];
226
-		} else {
227
-			// if we read a file without a header we fall-back to the legacy cipher
228
-			// which was used in <=oC6
229
-			$this->cipher = $this->crypt->getLegacyCipher();
230
-		}
231
-
232
-		return array('cipher' => $this->cipher, 'signed' => 'true');
233
-	}
234
-
235
-	/**
236
-	 * last chunk received. This is the place where you can perform some final
237
-	 * operation and return some remaining data if something is left in your
238
-	 * buffer.
239
-	 *
240
-	 * @param string $path to the file
241
-	 * @param int $position
242
-	 * @return string remained data which should be written to the file in case
243
-	 *                of a write operation
244
-	 * @throws PublicKeyMissingException
245
-	 * @throws \Exception
246
-	 * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
247
-	 */
248
-	public function end($path, $position = 0) {
249
-		$result = '';
250
-		if ($this->isWriteOperation) {
251
-			$this->keyManager->setVersion($path, $this->version + 1, new View());
252
-			// in case of a part file we remember the new signature versions
253
-			// the version will be set later on update.
254
-			// This way we make sure that other apps listening to the pre-hooks
255
-			// still get the old version which should be the correct value for them
256
-			if (Scanner::isPartialFile($path)) {
257
-				self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
258
-			}
259
-			if (!empty($this->writeCache)) {
260
-				$result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
261
-				$this->writeCache = '';
262
-			}
263
-			$publicKeys = array();
264
-			if ($this->useMasterPassword === true) {
265
-				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
266
-			} else {
267
-				foreach ($this->accessList['users'] as $uid) {
268
-					try {
269
-						$publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
270
-					} catch (PublicKeyMissingException $e) {
271
-						$this->logger->warning(
272
-							'no public key found for user "{uid}", user will not be able to read the file',
273
-							['app' => 'encryption', 'uid' => $uid]
274
-						);
275
-						// if the public key of the owner is missing we should fail
276
-						if ($uid === $this->user) {
277
-							throw $e;
278
-						}
279
-					}
280
-				}
281
-			}
282
-
283
-			$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->user);
284
-			$encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
285
-			$this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
286
-		}
287
-		return $result;
288
-	}
289
-
290
-	/**
291
-	 * encrypt data
292
-	 *
293
-	 * @param string $data you want to encrypt
294
-	 * @param int $position
295
-	 * @return string encrypted data
296
-	 */
297
-	public function encrypt($data, $position = 0) {
298
-		// If extra data is left over from the last round, make sure it
299
-		// is integrated into the next block
300
-		if ($this->writeCache) {
301
-
302
-			// Concat writeCache to start of $data
303
-			$data = $this->writeCache . $data;
304
-
305
-			// Clear the write cache, ready for reuse - it has been
306
-			// flushed and its old contents processed
307
-			$this->writeCache = '';
308
-
309
-		}
310
-
311
-		$encrypted = '';
312
-		// While there still remains some data to be processed & written
313
-		while (strlen($data) > 0) {
314
-
315
-			// Remaining length for this iteration, not of the
316
-			// entire file (may be greater than 8192 bytes)
317
-			$remainingLength = strlen($data);
318
-
319
-			// If data remaining to be written is less than the
320
-			// size of 1 6126 byte block
321
-			if ($remainingLength < $this->unencryptedBlockSizeSigned) {
322
-
323
-				// Set writeCache to contents of $data
324
-				// The writeCache will be carried over to the
325
-				// next write round, and added to the start of
326
-				// $data to ensure that written blocks are
327
-				// always the correct length. If there is still
328
-				// data in writeCache after the writing round
329
-				// has finished, then the data will be written
330
-				// to disk by $this->flush().
331
-				$this->writeCache = $data;
332
-
333
-				// Clear $data ready for next round
334
-				$data = '';
335
-
336
-			} else {
337
-
338
-				// Read the chunk from the start of $data
339
-				$chunk = substr($data, 0, $this->unencryptedBlockSizeSigned);
340
-
341
-				$encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position);
342
-
343
-				// Remove the chunk we just processed from
344
-				// $data, leaving only unprocessed data in $data
345
-				// var, for handling on the next round
346
-				$data = substr($data, $this->unencryptedBlockSizeSigned);
347
-
348
-			}
349
-
350
-		}
351
-
352
-		return $encrypted;
353
-	}
354
-
355
-	/**
356
-	 * decrypt data
357
-	 *
358
-	 * @param string $data you want to decrypt
359
-	 * @param int $position
360
-	 * @return string decrypted data
361
-	 * @throws DecryptionFailedException
362
-	 */
363
-	public function decrypt($data, $position = 0) {
364
-		if (empty($this->fileKey)) {
365
-			$msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
366
-			$hint = $this->l->t('Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
367
-			$this->logger->error($msg);
368
-
369
-			throw new DecryptionFailedException($msg, $hint);
370
-		}
371
-
372
-		return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
373
-	}
374
-
375
-	/**
376
-	 * update encrypted file, e.g. give additional users access to the file
377
-	 *
378
-	 * @param string $path path to the file which should be updated
379
-	 * @param string $uid of the user who performs the operation
380
-	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
381
-	 * @return boolean
382
-	 */
383
-	public function update($path, $uid, array $accessList) {
384
-
385
-		if (empty($accessList)) {
386
-			if (isset(self::$rememberVersion[$path])) {
387
-				$this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
388
-				unset(self::$rememberVersion[$path]);
389
-			}
390
-			return;
391
-		}
392
-
393
-		$fileKey = $this->keyManager->getFileKey($path, $uid);
394
-
395
-		if (!empty($fileKey)) {
396
-
397
-			$publicKeys = array();
398
-			if ($this->useMasterPassword === true) {
399
-				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
400
-			} else {
401
-				foreach ($accessList['users'] as $user) {
402
-					try {
403
-						$publicKeys[$user] = $this->keyManager->getPublicKey($user);
404
-					} catch (PublicKeyMissingException $e) {
405
-						$this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
406
-					}
407
-				}
408
-			}
409
-
410
-			$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $uid);
411
-
412
-			$encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
413
-
414
-			$this->keyManager->deleteAllFileKeys($path);
415
-
416
-			$this->keyManager->setAllFileKeys($path, $encryptedFileKey);
417
-
418
-		} else {
419
-			$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
420
-				array('file' => $path, 'app' => 'encryption'));
421
-
422
-			return false;
423
-		}
424
-
425
-		return true;
426
-	}
427
-
428
-	/**
429
-	 * should the file be encrypted or not
430
-	 *
431
-	 * @param string $path
432
-	 * @return boolean
433
-	 */
434
-	public function shouldEncrypt($path) {
435
-		if ($this->util->shouldEncryptHomeStorage() === false) {
436
-			$storage = $this->util->getStorage($path);
437
-			if ($storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
438
-				return false;
439
-			}
440
-		}
441
-		$parts = explode('/', $path);
442
-		if (count($parts) < 4) {
443
-			return false;
444
-		}
445
-
446
-		if ($parts[2] == 'files') {
447
-			return true;
448
-		}
449
-		if ($parts[2] == 'files_versions') {
450
-			return true;
451
-		}
452
-		if ($parts[2] == 'files_trashbin') {
453
-			return true;
454
-		}
455
-
456
-		return false;
457
-	}
458
-
459
-	/**
460
-	 * get size of the unencrypted payload per block.
461
-	 * Nextcloud read/write files with a block size of 8192 byte
462
-	 *
463
-	 * @param bool $signed
464
-	 * @return int
465
-	 */
466
-	public function getUnencryptedBlockSize($signed = false) {
467
-		if ($signed === false) {
468
-			return $this->unencryptedBlockSize;
469
-		}
470
-
471
-		return $this->unencryptedBlockSizeSigned;
472
-	}
473
-
474
-	/**
475
-	 * check if the encryption module is able to read the file,
476
-	 * e.g. if all encryption keys exists
477
-	 *
478
-	 * @param string $path
479
-	 * @param string $uid user for whom we want to check if he can read the file
480
-	 * @return bool
481
-	 * @throws DecryptionFailedException
482
-	 */
483
-	public function isReadable($path, $uid) {
484
-		$fileKey = $this->keyManager->getFileKey($path, $uid);
485
-		if (empty($fileKey)) {
486
-			$owner = $this->util->getOwner($path);
487
-			if ($owner !== $uid) {
488
-				// if it is a shared file we throw a exception with a useful
489
-				// error message because in this case it means that the file was
490
-				// shared with the user at a point where the user didn't had a
491
-				// valid private/public key
492
-				$msg = 'Encryption module "' . $this->getDisplayName() .
493
-					'" is not able to read ' . $path;
494
-				$hint = $this->l->t('Can not read this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
495
-				$this->logger->warning($msg);
496
-				throw new DecryptionFailedException($msg, $hint);
497
-			}
498
-			return false;
499
-		}
500
-
501
-		return true;
502
-	}
503
-
504
-	/**
505
-	 * Initial encryption of all files
506
-	 *
507
-	 * @param InputInterface $input
508
-	 * @param OutputInterface $output write some status information to the terminal during encryption
509
-	 */
510
-	public function encryptAll(InputInterface $input, OutputInterface $output) {
511
-		$this->encryptAll->encryptAll($input, $output);
512
-	}
513
-
514
-	/**
515
-	 * prepare module to perform decrypt all operation
516
-	 *
517
-	 * @param InputInterface $input
518
-	 * @param OutputInterface $output
519
-	 * @param string $user
520
-	 * @return bool
521
-	 */
522
-	public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
523
-		return $this->decryptAll->prepare($input, $output, $user);
524
-	}
525
-
526
-
527
-	/**
528
-	 * @param string $path
529
-	 * @return string
530
-	 */
531
-	protected function getPathToRealFile($path) {
532
-		$realPath = $path;
533
-		$parts = explode('/', $path);
534
-		if ($parts[2] === 'files_versions') {
535
-			$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
536
-			$length = strrpos($realPath, '.');
537
-			$realPath = substr($realPath, 0, $length);
538
-		}
539
-
540
-		return $realPath;
541
-	}
542
-
543
-	/**
544
-	 * remove .part file extension and the ocTransferId from the file to get the
545
-	 * original file name
546
-	 *
547
-	 * @param string $path
548
-	 * @return string
549
-	 */
550
-	protected function stripPartFileExtension($path) {
551
-		if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
552
-			$pos = strrpos($path, '.', -6);
553
-			$path = substr($path, 0, $pos);
554
-		}
555
-
556
-		return $path;
557
-	}
558
-
559
-	/**
560
-	 * Check if the module is ready to be used by that specific user.
561
-	 * In case a module is not ready - because e.g. key pairs have not been generated
562
-	 * upon login this method can return false before any operation starts and might
563
-	 * cause issues during operations.
564
-	 *
565
-	 * @param string $user
566
-	 * @return boolean
567
-	 * @since 9.1.0
568
-	 */
569
-	public function isReadyForUser($user) {
570
-		return $this->keyManager->userHasKeys($user);
571
-	}
46
+    const ID = 'OC_DEFAULT_MODULE';
47
+    const DISPLAY_NAME = 'Default encryption module';
48
+
49
+    /**
50
+     * @var Crypt
51
+     */
52
+    private $crypt;
53
+
54
+    /** @var string */
55
+    private $cipher;
56
+
57
+    /** @var string */
58
+    private $path;
59
+
60
+    /** @var string */
61
+    private $user;
62
+
63
+    /** @var string */
64
+    private $fileKey;
65
+
66
+    /** @var string */
67
+    private $writeCache;
68
+
69
+    /** @var KeyManager */
70
+    private $keyManager;
71
+
72
+    /** @var array */
73
+    private $accessList;
74
+
75
+    /** @var boolean */
76
+    private $isWriteOperation;
77
+
78
+    /** @var Util */
79
+    private $util;
80
+
81
+    /** @var  Session */
82
+    private $session;
83
+
84
+    /** @var  ILogger */
85
+    private $logger;
86
+
87
+    /** @var IL10N */
88
+    private $l;
89
+
90
+    /** @var EncryptAll */
91
+    private $encryptAll;
92
+
93
+    /** @var  bool */
94
+    private $useMasterPassword;
95
+
96
+    /** @var DecryptAll  */
97
+    private $decryptAll;
98
+
99
+    /** @var int unencrypted block size if block contains signature */
100
+    private $unencryptedBlockSizeSigned = 6072;
101
+
102
+    /** @var int unencrypted block size */
103
+    private $unencryptedBlockSize = 6126;
104
+
105
+    /** @var int Current version of the file */
106
+    private $version = 0;
107
+
108
+    /** @var array remember encryption signature version */
109
+    private static $rememberVersion = [];
110
+
111
+
112
+    /**
113
+     *
114
+     * @param Crypt $crypt
115
+     * @param KeyManager $keyManager
116
+     * @param Util $util
117
+     * @param Session $session
118
+     * @param EncryptAll $encryptAll
119
+     * @param DecryptAll $decryptAll
120
+     * @param ILogger $logger
121
+     * @param IL10N $il10n
122
+     */
123
+    public function __construct(Crypt $crypt,
124
+                                KeyManager $keyManager,
125
+                                Util $util,
126
+                                Session $session,
127
+                                EncryptAll $encryptAll,
128
+                                DecryptAll $decryptAll,
129
+                                ILogger $logger,
130
+                                IL10N $il10n) {
131
+        $this->crypt = $crypt;
132
+        $this->keyManager = $keyManager;
133
+        $this->util = $util;
134
+        $this->session = $session;
135
+        $this->encryptAll = $encryptAll;
136
+        $this->decryptAll = $decryptAll;
137
+        $this->logger = $logger;
138
+        $this->l = $il10n;
139
+        $this->useMasterPassword = $util->isMasterKeyEnabled();
140
+    }
141
+
142
+    /**
143
+     * @return string defining the technical unique id
144
+     */
145
+    public function getId() {
146
+        return self::ID;
147
+    }
148
+
149
+    /**
150
+     * In comparison to getKey() this function returns a human readable (maybe translated) name
151
+     *
152
+     * @return string
153
+     */
154
+    public function getDisplayName() {
155
+        return self::DISPLAY_NAME;
156
+    }
157
+
158
+    /**
159
+     * start receiving chunks from a file. This is the place where you can
160
+     * perform some initial step before starting encrypting/decrypting the
161
+     * chunks
162
+     *
163
+     * @param string $path to the file
164
+     * @param string $user who read/write the file
165
+     * @param string $mode php stream open mode
166
+     * @param array $header contains the header data read from the file
167
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
168
+     *
169
+     * @return array $header contain data as key-value pairs which should be
170
+     *                       written to the header, in case of a write operation
171
+     *                       or if no additional data is needed return a empty array
172
+     */
173
+    public function begin($path, $user, $mode, array $header, array $accessList) {
174
+        $this->path = $this->getPathToRealFile($path);
175
+        $this->accessList = $accessList;
176
+        $this->user = $user;
177
+        $this->isWriteOperation = false;
178
+        $this->writeCache = '';
179
+
180
+        if($this->session->isReady() === false) {
181
+            // if the master key is enabled we can initialize encryption
182
+            // with a empty password and user name
183
+            if ($this->util->isMasterKeyEnabled()) {
184
+                $this->keyManager->init('', '');
185
+            }
186
+        }
187
+
188
+        if ($this->session->decryptAllModeActivated()) {
189
+            $encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
190
+            $shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
191
+            $this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
192
+                $shareKey,
193
+                $this->session->getDecryptAllKey());
194
+        } else {
195
+            $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
196
+        }
197
+
198
+        // always use the version from the original file, also part files
199
+        // need to have a correct version number if they get moved over to the
200
+        // final location
201
+        $this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
202
+
203
+        if (
204
+            $mode === 'w'
205
+            || $mode === 'w+'
206
+            || $mode === 'wb'
207
+            || $mode === 'wb+'
208
+        ) {
209
+            $this->isWriteOperation = true;
210
+            if (empty($this->fileKey)) {
211
+                $this->fileKey = $this->crypt->generateFileKey();
212
+            }
213
+        } else {
214
+            // if we read a part file we need to increase the version by 1
215
+            // because the version number was also increased by writing
216
+            // the part file
217
+            if(Scanner::isPartialFile($path)) {
218
+                $this->version = $this->version + 1;
219
+            }
220
+        }
221
+
222
+        if ($this->isWriteOperation) {
223
+            $this->cipher = $this->crypt->getCipher();
224
+        } elseif (isset($header['cipher'])) {
225
+            $this->cipher = $header['cipher'];
226
+        } else {
227
+            // if we read a file without a header we fall-back to the legacy cipher
228
+            // which was used in <=oC6
229
+            $this->cipher = $this->crypt->getLegacyCipher();
230
+        }
231
+
232
+        return array('cipher' => $this->cipher, 'signed' => 'true');
233
+    }
234
+
235
+    /**
236
+     * last chunk received. This is the place where you can perform some final
237
+     * operation and return some remaining data if something is left in your
238
+     * buffer.
239
+     *
240
+     * @param string $path to the file
241
+     * @param int $position
242
+     * @return string remained data which should be written to the file in case
243
+     *                of a write operation
244
+     * @throws PublicKeyMissingException
245
+     * @throws \Exception
246
+     * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
247
+     */
248
+    public function end($path, $position = 0) {
249
+        $result = '';
250
+        if ($this->isWriteOperation) {
251
+            $this->keyManager->setVersion($path, $this->version + 1, new View());
252
+            // in case of a part file we remember the new signature versions
253
+            // the version will be set later on update.
254
+            // This way we make sure that other apps listening to the pre-hooks
255
+            // still get the old version which should be the correct value for them
256
+            if (Scanner::isPartialFile($path)) {
257
+                self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
258
+            }
259
+            if (!empty($this->writeCache)) {
260
+                $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
261
+                $this->writeCache = '';
262
+            }
263
+            $publicKeys = array();
264
+            if ($this->useMasterPassword === true) {
265
+                $publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
266
+            } else {
267
+                foreach ($this->accessList['users'] as $uid) {
268
+                    try {
269
+                        $publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
270
+                    } catch (PublicKeyMissingException $e) {
271
+                        $this->logger->warning(
272
+                            'no public key found for user "{uid}", user will not be able to read the file',
273
+                            ['app' => 'encryption', 'uid' => $uid]
274
+                        );
275
+                        // if the public key of the owner is missing we should fail
276
+                        if ($uid === $this->user) {
277
+                            throw $e;
278
+                        }
279
+                    }
280
+                }
281
+            }
282
+
283
+            $publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->user);
284
+            $encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
285
+            $this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
286
+        }
287
+        return $result;
288
+    }
289
+
290
+    /**
291
+     * encrypt data
292
+     *
293
+     * @param string $data you want to encrypt
294
+     * @param int $position
295
+     * @return string encrypted data
296
+     */
297
+    public function encrypt($data, $position = 0) {
298
+        // If extra data is left over from the last round, make sure it
299
+        // is integrated into the next block
300
+        if ($this->writeCache) {
301
+
302
+            // Concat writeCache to start of $data
303
+            $data = $this->writeCache . $data;
304
+
305
+            // Clear the write cache, ready for reuse - it has been
306
+            // flushed and its old contents processed
307
+            $this->writeCache = '';
308
+
309
+        }
310
+
311
+        $encrypted = '';
312
+        // While there still remains some data to be processed & written
313
+        while (strlen($data) > 0) {
314
+
315
+            // Remaining length for this iteration, not of the
316
+            // entire file (may be greater than 8192 bytes)
317
+            $remainingLength = strlen($data);
318
+
319
+            // If data remaining to be written is less than the
320
+            // size of 1 6126 byte block
321
+            if ($remainingLength < $this->unencryptedBlockSizeSigned) {
322
+
323
+                // Set writeCache to contents of $data
324
+                // The writeCache will be carried over to the
325
+                // next write round, and added to the start of
326
+                // $data to ensure that written blocks are
327
+                // always the correct length. If there is still
328
+                // data in writeCache after the writing round
329
+                // has finished, then the data will be written
330
+                // to disk by $this->flush().
331
+                $this->writeCache = $data;
332
+
333
+                // Clear $data ready for next round
334
+                $data = '';
335
+
336
+            } else {
337
+
338
+                // Read the chunk from the start of $data
339
+                $chunk = substr($data, 0, $this->unencryptedBlockSizeSigned);
340
+
341
+                $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position);
342
+
343
+                // Remove the chunk we just processed from
344
+                // $data, leaving only unprocessed data in $data
345
+                // var, for handling on the next round
346
+                $data = substr($data, $this->unencryptedBlockSizeSigned);
347
+
348
+            }
349
+
350
+        }
351
+
352
+        return $encrypted;
353
+    }
354
+
355
+    /**
356
+     * decrypt data
357
+     *
358
+     * @param string $data you want to decrypt
359
+     * @param int $position
360
+     * @return string decrypted data
361
+     * @throws DecryptionFailedException
362
+     */
363
+    public function decrypt($data, $position = 0) {
364
+        if (empty($this->fileKey)) {
365
+            $msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
366
+            $hint = $this->l->t('Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
367
+            $this->logger->error($msg);
368
+
369
+            throw new DecryptionFailedException($msg, $hint);
370
+        }
371
+
372
+        return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
373
+    }
374
+
375
+    /**
376
+     * update encrypted file, e.g. give additional users access to the file
377
+     *
378
+     * @param string $path path to the file which should be updated
379
+     * @param string $uid of the user who performs the operation
380
+     * @param array $accessList who has access to the file contains the key 'users' and 'public'
381
+     * @return boolean
382
+     */
383
+    public function update($path, $uid, array $accessList) {
384
+
385
+        if (empty($accessList)) {
386
+            if (isset(self::$rememberVersion[$path])) {
387
+                $this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
388
+                unset(self::$rememberVersion[$path]);
389
+            }
390
+            return;
391
+        }
392
+
393
+        $fileKey = $this->keyManager->getFileKey($path, $uid);
394
+
395
+        if (!empty($fileKey)) {
396
+
397
+            $publicKeys = array();
398
+            if ($this->useMasterPassword === true) {
399
+                $publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
400
+            } else {
401
+                foreach ($accessList['users'] as $user) {
402
+                    try {
403
+                        $publicKeys[$user] = $this->keyManager->getPublicKey($user);
404
+                    } catch (PublicKeyMissingException $e) {
405
+                        $this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
406
+                    }
407
+                }
408
+            }
409
+
410
+            $publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $uid);
411
+
412
+            $encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
413
+
414
+            $this->keyManager->deleteAllFileKeys($path);
415
+
416
+            $this->keyManager->setAllFileKeys($path, $encryptedFileKey);
417
+
418
+        } else {
419
+            $this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
420
+                array('file' => $path, 'app' => 'encryption'));
421
+
422
+            return false;
423
+        }
424
+
425
+        return true;
426
+    }
427
+
428
+    /**
429
+     * should the file be encrypted or not
430
+     *
431
+     * @param string $path
432
+     * @return boolean
433
+     */
434
+    public function shouldEncrypt($path) {
435
+        if ($this->util->shouldEncryptHomeStorage() === false) {
436
+            $storage = $this->util->getStorage($path);
437
+            if ($storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
438
+                return false;
439
+            }
440
+        }
441
+        $parts = explode('/', $path);
442
+        if (count($parts) < 4) {
443
+            return false;
444
+        }
445
+
446
+        if ($parts[2] == 'files') {
447
+            return true;
448
+        }
449
+        if ($parts[2] == 'files_versions') {
450
+            return true;
451
+        }
452
+        if ($parts[2] == 'files_trashbin') {
453
+            return true;
454
+        }
455
+
456
+        return false;
457
+    }
458
+
459
+    /**
460
+     * get size of the unencrypted payload per block.
461
+     * Nextcloud read/write files with a block size of 8192 byte
462
+     *
463
+     * @param bool $signed
464
+     * @return int
465
+     */
466
+    public function getUnencryptedBlockSize($signed = false) {
467
+        if ($signed === false) {
468
+            return $this->unencryptedBlockSize;
469
+        }
470
+
471
+        return $this->unencryptedBlockSizeSigned;
472
+    }
473
+
474
+    /**
475
+     * check if the encryption module is able to read the file,
476
+     * e.g. if all encryption keys exists
477
+     *
478
+     * @param string $path
479
+     * @param string $uid user for whom we want to check if he can read the file
480
+     * @return bool
481
+     * @throws DecryptionFailedException
482
+     */
483
+    public function isReadable($path, $uid) {
484
+        $fileKey = $this->keyManager->getFileKey($path, $uid);
485
+        if (empty($fileKey)) {
486
+            $owner = $this->util->getOwner($path);
487
+            if ($owner !== $uid) {
488
+                // if it is a shared file we throw a exception with a useful
489
+                // error message because in this case it means that the file was
490
+                // shared with the user at a point where the user didn't had a
491
+                // valid private/public key
492
+                $msg = 'Encryption module "' . $this->getDisplayName() .
493
+                    '" is not able to read ' . $path;
494
+                $hint = $this->l->t('Can not read this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
495
+                $this->logger->warning($msg);
496
+                throw new DecryptionFailedException($msg, $hint);
497
+            }
498
+            return false;
499
+        }
500
+
501
+        return true;
502
+    }
503
+
504
+    /**
505
+     * Initial encryption of all files
506
+     *
507
+     * @param InputInterface $input
508
+     * @param OutputInterface $output write some status information to the terminal during encryption
509
+     */
510
+    public function encryptAll(InputInterface $input, OutputInterface $output) {
511
+        $this->encryptAll->encryptAll($input, $output);
512
+    }
513
+
514
+    /**
515
+     * prepare module to perform decrypt all operation
516
+     *
517
+     * @param InputInterface $input
518
+     * @param OutputInterface $output
519
+     * @param string $user
520
+     * @return bool
521
+     */
522
+    public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
523
+        return $this->decryptAll->prepare($input, $output, $user);
524
+    }
525
+
526
+
527
+    /**
528
+     * @param string $path
529
+     * @return string
530
+     */
531
+    protected function getPathToRealFile($path) {
532
+        $realPath = $path;
533
+        $parts = explode('/', $path);
534
+        if ($parts[2] === 'files_versions') {
535
+            $realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
536
+            $length = strrpos($realPath, '.');
537
+            $realPath = substr($realPath, 0, $length);
538
+        }
539
+
540
+        return $realPath;
541
+    }
542
+
543
+    /**
544
+     * remove .part file extension and the ocTransferId from the file to get the
545
+     * original file name
546
+     *
547
+     * @param string $path
548
+     * @return string
549
+     */
550
+    protected function stripPartFileExtension($path) {
551
+        if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
552
+            $pos = strrpos($path, '.', -6);
553
+            $path = substr($path, 0, $pos);
554
+        }
555
+
556
+        return $path;
557
+    }
558
+
559
+    /**
560
+     * Check if the module is ready to be used by that specific user.
561
+     * In case a module is not ready - because e.g. key pairs have not been generated
562
+     * upon login this method can return false before any operation starts and might
563
+     * cause issues during operations.
564
+     *
565
+     * @param string $user
566
+     * @return boolean
567
+     * @since 9.1.0
568
+     */
569
+    public function isReadyForUser($user) {
570
+        return $this->keyManager->userHasKeys($user);
571
+    }
572 572
 }
Please login to merge, or discard this patch.
apps/encryption/lib/Hooks/UserHooks.php 1 patch
Indentation   +302 added lines, -302 removed lines patch added patch discarded remove patch
@@ -41,306 +41,306 @@
 block discarded – undo
41 41
 
42 42
 class UserHooks implements IHook {
43 43
 
44
-	/**
45
-	 * list of user for which we perform a password reset
46
-	 * @var array
47
-	 */
48
-	protected static $passwordResetUsers = [];
49
-
50
-	/**
51
-	 * @var KeyManager
52
-	 */
53
-	private $keyManager;
54
-	/**
55
-	 * @var IUserManager
56
-	 */
57
-	private $userManager;
58
-	/**
59
-	 * @var ILogger
60
-	 */
61
-	private $logger;
62
-	/**
63
-	 * @var Setup
64
-	 */
65
-	private $userSetup;
66
-	/**
67
-	 * @var IUserSession
68
-	 */
69
-	private $user;
70
-	/**
71
-	 * @var Util
72
-	 */
73
-	private $util;
74
-	/**
75
-	 * @var Session
76
-	 */
77
-	private $session;
78
-	/**
79
-	 * @var Recovery
80
-	 */
81
-	private $recovery;
82
-	/**
83
-	 * @var Crypt
84
-	 */
85
-	private $crypt;
86
-
87
-	/**
88
-	 * UserHooks constructor.
89
-	 *
90
-	 * @param KeyManager $keyManager
91
-	 * @param IUserManager $userManager
92
-	 * @param ILogger $logger
93
-	 * @param Setup $userSetup
94
-	 * @param IUserSession $user
95
-	 * @param Util $util
96
-	 * @param Session $session
97
-	 * @param Crypt $crypt
98
-	 * @param Recovery $recovery
99
-	 */
100
-	public function __construct(KeyManager $keyManager,
101
-								IUserManager $userManager,
102
-								ILogger $logger,
103
-								Setup $userSetup,
104
-								IUserSession $user,
105
-								Util $util,
106
-								Session $session,
107
-								Crypt $crypt,
108
-								Recovery $recovery) {
109
-
110
-		$this->keyManager = $keyManager;
111
-		$this->userManager = $userManager;
112
-		$this->logger = $logger;
113
-		$this->userSetup = $userSetup;
114
-		$this->user = $user;
115
-		$this->util = $util;
116
-		$this->session = $session;
117
-		$this->recovery = $recovery;
118
-		$this->crypt = $crypt;
119
-	}
120
-
121
-	/**
122
-	 * Connects Hooks
123
-	 *
124
-	 * @return null
125
-	 */
126
-	public function addHooks() {
127
-		OCUtil::connectHook('OC_User', 'post_login', $this, 'login');
128
-		OCUtil::connectHook('OC_User', 'logout', $this, 'logout');
129
-
130
-		// this hooks only make sense if no master key is used
131
-		if ($this->util->isMasterKeyEnabled() === false) {
132
-			OCUtil::connectHook('OC_User',
133
-				'post_setPassword',
134
-				$this,
135
-				'setPassphrase');
136
-
137
-			OCUtil::connectHook('OC_User',
138
-				'pre_setPassword',
139
-				$this,
140
-				'preSetPassphrase');
141
-
142
-			OCUtil::connectHook('\OC\Core\LostPassword\Controller\LostController',
143
-				'post_passwordReset',
144
-				$this,
145
-				'postPasswordReset');
146
-
147
-			OCUtil::connectHook('\OC\Core\LostPassword\Controller\LostController',
148
-				'pre_passwordReset',
149
-				$this,
150
-				'prePasswordReset');
151
-
152
-			OCUtil::connectHook('OC_User',
153
-				'post_createUser',
154
-				$this,
155
-				'postCreateUser');
156
-
157
-			OCUtil::connectHook('OC_User',
158
-				'post_deleteUser',
159
-				$this,
160
-				'postDeleteUser');
161
-		}
162
-	}
163
-
164
-
165
-	/**
166
-	 * Startup encryption backend upon user login
167
-	 *
168
-	 * @note This method should never be called for users using client side encryption
169
-	 * @param array $params
170
-	 * @return boolean|null
171
-	 */
172
-	public function login($params) {
173
-
174
-		if (!App::isEnabled('encryption')) {
175
-			return true;
176
-		}
177
-
178
-		// ensure filesystem is loaded
179
-		if (!\OC\Files\Filesystem::$loaded) {
180
-			$this->setupFS($params['uid']);
181
-		}
182
-		if ($this->util->isMasterKeyEnabled() === false) {
183
-			$this->userSetup->setupUser($params['uid'], $params['password']);
184
-		}
185
-
186
-		$this->keyManager->init($params['uid'], $params['password']);
187
-	}
188
-
189
-	/**
190
-	 * remove keys from session during logout
191
-	 */
192
-	public function logout() {
193
-		$this->session->clear();
194
-	}
195
-
196
-	/**
197
-	 * setup encryption backend upon user created
198
-	 *
199
-	 * @note This method should never be called for users using client side encryption
200
-	 * @param array $params
201
-	 */
202
-	public function postCreateUser($params) {
203
-
204
-		if (App::isEnabled('encryption')) {
205
-			$this->userSetup->setupUser($params['uid'], $params['password']);
206
-		}
207
-	}
208
-
209
-	/**
210
-	 * cleanup encryption backend upon user deleted
211
-	 *
212
-	 * @param array $params : uid, password
213
-	 * @note This method should never be called for users using client side encryption
214
-	 */
215
-	public function postDeleteUser($params) {
216
-
217
-		if (App::isEnabled('encryption')) {
218
-			$this->keyManager->deletePublicKey($params['uid']);
219
-		}
220
-	}
221
-
222
-	public function prePasswordReset($params) {
223
-		if (App::isEnabled('encryption')) {
224
-			$user = $params['uid'];
225
-			self::$passwordResetUsers[$user] = true;
226
-		}
227
-	}
228
-
229
-	public function postPasswordReset($params) {
230
-		$uid = $params['uid'];
231
-		$password = $params['password'];
232
-		$this->keyManager->backupUserKeys('passwordReset', $uid);
233
-		$this->keyManager->deleteUserKeys($uid);
234
-		$this->userSetup->setupUser($uid, $password);
235
-		unset(self::$passwordResetUsers[$uid]);
236
-	}
237
-
238
-	/**
239
-	 * If the password can't be changed within Nextcloud, than update the key password in advance.
240
-	 *
241
-	 * @param array $params : uid, password
242
-	 * @return boolean|null
243
-	 */
244
-	public function preSetPassphrase($params) {
245
-		$user = $this->userManager->get($params['uid']);
246
-
247
-		if ($user && !$user->canChangePassword()) {
248
-			$this->setPassphrase($params);
249
-		}
250
-	}
251
-
252
-	/**
253
-	 * Change a user's encryption passphrase
254
-	 *
255
-	 * @param array $params keys: uid, password
256
-	 * @return boolean|null
257
-	 */
258
-	public function setPassphrase($params) {
259
-
260
-		// if we are in the process to resetting a user password, we have nothing
261
-		// to do here
262
-		if (isset(self::$passwordResetUsers[$params['uid']])) {
263
-			return true;
264
-		}
265
-
266
-		// Get existing decrypted private key
267
-		$privateKey = $this->session->getPrivateKey();
268
-		$user = $this->user->getUser();
269
-
270
-		// current logged in user changes his own password
271
-		if ($user && $params['uid'] === $user->getUID() && $privateKey) {
272
-
273
-			// Encrypt private key with new user pwd as passphrase
274
-			$encryptedPrivateKey = $this->crypt->encryptPrivateKey($privateKey, $params['password'], $params['uid']);
275
-
276
-			// Save private key
277
-			if ($encryptedPrivateKey) {
278
-				$this->keyManager->setPrivateKey($this->user->getUser()->getUID(),
279
-					$this->crypt->generateHeader() . $encryptedPrivateKey);
280
-			} else {
281
-				$this->logger->error('Encryption could not update users encryption password');
282
-			}
283
-
284
-			// NOTE: Session does not need to be updated as the
285
-			// private key has not changed, only the passphrase
286
-			// used to decrypt it has changed
287
-		} else { // admin changed the password for a different user, create new keys and re-encrypt file keys
288
-			$user = $params['uid'];
289
-			$this->initMountPoints($user);
290
-			$recoveryPassword = isset($params['recoveryPassword']) ? $params['recoveryPassword'] : null;
291
-
292
-			// we generate new keys if...
293
-			// ...we have a recovery password and the user enabled the recovery key
294
-			// ...encryption was activated for the first time (no keys exists)
295
-			// ...the user doesn't have any files
296
-			if (
297
-				($this->recovery->isRecoveryEnabledForUser($user) && $recoveryPassword)
298
-				|| !$this->keyManager->userHasKeys($user)
299
-				|| !$this->util->userHasFiles($user)
300
-			) {
301
-
302
-				// backup old keys
303
-				//$this->backupAllKeys('recovery');
304
-
305
-				$newUserPassword = $params['password'];
306
-
307
-				$keyPair = $this->crypt->createKeyPair();
308
-
309
-				// Save public key
310
-				$this->keyManager->setPublicKey($user, $keyPair['publicKey']);
311
-
312
-				// Encrypt private key with new password
313
-				$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $newUserPassword, $user);
314
-
315
-				if ($encryptedKey) {
316
-					$this->keyManager->setPrivateKey($user, $this->crypt->generateHeader() . $encryptedKey);
317
-
318
-					if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
319
-						$this->recovery->recoverUsersFiles($recoveryPassword, $user);
320
-					}
321
-				} else {
322
-					$this->logger->error('Encryption Could not update users encryption password');
323
-				}
324
-			}
325
-		}
326
-	}
327
-
328
-	/**
329
-	 * init mount points for given user
330
-	 *
331
-	 * @param string $user
332
-	 * @throws \OC\User\NoUserException
333
-	 */
334
-	protected function initMountPoints($user) {
335
-		Filesystem::initMountPoints($user);
336
-	}
337
-
338
-	/**
339
-	 * setup file system for user
340
-	 *
341
-	 * @param string $uid user id
342
-	 */
343
-	protected function setupFS($uid) {
344
-		\OC_Util::setupFS($uid);
345
-	}
44
+    /**
45
+     * list of user for which we perform a password reset
46
+     * @var array
47
+     */
48
+    protected static $passwordResetUsers = [];
49
+
50
+    /**
51
+     * @var KeyManager
52
+     */
53
+    private $keyManager;
54
+    /**
55
+     * @var IUserManager
56
+     */
57
+    private $userManager;
58
+    /**
59
+     * @var ILogger
60
+     */
61
+    private $logger;
62
+    /**
63
+     * @var Setup
64
+     */
65
+    private $userSetup;
66
+    /**
67
+     * @var IUserSession
68
+     */
69
+    private $user;
70
+    /**
71
+     * @var Util
72
+     */
73
+    private $util;
74
+    /**
75
+     * @var Session
76
+     */
77
+    private $session;
78
+    /**
79
+     * @var Recovery
80
+     */
81
+    private $recovery;
82
+    /**
83
+     * @var Crypt
84
+     */
85
+    private $crypt;
86
+
87
+    /**
88
+     * UserHooks constructor.
89
+     *
90
+     * @param KeyManager $keyManager
91
+     * @param IUserManager $userManager
92
+     * @param ILogger $logger
93
+     * @param Setup $userSetup
94
+     * @param IUserSession $user
95
+     * @param Util $util
96
+     * @param Session $session
97
+     * @param Crypt $crypt
98
+     * @param Recovery $recovery
99
+     */
100
+    public function __construct(KeyManager $keyManager,
101
+                                IUserManager $userManager,
102
+                                ILogger $logger,
103
+                                Setup $userSetup,
104
+                                IUserSession $user,
105
+                                Util $util,
106
+                                Session $session,
107
+                                Crypt $crypt,
108
+                                Recovery $recovery) {
109
+
110
+        $this->keyManager = $keyManager;
111
+        $this->userManager = $userManager;
112
+        $this->logger = $logger;
113
+        $this->userSetup = $userSetup;
114
+        $this->user = $user;
115
+        $this->util = $util;
116
+        $this->session = $session;
117
+        $this->recovery = $recovery;
118
+        $this->crypt = $crypt;
119
+    }
120
+
121
+    /**
122
+     * Connects Hooks
123
+     *
124
+     * @return null
125
+     */
126
+    public function addHooks() {
127
+        OCUtil::connectHook('OC_User', 'post_login', $this, 'login');
128
+        OCUtil::connectHook('OC_User', 'logout', $this, 'logout');
129
+
130
+        // this hooks only make sense if no master key is used
131
+        if ($this->util->isMasterKeyEnabled() === false) {
132
+            OCUtil::connectHook('OC_User',
133
+                'post_setPassword',
134
+                $this,
135
+                'setPassphrase');
136
+
137
+            OCUtil::connectHook('OC_User',
138
+                'pre_setPassword',
139
+                $this,
140
+                'preSetPassphrase');
141
+
142
+            OCUtil::connectHook('\OC\Core\LostPassword\Controller\LostController',
143
+                'post_passwordReset',
144
+                $this,
145
+                'postPasswordReset');
146
+
147
+            OCUtil::connectHook('\OC\Core\LostPassword\Controller\LostController',
148
+                'pre_passwordReset',
149
+                $this,
150
+                'prePasswordReset');
151
+
152
+            OCUtil::connectHook('OC_User',
153
+                'post_createUser',
154
+                $this,
155
+                'postCreateUser');
156
+
157
+            OCUtil::connectHook('OC_User',
158
+                'post_deleteUser',
159
+                $this,
160
+                'postDeleteUser');
161
+        }
162
+    }
163
+
164
+
165
+    /**
166
+     * Startup encryption backend upon user login
167
+     *
168
+     * @note This method should never be called for users using client side encryption
169
+     * @param array $params
170
+     * @return boolean|null
171
+     */
172
+    public function login($params) {
173
+
174
+        if (!App::isEnabled('encryption')) {
175
+            return true;
176
+        }
177
+
178
+        // ensure filesystem is loaded
179
+        if (!\OC\Files\Filesystem::$loaded) {
180
+            $this->setupFS($params['uid']);
181
+        }
182
+        if ($this->util->isMasterKeyEnabled() === false) {
183
+            $this->userSetup->setupUser($params['uid'], $params['password']);
184
+        }
185
+
186
+        $this->keyManager->init($params['uid'], $params['password']);
187
+    }
188
+
189
+    /**
190
+     * remove keys from session during logout
191
+     */
192
+    public function logout() {
193
+        $this->session->clear();
194
+    }
195
+
196
+    /**
197
+     * setup encryption backend upon user created
198
+     *
199
+     * @note This method should never be called for users using client side encryption
200
+     * @param array $params
201
+     */
202
+    public function postCreateUser($params) {
203
+
204
+        if (App::isEnabled('encryption')) {
205
+            $this->userSetup->setupUser($params['uid'], $params['password']);
206
+        }
207
+    }
208
+
209
+    /**
210
+     * cleanup encryption backend upon user deleted
211
+     *
212
+     * @param array $params : uid, password
213
+     * @note This method should never be called for users using client side encryption
214
+     */
215
+    public function postDeleteUser($params) {
216
+
217
+        if (App::isEnabled('encryption')) {
218
+            $this->keyManager->deletePublicKey($params['uid']);
219
+        }
220
+    }
221
+
222
+    public function prePasswordReset($params) {
223
+        if (App::isEnabled('encryption')) {
224
+            $user = $params['uid'];
225
+            self::$passwordResetUsers[$user] = true;
226
+        }
227
+    }
228
+
229
+    public function postPasswordReset($params) {
230
+        $uid = $params['uid'];
231
+        $password = $params['password'];
232
+        $this->keyManager->backupUserKeys('passwordReset', $uid);
233
+        $this->keyManager->deleteUserKeys($uid);
234
+        $this->userSetup->setupUser($uid, $password);
235
+        unset(self::$passwordResetUsers[$uid]);
236
+    }
237
+
238
+    /**
239
+     * If the password can't be changed within Nextcloud, than update the key password in advance.
240
+     *
241
+     * @param array $params : uid, password
242
+     * @return boolean|null
243
+     */
244
+    public function preSetPassphrase($params) {
245
+        $user = $this->userManager->get($params['uid']);
246
+
247
+        if ($user && !$user->canChangePassword()) {
248
+            $this->setPassphrase($params);
249
+        }
250
+    }
251
+
252
+    /**
253
+     * Change a user's encryption passphrase
254
+     *
255
+     * @param array $params keys: uid, password
256
+     * @return boolean|null
257
+     */
258
+    public function setPassphrase($params) {
259
+
260
+        // if we are in the process to resetting a user password, we have nothing
261
+        // to do here
262
+        if (isset(self::$passwordResetUsers[$params['uid']])) {
263
+            return true;
264
+        }
265
+
266
+        // Get existing decrypted private key
267
+        $privateKey = $this->session->getPrivateKey();
268
+        $user = $this->user->getUser();
269
+
270
+        // current logged in user changes his own password
271
+        if ($user && $params['uid'] === $user->getUID() && $privateKey) {
272
+
273
+            // Encrypt private key with new user pwd as passphrase
274
+            $encryptedPrivateKey = $this->crypt->encryptPrivateKey($privateKey, $params['password'], $params['uid']);
275
+
276
+            // Save private key
277
+            if ($encryptedPrivateKey) {
278
+                $this->keyManager->setPrivateKey($this->user->getUser()->getUID(),
279
+                    $this->crypt->generateHeader() . $encryptedPrivateKey);
280
+            } else {
281
+                $this->logger->error('Encryption could not update users encryption password');
282
+            }
283
+
284
+            // NOTE: Session does not need to be updated as the
285
+            // private key has not changed, only the passphrase
286
+            // used to decrypt it has changed
287
+        } else { // admin changed the password for a different user, create new keys and re-encrypt file keys
288
+            $user = $params['uid'];
289
+            $this->initMountPoints($user);
290
+            $recoveryPassword = isset($params['recoveryPassword']) ? $params['recoveryPassword'] : null;
291
+
292
+            // we generate new keys if...
293
+            // ...we have a recovery password and the user enabled the recovery key
294
+            // ...encryption was activated for the first time (no keys exists)
295
+            // ...the user doesn't have any files
296
+            if (
297
+                ($this->recovery->isRecoveryEnabledForUser($user) && $recoveryPassword)
298
+                || !$this->keyManager->userHasKeys($user)
299
+                || !$this->util->userHasFiles($user)
300
+            ) {
301
+
302
+                // backup old keys
303
+                //$this->backupAllKeys('recovery');
304
+
305
+                $newUserPassword = $params['password'];
306
+
307
+                $keyPair = $this->crypt->createKeyPair();
308
+
309
+                // Save public key
310
+                $this->keyManager->setPublicKey($user, $keyPair['publicKey']);
311
+
312
+                // Encrypt private key with new password
313
+                $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $newUserPassword, $user);
314
+
315
+                if ($encryptedKey) {
316
+                    $this->keyManager->setPrivateKey($user, $this->crypt->generateHeader() . $encryptedKey);
317
+
318
+                    if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
319
+                        $this->recovery->recoverUsersFiles($recoveryPassword, $user);
320
+                    }
321
+                } else {
322
+                    $this->logger->error('Encryption Could not update users encryption password');
323
+                }
324
+            }
325
+        }
326
+    }
327
+
328
+    /**
329
+     * init mount points for given user
330
+     *
331
+     * @param string $user
332
+     * @throws \OC\User\NoUserException
333
+     */
334
+    protected function initMountPoints($user) {
335
+        Filesystem::initMountPoints($user);
336
+    }
337
+
338
+    /**
339
+     * setup file system for user
340
+     *
341
+     * @param string $uid user id
342
+     */
343
+    protected function setupFS($uid) {
344
+        \OC_Util::setupFS($uid);
345
+    }
346 346
 }
Please login to merge, or discard this patch.
apps/encryption/lib/KeyManager.php 1 patch
Indentation   +672 added lines, -672 removed lines patch added patch discarded remove patch
@@ -38,676 +38,676 @@
 block discarded – undo
38 38
 
39 39
 class KeyManager {
40 40
 
41
-	/**
42
-	 * @var Session
43
-	 */
44
-	protected $session;
45
-	/**
46
-	 * @var IStorage
47
-	 */
48
-	private $keyStorage;
49
-	/**
50
-	 * @var Crypt
51
-	 */
52
-	private $crypt;
53
-	/**
54
-	 * @var string
55
-	 */
56
-	private $recoveryKeyId;
57
-	/**
58
-	 * @var string
59
-	 */
60
-	private $publicShareKeyId;
61
-	/**
62
-	 * @var string
63
-	 */
64
-	private $masterKeyId;
65
-	/**
66
-	 * @var string UserID
67
-	 */
68
-	private $keyId;
69
-	/**
70
-	 * @var string
71
-	 */
72
-	private $publicKeyId = 'publicKey';
73
-	/**
74
-	 * @var string
75
-	 */
76
-	private $privateKeyId = 'privateKey';
77
-
78
-	/**
79
-	 * @var string
80
-	 */
81
-	private $shareKeyId = 'shareKey';
82
-
83
-	/**
84
-	 * @var string
85
-	 */
86
-	private $fileKeyId = 'fileKey';
87
-	/**
88
-	 * @var IConfig
89
-	 */
90
-	private $config;
91
-	/**
92
-	 * @var ILogger
93
-	 */
94
-	private $log;
95
-	/**
96
-	 * @var Util
97
-	 */
98
-	private $util;
99
-
100
-	/**
101
-	 * @param IStorage $keyStorage
102
-	 * @param Crypt $crypt
103
-	 * @param IConfig $config
104
-	 * @param IUserSession $userSession
105
-	 * @param Session $session
106
-	 * @param ILogger $log
107
-	 * @param Util $util
108
-	 */
109
-	public function __construct(
110
-		IStorage $keyStorage,
111
-		Crypt $crypt,
112
-		IConfig $config,
113
-		IUserSession $userSession,
114
-		Session $session,
115
-		ILogger $log,
116
-		Util $util
117
-	) {
118
-
119
-		$this->util = $util;
120
-		$this->session = $session;
121
-		$this->keyStorage = $keyStorage;
122
-		$this->crypt = $crypt;
123
-		$this->config = $config;
124
-		$this->log = $log;
125
-
126
-		$this->recoveryKeyId = $this->config->getAppValue('encryption',
127
-			'recoveryKeyId');
128
-		if (empty($this->recoveryKeyId)) {
129
-			$this->recoveryKeyId = 'recoveryKey_' . substr(md5(time()), 0, 8);
130
-			$this->config->setAppValue('encryption',
131
-				'recoveryKeyId',
132
-				$this->recoveryKeyId);
133
-		}
134
-
135
-		$this->publicShareKeyId = $this->config->getAppValue('encryption',
136
-			'publicShareKeyId');
137
-		if (empty($this->publicShareKeyId)) {
138
-			$this->publicShareKeyId = 'pubShare_' . substr(md5(time()), 0, 8);
139
-			$this->config->setAppValue('encryption', 'publicShareKeyId', $this->publicShareKeyId);
140
-		}
141
-
142
-		$this->masterKeyId = $this->config->getAppValue('encryption',
143
-			'masterKeyId');
144
-		if (empty($this->masterKeyId)) {
145
-			$this->masterKeyId = 'master_' . substr(md5(time()), 0, 8);
146
-			$this->config->setAppValue('encryption', 'masterKeyId', $this->masterKeyId);
147
-		}
148
-
149
-		$this->keyId = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false;
150
-		$this->log = $log;
151
-	}
152
-
153
-	/**
154
-	 * check if key pair for public link shares exists, if not we create one
155
-	 */
156
-	public function validateShareKey() {
157
-		$shareKey = $this->getPublicShareKey();
158
-		if (empty($shareKey)) {
159
-			$keyPair = $this->crypt->createKeyPair();
160
-
161
-			// Save public key
162
-			$this->keyStorage->setSystemUserKey(
163
-				$this->publicShareKeyId . '.publicKey', $keyPair['publicKey'],
164
-				Encryption::ID);
165
-
166
-			// Encrypt private key empty passphrase
167
-			$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], '');
168
-			$header = $this->crypt->generateHeader();
169
-			$this->setSystemPrivateKey($this->publicShareKeyId, $header . $encryptedKey);
170
-		}
171
-	}
172
-
173
-	/**
174
-	 * check if a key pair for the master key exists, if not we create one
175
-	 */
176
-	public function validateMasterKey() {
177
-
178
-		if ($this->util->isMasterKeyEnabled() === false) {
179
-			return;
180
-		}
181
-
182
-		$masterKey = $this->getPublicMasterKey();
183
-		if (empty($masterKey)) {
184
-			$keyPair = $this->crypt->createKeyPair();
185
-
186
-			// Save public key
187
-			$this->keyStorage->setSystemUserKey(
188
-				$this->masterKeyId . '.publicKey', $keyPair['publicKey'],
189
-				Encryption::ID);
190
-
191
-			// Encrypt private key with system password
192
-			$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $this->getMasterKeyPassword(), $this->masterKeyId);
193
-			$header = $this->crypt->generateHeader();
194
-			$this->setSystemPrivateKey($this->masterKeyId, $header . $encryptedKey);
195
-		}
196
-	}
197
-
198
-	/**
199
-	 * @return bool
200
-	 */
201
-	public function recoveryKeyExists() {
202
-		$key = $this->getRecoveryKey();
203
-		return (!empty($key));
204
-	}
205
-
206
-	/**
207
-	 * get recovery key
208
-	 *
209
-	 * @return string
210
-	 */
211
-	public function getRecoveryKey() {
212
-		return $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.publicKey', Encryption::ID);
213
-	}
214
-
215
-	/**
216
-	 * get recovery key ID
217
-	 *
218
-	 * @return string
219
-	 */
220
-	public function getRecoveryKeyId() {
221
-		return $this->recoveryKeyId;
222
-	}
223
-
224
-	/**
225
-	 * @param string $password
226
-	 * @return bool
227
-	 */
228
-	public function checkRecoveryPassword($password) {
229
-		$recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.privateKey', Encryption::ID);
230
-		$decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
231
-
232
-		if ($decryptedRecoveryKey) {
233
-			return true;
234
-		}
235
-		return false;
236
-	}
237
-
238
-	/**
239
-	 * @param string $uid
240
-	 * @param string $password
241
-	 * @param string $keyPair
242
-	 * @return bool
243
-	 */
244
-	public function storeKeyPair($uid, $password, $keyPair) {
245
-		// Save Public Key
246
-		$this->setPublicKey($uid, $keyPair['publicKey']);
247
-
248
-		$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password, $uid);
249
-
250
-		$header = $this->crypt->generateHeader();
251
-
252
-		if ($encryptedKey) {
253
-			$this->setPrivateKey($uid, $header . $encryptedKey);
254
-			return true;
255
-		}
256
-		return false;
257
-	}
258
-
259
-	/**
260
-	 * @param string $password
261
-	 * @param array $keyPair
262
-	 * @return bool
263
-	 */
264
-	public function setRecoveryKey($password, $keyPair) {
265
-		// Save Public Key
266
-		$this->keyStorage->setSystemUserKey($this->getRecoveryKeyId().
267
-			'.publicKey',
268
-			$keyPair['publicKey'],
269
-			Encryption::ID);
270
-
271
-		$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password);
272
-		$header = $this->crypt->generateHeader();
273
-
274
-		if ($encryptedKey) {
275
-			$this->setSystemPrivateKey($this->getRecoveryKeyId(), $header . $encryptedKey);
276
-			return true;
277
-		}
278
-		return false;
279
-	}
280
-
281
-	/**
282
-	 * @param $userId
283
-	 * @param $key
284
-	 * @return bool
285
-	 */
286
-	public function setPublicKey($userId, $key) {
287
-		return $this->keyStorage->setUserKey($userId, $this->publicKeyId, $key, Encryption::ID);
288
-	}
289
-
290
-	/**
291
-	 * @param $userId
292
-	 * @param string $key
293
-	 * @return bool
294
-	 */
295
-	public function setPrivateKey($userId, $key) {
296
-		return $this->keyStorage->setUserKey($userId,
297
-			$this->privateKeyId,
298
-			$key,
299
-			Encryption::ID);
300
-	}
301
-
302
-	/**
303
-	 * write file key to key storage
304
-	 *
305
-	 * @param string $path
306
-	 * @param string $key
307
-	 * @return boolean
308
-	 */
309
-	public function setFileKey($path, $key) {
310
-		return $this->keyStorage->setFileKey($path, $this->fileKeyId, $key, Encryption::ID);
311
-	}
312
-
313
-	/**
314
-	 * set all file keys (the file key and the corresponding share keys)
315
-	 *
316
-	 * @param string $path
317
-	 * @param array $keys
318
-	 */
319
-	public function setAllFileKeys($path, $keys) {
320
-		$this->setFileKey($path, $keys['data']);
321
-		foreach ($keys['keys'] as $uid => $keyFile) {
322
-			$this->setShareKey($path, $uid, $keyFile);
323
-		}
324
-	}
325
-
326
-	/**
327
-	 * write share key to the key storage
328
-	 *
329
-	 * @param string $path
330
-	 * @param string $uid
331
-	 * @param string $key
332
-	 * @return boolean
333
-	 */
334
-	public function setShareKey($path, $uid, $key) {
335
-		$keyId = $uid . '.' . $this->shareKeyId;
336
-		return $this->keyStorage->setFileKey($path, $keyId, $key, Encryption::ID);
337
-	}
338
-
339
-	/**
340
-	 * Decrypt private key and store it
341
-	 *
342
-	 * @param string $uid user id
343
-	 * @param string $passPhrase users password
344
-	 * @return boolean
345
-	 */
346
-	public function init($uid, $passPhrase) {
347
-
348
-		$this->session->setStatus(Session::INIT_EXECUTED);
349
-
350
-		try {
351
-			if($this->util->isMasterKeyEnabled()) {
352
-				$uid = $this->getMasterKeyId();
353
-				$passPhrase = $this->getMasterKeyPassword();
354
-				$privateKey = $this->getSystemPrivateKey($uid);
355
-			} else {
356
-				$privateKey = $this->getPrivateKey($uid);
357
-			}
358
-			$privateKey = $this->crypt->decryptPrivateKey($privateKey, $passPhrase, $uid);
359
-		} catch (PrivateKeyMissingException $e) {
360
-			return false;
361
-		} catch (DecryptionFailedException $e) {
362
-			return false;
363
-		} catch (\Exception $e) {
364
-			$this->log->warning(
365
-				'Could not decrypt the private key from user "' . $uid . '"" during login. ' .
366
-				'Assume password change on the user back-end. Error message: '
367
-				. $e->getMessage()
368
-			);
369
-			return false;
370
-		}
371
-
372
-		if ($privateKey) {
373
-			$this->session->setPrivateKey($privateKey);
374
-			$this->session->setStatus(Session::INIT_SUCCESSFUL);
375
-			return true;
376
-		}
377
-
378
-		return false;
379
-	}
380
-
381
-	/**
382
-	 * @param $userId
383
-	 * @return string
384
-	 * @throws PrivateKeyMissingException
385
-	 */
386
-	public function getPrivateKey($userId) {
387
-		$privateKey = $this->keyStorage->getUserKey($userId,
388
-			$this->privateKeyId, Encryption::ID);
389
-
390
-		if (strlen($privateKey) !== 0) {
391
-			return $privateKey;
392
-		}
393
-		throw new PrivateKeyMissingException($userId);
394
-	}
395
-
396
-	/**
397
-	 * @param string $path
398
-	 * @param $uid
399
-	 * @return string
400
-	 */
401
-	public function getFileKey($path, $uid) {
402
-		if ($uid === '') {
403
-			$uid = null;
404
-		}
405
-		$publicAccess = is_null($uid);
406
-		$encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID);
407
-
408
-		if (empty($encryptedFileKey)) {
409
-			return '';
410
-		}
411
-
412
-		if ($this->util->isMasterKeyEnabled()) {
413
-			$uid = $this->getMasterKeyId();
414
-			$shareKey = $this->getShareKey($path, $uid);
415
-			if ($publicAccess) {
416
-				$privateKey = $this->getSystemPrivateKey($uid);
417
-				$privateKey = $this->crypt->decryptPrivateKey($privateKey, $this->getMasterKeyPassword(), $uid);
418
-			} else {
419
-				// when logged in, the master key is already decrypted in the session
420
-				$privateKey = $this->session->getPrivateKey();
421
-			}
422
-		} else if ($publicAccess) {
423
-			// use public share key for public links
424
-			$uid = $this->getPublicShareKeyId();
425
-			$shareKey = $this->getShareKey($path, $uid);
426
-			$privateKey = $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.privateKey', Encryption::ID);
427
-			$privateKey = $this->crypt->decryptPrivateKey($privateKey);
428
-		} else {
429
-			$shareKey = $this->getShareKey($path, $uid);
430
-			$privateKey = $this->session->getPrivateKey();
431
-		}
432
-
433
-		if ($encryptedFileKey && $shareKey && $privateKey) {
434
-			return $this->crypt->multiKeyDecrypt($encryptedFileKey,
435
-				$shareKey,
436
-				$privateKey);
437
-		}
438
-
439
-		return '';
440
-	}
441
-
442
-	/**
443
-	 * Get the current version of a file
444
-	 *
445
-	 * @param string $path
446
-	 * @param View $view
447
-	 * @return int
448
-	 */
449
-	public function getVersion($path, View $view) {
450
-		$fileInfo = $view->getFileInfo($path);
451
-		if($fileInfo === false) {
452
-			return 0;
453
-		}
454
-		return $fileInfo->getEncryptedVersion();
455
-	}
456
-
457
-	/**
458
-	 * Set the current version of a file
459
-	 *
460
-	 * @param string $path
461
-	 * @param int $version
462
-	 * @param View $view
463
-	 */
464
-	public function setVersion($path, $version, View $view) {
465
-		$fileInfo= $view->getFileInfo($path);
466
-
467
-		if($fileInfo !== false) {
468
-			$cache = $fileInfo->getStorage()->getCache();
469
-			$cache->update($fileInfo->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
470
-		}
471
-	}
472
-
473
-	/**
474
-	 * get the encrypted file key
475
-	 *
476
-	 * @param string $path
477
-	 * @return string
478
-	 */
479
-	public function getEncryptedFileKey($path) {
480
-		$encryptedFileKey = $this->keyStorage->getFileKey($path,
481
-			$this->fileKeyId, Encryption::ID);
482
-
483
-		return $encryptedFileKey;
484
-	}
485
-
486
-	/**
487
-	 * delete share key
488
-	 *
489
-	 * @param string $path
490
-	 * @param string $keyId
491
-	 * @return boolean
492
-	 */
493
-	public function deleteShareKey($path, $keyId) {
494
-		return $this->keyStorage->deleteFileKey(
495
-			$path,
496
-			$keyId . '.' . $this->shareKeyId,
497
-			Encryption::ID);
498
-	}
499
-
500
-
501
-	/**
502
-	 * @param $path
503
-	 * @param $uid
504
-	 * @return mixed
505
-	 */
506
-	public function getShareKey($path, $uid) {
507
-		$keyId = $uid . '.' . $this->shareKeyId;
508
-		return $this->keyStorage->getFileKey($path, $keyId, Encryption::ID);
509
-	}
510
-
511
-	/**
512
-	 * check if user has a private and a public key
513
-	 *
514
-	 * @param string $userId
515
-	 * @return bool
516
-	 * @throws PrivateKeyMissingException
517
-	 * @throws PublicKeyMissingException
518
-	 */
519
-	public function userHasKeys($userId) {
520
-		$privateKey = $publicKey = true;
521
-		$exception = null;
522
-
523
-		try {
524
-			$this->getPrivateKey($userId);
525
-		} catch (PrivateKeyMissingException $e) {
526
-			$privateKey = false;
527
-			$exception = $e;
528
-		}
529
-		try {
530
-			$this->getPublicKey($userId);
531
-		} catch (PublicKeyMissingException $e) {
532
-			$publicKey = false;
533
-			$exception = $e;
534
-		}
535
-
536
-		if ($privateKey && $publicKey) {
537
-			return true;
538
-		} elseif (!$privateKey && !$publicKey) {
539
-			return false;
540
-		} else {
541
-			throw $exception;
542
-		}
543
-	}
544
-
545
-	/**
546
-	 * @param $userId
547
-	 * @return mixed
548
-	 * @throws PublicKeyMissingException
549
-	 */
550
-	public function getPublicKey($userId) {
551
-		$publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId, Encryption::ID);
552
-
553
-		if (strlen($publicKey) !== 0) {
554
-			return $publicKey;
555
-		}
556
-		throw new PublicKeyMissingException($userId);
557
-	}
558
-
559
-	public function getPublicShareKeyId() {
560
-		return $this->publicShareKeyId;
561
-	}
562
-
563
-	/**
564
-	 * get public key for public link shares
565
-	 *
566
-	 * @return string
567
-	 */
568
-	public function getPublicShareKey() {
569
-		return $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.publicKey', Encryption::ID);
570
-	}
571
-
572
-	/**
573
-	 * @param string $purpose
574
-	 * @param string $uid
575
-	 */
576
-	public function backupUserKeys($purpose, $uid) {
577
-		$this->keyStorage->backupUserKeys(Encryption::ID, $purpose, $uid);
578
-	}
579
-
580
-	/**
581
-	 * creat a backup of the users private and public key and then  delete it
582
-	 *
583
-	 * @param string $uid
584
-	 */
585
-	public function deleteUserKeys($uid) {
586
-		$this->deletePublicKey($uid);
587
-		$this->deletePrivateKey($uid);
588
-	}
589
-
590
-	/**
591
-	 * @param $uid
592
-	 * @return bool
593
-	 */
594
-	public function deletePublicKey($uid) {
595
-		return $this->keyStorage->deleteUserKey($uid, $this->publicKeyId, Encryption::ID);
596
-	}
597
-
598
-	/**
599
-	 * @param string $uid
600
-	 * @return bool
601
-	 */
602
-	private function deletePrivateKey($uid) {
603
-		return $this->keyStorage->deleteUserKey($uid, $this->privateKeyId, Encryption::ID);
604
-	}
605
-
606
-	/**
607
-	 * @param string $path
608
-	 * @return bool
609
-	 */
610
-	public function deleteAllFileKeys($path) {
611
-		return $this->keyStorage->deleteAllFileKeys($path);
612
-	}
613
-
614
-	/**
615
-	 * @param array $userIds
616
-	 * @return array
617
-	 * @throws PublicKeyMissingException
618
-	 */
619
-	public function getPublicKeys(array $userIds) {
620
-		$keys = [];
621
-
622
-		foreach ($userIds as $userId) {
623
-			try {
624
-				$keys[$userId] = $this->getPublicKey($userId);
625
-			} catch (PublicKeyMissingException $e) {
626
-				continue;
627
-			}
628
-		}
629
-
630
-		return $keys;
631
-
632
-	}
633
-
634
-	/**
635
-	 * @param string $keyId
636
-	 * @return string returns openssl key
637
-	 */
638
-	public function getSystemPrivateKey($keyId) {
639
-		return $this->keyStorage->getSystemUserKey($keyId . '.' . $this->privateKeyId, Encryption::ID);
640
-	}
641
-
642
-	/**
643
-	 * @param string $keyId
644
-	 * @param string $key
645
-	 * @return string returns openssl key
646
-	 */
647
-	public function setSystemPrivateKey($keyId, $key) {
648
-		return $this->keyStorage->setSystemUserKey(
649
-			$keyId . '.' . $this->privateKeyId,
650
-			$key,
651
-			Encryption::ID);
652
-	}
653
-
654
-	/**
655
-	 * add system keys such as the public share key and the recovery key
656
-	 *
657
-	 * @param array $accessList
658
-	 * @param array $publicKeys
659
-	 * @param string $uid
660
-	 * @return array
661
-	 * @throws PublicKeyMissingException
662
-	 */
663
-	public function addSystemKeys(array $accessList, array $publicKeys, $uid) {
664
-		if (!empty($accessList['public'])) {
665
-			$publicShareKey = $this->getPublicShareKey();
666
-			if (empty($publicShareKey)) {
667
-				throw new PublicKeyMissingException($this->getPublicShareKeyId());
668
-			}
669
-			$publicKeys[$this->getPublicShareKeyId()] = $publicShareKey;
670
-		}
671
-
672
-		if ($this->recoveryKeyExists() &&
673
-			$this->util->isRecoveryEnabledForUser($uid)) {
674
-
675
-			$publicKeys[$this->getRecoveryKeyId()] = $this->getRecoveryKey();
676
-		}
677
-
678
-		return $publicKeys;
679
-	}
680
-
681
-	/**
682
-	 * get master key password
683
-	 *
684
-	 * @return string
685
-	 * @throws \Exception
686
-	 */
687
-	public function getMasterKeyPassword() {
688
-		$password = $this->config->getSystemValue('secret');
689
-		if (empty($password)){
690
-			throw new \Exception('Can not get secret from Nextcloud instance');
691
-		}
692
-
693
-		return $password;
694
-	}
695
-
696
-	/**
697
-	 * return master key id
698
-	 *
699
-	 * @return string
700
-	 */
701
-	public function getMasterKeyId() {
702
-		return $this->masterKeyId;
703
-	}
704
-
705
-	/**
706
-	 * get public master key
707
-	 *
708
-	 * @return string
709
-	 */
710
-	public function getPublicMasterKey() {
711
-		return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.publicKey', Encryption::ID);
712
-	}
41
+    /**
42
+     * @var Session
43
+     */
44
+    protected $session;
45
+    /**
46
+     * @var IStorage
47
+     */
48
+    private $keyStorage;
49
+    /**
50
+     * @var Crypt
51
+     */
52
+    private $crypt;
53
+    /**
54
+     * @var string
55
+     */
56
+    private $recoveryKeyId;
57
+    /**
58
+     * @var string
59
+     */
60
+    private $publicShareKeyId;
61
+    /**
62
+     * @var string
63
+     */
64
+    private $masterKeyId;
65
+    /**
66
+     * @var string UserID
67
+     */
68
+    private $keyId;
69
+    /**
70
+     * @var string
71
+     */
72
+    private $publicKeyId = 'publicKey';
73
+    /**
74
+     * @var string
75
+     */
76
+    private $privateKeyId = 'privateKey';
77
+
78
+    /**
79
+     * @var string
80
+     */
81
+    private $shareKeyId = 'shareKey';
82
+
83
+    /**
84
+     * @var string
85
+     */
86
+    private $fileKeyId = 'fileKey';
87
+    /**
88
+     * @var IConfig
89
+     */
90
+    private $config;
91
+    /**
92
+     * @var ILogger
93
+     */
94
+    private $log;
95
+    /**
96
+     * @var Util
97
+     */
98
+    private $util;
99
+
100
+    /**
101
+     * @param IStorage $keyStorage
102
+     * @param Crypt $crypt
103
+     * @param IConfig $config
104
+     * @param IUserSession $userSession
105
+     * @param Session $session
106
+     * @param ILogger $log
107
+     * @param Util $util
108
+     */
109
+    public function __construct(
110
+        IStorage $keyStorage,
111
+        Crypt $crypt,
112
+        IConfig $config,
113
+        IUserSession $userSession,
114
+        Session $session,
115
+        ILogger $log,
116
+        Util $util
117
+    ) {
118
+
119
+        $this->util = $util;
120
+        $this->session = $session;
121
+        $this->keyStorage = $keyStorage;
122
+        $this->crypt = $crypt;
123
+        $this->config = $config;
124
+        $this->log = $log;
125
+
126
+        $this->recoveryKeyId = $this->config->getAppValue('encryption',
127
+            'recoveryKeyId');
128
+        if (empty($this->recoveryKeyId)) {
129
+            $this->recoveryKeyId = 'recoveryKey_' . substr(md5(time()), 0, 8);
130
+            $this->config->setAppValue('encryption',
131
+                'recoveryKeyId',
132
+                $this->recoveryKeyId);
133
+        }
134
+
135
+        $this->publicShareKeyId = $this->config->getAppValue('encryption',
136
+            'publicShareKeyId');
137
+        if (empty($this->publicShareKeyId)) {
138
+            $this->publicShareKeyId = 'pubShare_' . substr(md5(time()), 0, 8);
139
+            $this->config->setAppValue('encryption', 'publicShareKeyId', $this->publicShareKeyId);
140
+        }
141
+
142
+        $this->masterKeyId = $this->config->getAppValue('encryption',
143
+            'masterKeyId');
144
+        if (empty($this->masterKeyId)) {
145
+            $this->masterKeyId = 'master_' . substr(md5(time()), 0, 8);
146
+            $this->config->setAppValue('encryption', 'masterKeyId', $this->masterKeyId);
147
+        }
148
+
149
+        $this->keyId = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false;
150
+        $this->log = $log;
151
+    }
152
+
153
+    /**
154
+     * check if key pair for public link shares exists, if not we create one
155
+     */
156
+    public function validateShareKey() {
157
+        $shareKey = $this->getPublicShareKey();
158
+        if (empty($shareKey)) {
159
+            $keyPair = $this->crypt->createKeyPair();
160
+
161
+            // Save public key
162
+            $this->keyStorage->setSystemUserKey(
163
+                $this->publicShareKeyId . '.publicKey', $keyPair['publicKey'],
164
+                Encryption::ID);
165
+
166
+            // Encrypt private key empty passphrase
167
+            $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], '');
168
+            $header = $this->crypt->generateHeader();
169
+            $this->setSystemPrivateKey($this->publicShareKeyId, $header . $encryptedKey);
170
+        }
171
+    }
172
+
173
+    /**
174
+     * check if a key pair for the master key exists, if not we create one
175
+     */
176
+    public function validateMasterKey() {
177
+
178
+        if ($this->util->isMasterKeyEnabled() === false) {
179
+            return;
180
+        }
181
+
182
+        $masterKey = $this->getPublicMasterKey();
183
+        if (empty($masterKey)) {
184
+            $keyPair = $this->crypt->createKeyPair();
185
+
186
+            // Save public key
187
+            $this->keyStorage->setSystemUserKey(
188
+                $this->masterKeyId . '.publicKey', $keyPair['publicKey'],
189
+                Encryption::ID);
190
+
191
+            // Encrypt private key with system password
192
+            $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $this->getMasterKeyPassword(), $this->masterKeyId);
193
+            $header = $this->crypt->generateHeader();
194
+            $this->setSystemPrivateKey($this->masterKeyId, $header . $encryptedKey);
195
+        }
196
+    }
197
+
198
+    /**
199
+     * @return bool
200
+     */
201
+    public function recoveryKeyExists() {
202
+        $key = $this->getRecoveryKey();
203
+        return (!empty($key));
204
+    }
205
+
206
+    /**
207
+     * get recovery key
208
+     *
209
+     * @return string
210
+     */
211
+    public function getRecoveryKey() {
212
+        return $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.publicKey', Encryption::ID);
213
+    }
214
+
215
+    /**
216
+     * get recovery key ID
217
+     *
218
+     * @return string
219
+     */
220
+    public function getRecoveryKeyId() {
221
+        return $this->recoveryKeyId;
222
+    }
223
+
224
+    /**
225
+     * @param string $password
226
+     * @return bool
227
+     */
228
+    public function checkRecoveryPassword($password) {
229
+        $recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId . '.privateKey', Encryption::ID);
230
+        $decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
231
+
232
+        if ($decryptedRecoveryKey) {
233
+            return true;
234
+        }
235
+        return false;
236
+    }
237
+
238
+    /**
239
+     * @param string $uid
240
+     * @param string $password
241
+     * @param string $keyPair
242
+     * @return bool
243
+     */
244
+    public function storeKeyPair($uid, $password, $keyPair) {
245
+        // Save Public Key
246
+        $this->setPublicKey($uid, $keyPair['publicKey']);
247
+
248
+        $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password, $uid);
249
+
250
+        $header = $this->crypt->generateHeader();
251
+
252
+        if ($encryptedKey) {
253
+            $this->setPrivateKey($uid, $header . $encryptedKey);
254
+            return true;
255
+        }
256
+        return false;
257
+    }
258
+
259
+    /**
260
+     * @param string $password
261
+     * @param array $keyPair
262
+     * @return bool
263
+     */
264
+    public function setRecoveryKey($password, $keyPair) {
265
+        // Save Public Key
266
+        $this->keyStorage->setSystemUserKey($this->getRecoveryKeyId().
267
+            '.publicKey',
268
+            $keyPair['publicKey'],
269
+            Encryption::ID);
270
+
271
+        $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password);
272
+        $header = $this->crypt->generateHeader();
273
+
274
+        if ($encryptedKey) {
275
+            $this->setSystemPrivateKey($this->getRecoveryKeyId(), $header . $encryptedKey);
276
+            return true;
277
+        }
278
+        return false;
279
+    }
280
+
281
+    /**
282
+     * @param $userId
283
+     * @param $key
284
+     * @return bool
285
+     */
286
+    public function setPublicKey($userId, $key) {
287
+        return $this->keyStorage->setUserKey($userId, $this->publicKeyId, $key, Encryption::ID);
288
+    }
289
+
290
+    /**
291
+     * @param $userId
292
+     * @param string $key
293
+     * @return bool
294
+     */
295
+    public function setPrivateKey($userId, $key) {
296
+        return $this->keyStorage->setUserKey($userId,
297
+            $this->privateKeyId,
298
+            $key,
299
+            Encryption::ID);
300
+    }
301
+
302
+    /**
303
+     * write file key to key storage
304
+     *
305
+     * @param string $path
306
+     * @param string $key
307
+     * @return boolean
308
+     */
309
+    public function setFileKey($path, $key) {
310
+        return $this->keyStorage->setFileKey($path, $this->fileKeyId, $key, Encryption::ID);
311
+    }
312
+
313
+    /**
314
+     * set all file keys (the file key and the corresponding share keys)
315
+     *
316
+     * @param string $path
317
+     * @param array $keys
318
+     */
319
+    public function setAllFileKeys($path, $keys) {
320
+        $this->setFileKey($path, $keys['data']);
321
+        foreach ($keys['keys'] as $uid => $keyFile) {
322
+            $this->setShareKey($path, $uid, $keyFile);
323
+        }
324
+    }
325
+
326
+    /**
327
+     * write share key to the key storage
328
+     *
329
+     * @param string $path
330
+     * @param string $uid
331
+     * @param string $key
332
+     * @return boolean
333
+     */
334
+    public function setShareKey($path, $uid, $key) {
335
+        $keyId = $uid . '.' . $this->shareKeyId;
336
+        return $this->keyStorage->setFileKey($path, $keyId, $key, Encryption::ID);
337
+    }
338
+
339
+    /**
340
+     * Decrypt private key and store it
341
+     *
342
+     * @param string $uid user id
343
+     * @param string $passPhrase users password
344
+     * @return boolean
345
+     */
346
+    public function init($uid, $passPhrase) {
347
+
348
+        $this->session->setStatus(Session::INIT_EXECUTED);
349
+
350
+        try {
351
+            if($this->util->isMasterKeyEnabled()) {
352
+                $uid = $this->getMasterKeyId();
353
+                $passPhrase = $this->getMasterKeyPassword();
354
+                $privateKey = $this->getSystemPrivateKey($uid);
355
+            } else {
356
+                $privateKey = $this->getPrivateKey($uid);
357
+            }
358
+            $privateKey = $this->crypt->decryptPrivateKey($privateKey, $passPhrase, $uid);
359
+        } catch (PrivateKeyMissingException $e) {
360
+            return false;
361
+        } catch (DecryptionFailedException $e) {
362
+            return false;
363
+        } catch (\Exception $e) {
364
+            $this->log->warning(
365
+                'Could not decrypt the private key from user "' . $uid . '"" during login. ' .
366
+                'Assume password change on the user back-end. Error message: '
367
+                . $e->getMessage()
368
+            );
369
+            return false;
370
+        }
371
+
372
+        if ($privateKey) {
373
+            $this->session->setPrivateKey($privateKey);
374
+            $this->session->setStatus(Session::INIT_SUCCESSFUL);
375
+            return true;
376
+        }
377
+
378
+        return false;
379
+    }
380
+
381
+    /**
382
+     * @param $userId
383
+     * @return string
384
+     * @throws PrivateKeyMissingException
385
+     */
386
+    public function getPrivateKey($userId) {
387
+        $privateKey = $this->keyStorage->getUserKey($userId,
388
+            $this->privateKeyId, Encryption::ID);
389
+
390
+        if (strlen($privateKey) !== 0) {
391
+            return $privateKey;
392
+        }
393
+        throw new PrivateKeyMissingException($userId);
394
+    }
395
+
396
+    /**
397
+     * @param string $path
398
+     * @param $uid
399
+     * @return string
400
+     */
401
+    public function getFileKey($path, $uid) {
402
+        if ($uid === '') {
403
+            $uid = null;
404
+        }
405
+        $publicAccess = is_null($uid);
406
+        $encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID);
407
+
408
+        if (empty($encryptedFileKey)) {
409
+            return '';
410
+        }
411
+
412
+        if ($this->util->isMasterKeyEnabled()) {
413
+            $uid = $this->getMasterKeyId();
414
+            $shareKey = $this->getShareKey($path, $uid);
415
+            if ($publicAccess) {
416
+                $privateKey = $this->getSystemPrivateKey($uid);
417
+                $privateKey = $this->crypt->decryptPrivateKey($privateKey, $this->getMasterKeyPassword(), $uid);
418
+            } else {
419
+                // when logged in, the master key is already decrypted in the session
420
+                $privateKey = $this->session->getPrivateKey();
421
+            }
422
+        } else if ($publicAccess) {
423
+            // use public share key for public links
424
+            $uid = $this->getPublicShareKeyId();
425
+            $shareKey = $this->getShareKey($path, $uid);
426
+            $privateKey = $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.privateKey', Encryption::ID);
427
+            $privateKey = $this->crypt->decryptPrivateKey($privateKey);
428
+        } else {
429
+            $shareKey = $this->getShareKey($path, $uid);
430
+            $privateKey = $this->session->getPrivateKey();
431
+        }
432
+
433
+        if ($encryptedFileKey && $shareKey && $privateKey) {
434
+            return $this->crypt->multiKeyDecrypt($encryptedFileKey,
435
+                $shareKey,
436
+                $privateKey);
437
+        }
438
+
439
+        return '';
440
+    }
441
+
442
+    /**
443
+     * Get the current version of a file
444
+     *
445
+     * @param string $path
446
+     * @param View $view
447
+     * @return int
448
+     */
449
+    public function getVersion($path, View $view) {
450
+        $fileInfo = $view->getFileInfo($path);
451
+        if($fileInfo === false) {
452
+            return 0;
453
+        }
454
+        return $fileInfo->getEncryptedVersion();
455
+    }
456
+
457
+    /**
458
+     * Set the current version of a file
459
+     *
460
+     * @param string $path
461
+     * @param int $version
462
+     * @param View $view
463
+     */
464
+    public function setVersion($path, $version, View $view) {
465
+        $fileInfo= $view->getFileInfo($path);
466
+
467
+        if($fileInfo !== false) {
468
+            $cache = $fileInfo->getStorage()->getCache();
469
+            $cache->update($fileInfo->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
470
+        }
471
+    }
472
+
473
+    /**
474
+     * get the encrypted file key
475
+     *
476
+     * @param string $path
477
+     * @return string
478
+     */
479
+    public function getEncryptedFileKey($path) {
480
+        $encryptedFileKey = $this->keyStorage->getFileKey($path,
481
+            $this->fileKeyId, Encryption::ID);
482
+
483
+        return $encryptedFileKey;
484
+    }
485
+
486
+    /**
487
+     * delete share key
488
+     *
489
+     * @param string $path
490
+     * @param string $keyId
491
+     * @return boolean
492
+     */
493
+    public function deleteShareKey($path, $keyId) {
494
+        return $this->keyStorage->deleteFileKey(
495
+            $path,
496
+            $keyId . '.' . $this->shareKeyId,
497
+            Encryption::ID);
498
+    }
499
+
500
+
501
+    /**
502
+     * @param $path
503
+     * @param $uid
504
+     * @return mixed
505
+     */
506
+    public function getShareKey($path, $uid) {
507
+        $keyId = $uid . '.' . $this->shareKeyId;
508
+        return $this->keyStorage->getFileKey($path, $keyId, Encryption::ID);
509
+    }
510
+
511
+    /**
512
+     * check if user has a private and a public key
513
+     *
514
+     * @param string $userId
515
+     * @return bool
516
+     * @throws PrivateKeyMissingException
517
+     * @throws PublicKeyMissingException
518
+     */
519
+    public function userHasKeys($userId) {
520
+        $privateKey = $publicKey = true;
521
+        $exception = null;
522
+
523
+        try {
524
+            $this->getPrivateKey($userId);
525
+        } catch (PrivateKeyMissingException $e) {
526
+            $privateKey = false;
527
+            $exception = $e;
528
+        }
529
+        try {
530
+            $this->getPublicKey($userId);
531
+        } catch (PublicKeyMissingException $e) {
532
+            $publicKey = false;
533
+            $exception = $e;
534
+        }
535
+
536
+        if ($privateKey && $publicKey) {
537
+            return true;
538
+        } elseif (!$privateKey && !$publicKey) {
539
+            return false;
540
+        } else {
541
+            throw $exception;
542
+        }
543
+    }
544
+
545
+    /**
546
+     * @param $userId
547
+     * @return mixed
548
+     * @throws PublicKeyMissingException
549
+     */
550
+    public function getPublicKey($userId) {
551
+        $publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId, Encryption::ID);
552
+
553
+        if (strlen($publicKey) !== 0) {
554
+            return $publicKey;
555
+        }
556
+        throw new PublicKeyMissingException($userId);
557
+    }
558
+
559
+    public function getPublicShareKeyId() {
560
+        return $this->publicShareKeyId;
561
+    }
562
+
563
+    /**
564
+     * get public key for public link shares
565
+     *
566
+     * @return string
567
+     */
568
+    public function getPublicShareKey() {
569
+        return $this->keyStorage->getSystemUserKey($this->publicShareKeyId . '.publicKey', Encryption::ID);
570
+    }
571
+
572
+    /**
573
+     * @param string $purpose
574
+     * @param string $uid
575
+     */
576
+    public function backupUserKeys($purpose, $uid) {
577
+        $this->keyStorage->backupUserKeys(Encryption::ID, $purpose, $uid);
578
+    }
579
+
580
+    /**
581
+     * creat a backup of the users private and public key and then  delete it
582
+     *
583
+     * @param string $uid
584
+     */
585
+    public function deleteUserKeys($uid) {
586
+        $this->deletePublicKey($uid);
587
+        $this->deletePrivateKey($uid);
588
+    }
589
+
590
+    /**
591
+     * @param $uid
592
+     * @return bool
593
+     */
594
+    public function deletePublicKey($uid) {
595
+        return $this->keyStorage->deleteUserKey($uid, $this->publicKeyId, Encryption::ID);
596
+    }
597
+
598
+    /**
599
+     * @param string $uid
600
+     * @return bool
601
+     */
602
+    private function deletePrivateKey($uid) {
603
+        return $this->keyStorage->deleteUserKey($uid, $this->privateKeyId, Encryption::ID);
604
+    }
605
+
606
+    /**
607
+     * @param string $path
608
+     * @return bool
609
+     */
610
+    public function deleteAllFileKeys($path) {
611
+        return $this->keyStorage->deleteAllFileKeys($path);
612
+    }
613
+
614
+    /**
615
+     * @param array $userIds
616
+     * @return array
617
+     * @throws PublicKeyMissingException
618
+     */
619
+    public function getPublicKeys(array $userIds) {
620
+        $keys = [];
621
+
622
+        foreach ($userIds as $userId) {
623
+            try {
624
+                $keys[$userId] = $this->getPublicKey($userId);
625
+            } catch (PublicKeyMissingException $e) {
626
+                continue;
627
+            }
628
+        }
629
+
630
+        return $keys;
631
+
632
+    }
633
+
634
+    /**
635
+     * @param string $keyId
636
+     * @return string returns openssl key
637
+     */
638
+    public function getSystemPrivateKey($keyId) {
639
+        return $this->keyStorage->getSystemUserKey($keyId . '.' . $this->privateKeyId, Encryption::ID);
640
+    }
641
+
642
+    /**
643
+     * @param string $keyId
644
+     * @param string $key
645
+     * @return string returns openssl key
646
+     */
647
+    public function setSystemPrivateKey($keyId, $key) {
648
+        return $this->keyStorage->setSystemUserKey(
649
+            $keyId . '.' . $this->privateKeyId,
650
+            $key,
651
+            Encryption::ID);
652
+    }
653
+
654
+    /**
655
+     * add system keys such as the public share key and the recovery key
656
+     *
657
+     * @param array $accessList
658
+     * @param array $publicKeys
659
+     * @param string $uid
660
+     * @return array
661
+     * @throws PublicKeyMissingException
662
+     */
663
+    public function addSystemKeys(array $accessList, array $publicKeys, $uid) {
664
+        if (!empty($accessList['public'])) {
665
+            $publicShareKey = $this->getPublicShareKey();
666
+            if (empty($publicShareKey)) {
667
+                throw new PublicKeyMissingException($this->getPublicShareKeyId());
668
+            }
669
+            $publicKeys[$this->getPublicShareKeyId()] = $publicShareKey;
670
+        }
671
+
672
+        if ($this->recoveryKeyExists() &&
673
+            $this->util->isRecoveryEnabledForUser($uid)) {
674
+
675
+            $publicKeys[$this->getRecoveryKeyId()] = $this->getRecoveryKey();
676
+        }
677
+
678
+        return $publicKeys;
679
+    }
680
+
681
+    /**
682
+     * get master key password
683
+     *
684
+     * @return string
685
+     * @throws \Exception
686
+     */
687
+    public function getMasterKeyPassword() {
688
+        $password = $this->config->getSystemValue('secret');
689
+        if (empty($password)){
690
+            throw new \Exception('Can not get secret from Nextcloud instance');
691
+        }
692
+
693
+        return $password;
694
+    }
695
+
696
+    /**
697
+     * return master key id
698
+     *
699
+     * @return string
700
+     */
701
+    public function getMasterKeyId() {
702
+        return $this->masterKeyId;
703
+    }
704
+
705
+    /**
706
+     * get public master key
707
+     *
708
+     * @return string
709
+     */
710
+    public function getPublicMasterKey() {
711
+        return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.publicKey', Encryption::ID);
712
+    }
713 713
 }
Please login to merge, or discard this patch.
apps/federatedfilesharing/lib/FederatedShareProvider.php 1 patch
Indentation   +915 added lines, -915 removed lines patch added patch discarded remove patch
@@ -49,929 +49,929 @@
 block discarded – undo
49 49
  */
50 50
 class FederatedShareProvider implements IShareProvider {
51 51
 
52
-	const SHARE_TYPE_REMOTE = 6;
53
-
54
-	/** @var IDBConnection */
55
-	private $dbConnection;
56
-
57
-	/** @var AddressHandler */
58
-	private $addressHandler;
59
-
60
-	/** @var Notifications */
61
-	private $notifications;
62
-
63
-	/** @var TokenHandler */
64
-	private $tokenHandler;
65
-
66
-	/** @var IL10N */
67
-	private $l;
68
-
69
-	/** @var ILogger */
70
-	private $logger;
71
-
72
-	/** @var IRootFolder */
73
-	private $rootFolder;
74
-
75
-	/** @var IConfig */
76
-	private $config;
77
-
78
-	/** @var string */
79
-	private $externalShareTable = 'share_external';
80
-
81
-	/** @var IUserManager */
82
-	private $userManager;
83
-
84
-	/** @var ICloudIdManager */
85
-	private $cloudIdManager;
86
-
87
-	/**
88
-	 * DefaultShareProvider constructor.
89
-	 *
90
-	 * @param IDBConnection $connection
91
-	 * @param AddressHandler $addressHandler
92
-	 * @param Notifications $notifications
93
-	 * @param TokenHandler $tokenHandler
94
-	 * @param IL10N $l10n
95
-	 * @param ILogger $logger
96
-	 * @param IRootFolder $rootFolder
97
-	 * @param IConfig $config
98
-	 * @param IUserManager $userManager
99
-	 * @param ICloudIdManager $cloudIdManager
100
-	 */
101
-	public function __construct(
102
-			IDBConnection $connection,
103
-			AddressHandler $addressHandler,
104
-			Notifications $notifications,
105
-			TokenHandler $tokenHandler,
106
-			IL10N $l10n,
107
-			ILogger $logger,
108
-			IRootFolder $rootFolder,
109
-			IConfig $config,
110
-			IUserManager $userManager,
111
-			ICloudIdManager $cloudIdManager
112
-	) {
113
-		$this->dbConnection = $connection;
114
-		$this->addressHandler = $addressHandler;
115
-		$this->notifications = $notifications;
116
-		$this->tokenHandler = $tokenHandler;
117
-		$this->l = $l10n;
118
-		$this->logger = $logger;
119
-		$this->rootFolder = $rootFolder;
120
-		$this->config = $config;
121
-		$this->userManager = $userManager;
122
-		$this->cloudIdManager = $cloudIdManager;
123
-	}
124
-
125
-	/**
126
-	 * Return the identifier of this provider.
127
-	 *
128
-	 * @return string Containing only [a-zA-Z0-9]
129
-	 */
130
-	public function identifier() {
131
-		return 'ocFederatedSharing';
132
-	}
133
-
134
-	/**
135
-	 * Share a path
136
-	 *
137
-	 * @param IShare $share
138
-	 * @return IShare The share object
139
-	 * @throws ShareNotFound
140
-	 * @throws \Exception
141
-	 */
142
-	public function create(IShare $share) {
143
-
144
-		$shareWith = $share->getSharedWith();
145
-		$itemSource = $share->getNodeId();
146
-		$itemType = $share->getNodeType();
147
-		$permissions = $share->getPermissions();
148
-		$sharedBy = $share->getSharedBy();
149
-
150
-		/*
52
+    const SHARE_TYPE_REMOTE = 6;
53
+
54
+    /** @var IDBConnection */
55
+    private $dbConnection;
56
+
57
+    /** @var AddressHandler */
58
+    private $addressHandler;
59
+
60
+    /** @var Notifications */
61
+    private $notifications;
62
+
63
+    /** @var TokenHandler */
64
+    private $tokenHandler;
65
+
66
+    /** @var IL10N */
67
+    private $l;
68
+
69
+    /** @var ILogger */
70
+    private $logger;
71
+
72
+    /** @var IRootFolder */
73
+    private $rootFolder;
74
+
75
+    /** @var IConfig */
76
+    private $config;
77
+
78
+    /** @var string */
79
+    private $externalShareTable = 'share_external';
80
+
81
+    /** @var IUserManager */
82
+    private $userManager;
83
+
84
+    /** @var ICloudIdManager */
85
+    private $cloudIdManager;
86
+
87
+    /**
88
+     * DefaultShareProvider constructor.
89
+     *
90
+     * @param IDBConnection $connection
91
+     * @param AddressHandler $addressHandler
92
+     * @param Notifications $notifications
93
+     * @param TokenHandler $tokenHandler
94
+     * @param IL10N $l10n
95
+     * @param ILogger $logger
96
+     * @param IRootFolder $rootFolder
97
+     * @param IConfig $config
98
+     * @param IUserManager $userManager
99
+     * @param ICloudIdManager $cloudIdManager
100
+     */
101
+    public function __construct(
102
+            IDBConnection $connection,
103
+            AddressHandler $addressHandler,
104
+            Notifications $notifications,
105
+            TokenHandler $tokenHandler,
106
+            IL10N $l10n,
107
+            ILogger $logger,
108
+            IRootFolder $rootFolder,
109
+            IConfig $config,
110
+            IUserManager $userManager,
111
+            ICloudIdManager $cloudIdManager
112
+    ) {
113
+        $this->dbConnection = $connection;
114
+        $this->addressHandler = $addressHandler;
115
+        $this->notifications = $notifications;
116
+        $this->tokenHandler = $tokenHandler;
117
+        $this->l = $l10n;
118
+        $this->logger = $logger;
119
+        $this->rootFolder = $rootFolder;
120
+        $this->config = $config;
121
+        $this->userManager = $userManager;
122
+        $this->cloudIdManager = $cloudIdManager;
123
+    }
124
+
125
+    /**
126
+     * Return the identifier of this provider.
127
+     *
128
+     * @return string Containing only [a-zA-Z0-9]
129
+     */
130
+    public function identifier() {
131
+        return 'ocFederatedSharing';
132
+    }
133
+
134
+    /**
135
+     * Share a path
136
+     *
137
+     * @param IShare $share
138
+     * @return IShare The share object
139
+     * @throws ShareNotFound
140
+     * @throws \Exception
141
+     */
142
+    public function create(IShare $share) {
143
+
144
+        $shareWith = $share->getSharedWith();
145
+        $itemSource = $share->getNodeId();
146
+        $itemType = $share->getNodeType();
147
+        $permissions = $share->getPermissions();
148
+        $sharedBy = $share->getSharedBy();
149
+
150
+        /*
151 151
 		 * Check if file is not already shared with the remote user
152 152
 		 */
153
-		$alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0);
154
-		if (!empty($alreadyShared)) {
155
-			$message = 'Sharing %s failed, because this item is already shared with %s';
156
-			$message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith));
157
-			$this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
158
-			throw new \Exception($message_t);
159
-		}
160
-
161
-
162
-		// don't allow federated shares if source and target server are the same
163
-		$cloudId = $this->cloudIdManager->resolveCloudId($shareWith);
164
-		$currentServer = $this->addressHandler->generateRemoteURL();
165
-		$currentUser = $sharedBy;
166
-		if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) {
167
-			$message = 'Not allowed to create a federated share with the same user.';
168
-			$message_t = $this->l->t('Not allowed to create a federated share with the same user');
169
-			$this->logger->debug($message, ['app' => 'Federated File Sharing']);
170
-			throw new \Exception($message_t);
171
-		}
172
-
173
-
174
-		$share->setSharedWith($cloudId->getId());
175
-
176
-		try {
177
-			$remoteShare = $this->getShareFromExternalShareTable($share);
178
-		} catch (ShareNotFound $e) {
179
-			$remoteShare = null;
180
-		}
181
-
182
-		if ($remoteShare) {
183
-			try {
184
-				$ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
185
-				$shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time());
186
-				$share->setId($shareId);
187
-				list($token, $remoteId) = $this->askOwnerToReShare($shareWith, $share, $shareId);
188
-				// remote share was create successfully if we get a valid token as return
189
-				$send = is_string($token) && $token !== '';
190
-			} catch (\Exception $e) {
191
-				// fall back to old re-share behavior if the remote server
192
-				// doesn't support flat re-shares (was introduced with Nextcloud 9.1)
193
-				$this->removeShareFromTable($share);
194
-				$shareId = $this->createFederatedShare($share);
195
-			}
196
-			if ($send) {
197
-				$this->updateSuccessfulReshare($shareId, $token);
198
-				$this->storeRemoteId($shareId, $remoteId);
199
-			} else {
200
-				$this->removeShareFromTable($share);
201
-				$message_t = $this->l->t('File is already shared with %s', [$shareWith]);
202
-				throw new \Exception($message_t);
203
-			}
204
-
205
-		} else {
206
-			$shareId = $this->createFederatedShare($share);
207
-		}
208
-
209
-		$data = $this->getRawShare($shareId);
210
-		return $this->createShareObject($data);
211
-	}
212
-
213
-	/**
214
-	 * create federated share and inform the recipient
215
-	 *
216
-	 * @param IShare $share
217
-	 * @return int
218
-	 * @throws ShareNotFound
219
-	 * @throws \Exception
220
-	 */
221
-	protected function createFederatedShare(IShare $share) {
222
-		$token = $this->tokenHandler->generateToken();
223
-		$shareId = $this->addShareToDB(
224
-			$share->getNodeId(),
225
-			$share->getNodeType(),
226
-			$share->getSharedWith(),
227
-			$share->getSharedBy(),
228
-			$share->getShareOwner(),
229
-			$share->getPermissions(),
230
-			$token
231
-		);
232
-
233
-		$failure = false;
234
-
235
-		try {
236
-			$sharedByFederatedId = $share->getSharedBy();
237
-			if ($this->userManager->userExists($sharedByFederatedId)) {
238
-				$cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL());
239
-				$sharedByFederatedId = $cloudId->getId();
240
-			}
241
-			$ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL());
242
-			$send = $this->notifications->sendRemoteShare(
243
-				$token,
244
-				$share->getSharedWith(),
245
-				$share->getNode()->getName(),
246
-				$shareId,
247
-				$share->getShareOwner(),
248
-				$ownerCloudId->getId(),
249
-				$share->getSharedBy(),
250
-				$sharedByFederatedId
251
-			);
252
-
253
-			if ($send === false) {
254
-				$failure = true;
255
-			}
256
-		} catch (\Exception $e) {
257
-			$this->logger->error('Failed to notify remote server of federated share, removing share (' . $e->getMessage() . ')');
258
-			$failure = true;
259
-		}
260
-
261
-		if($failure) {
262
-			$this->removeShareFromTableById($shareId);
263
-			$message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable or uses a self-signed certificate.',
264
-				[$share->getNode()->getName(), $share->getSharedWith()]);
265
-			throw new \Exception($message_t);
266
-		}
267
-
268
-		return $shareId;
269
-
270
-	}
271
-
272
-	/**
273
-	 * @param string $shareWith
274
-	 * @param IShare $share
275
-	 * @param string $shareId internal share Id
276
-	 * @return array
277
-	 * @throws \Exception
278
-	 */
279
-	protected function askOwnerToReShare($shareWith, IShare $share, $shareId) {
280
-
281
-		$remoteShare = $this->getShareFromExternalShareTable($share);
282
-		$token = $remoteShare['share_token'];
283
-		$remoteId = $remoteShare['remote_id'];
284
-		$remote = $remoteShare['remote'];
285
-
286
-		list($token, $remoteId) = $this->notifications->requestReShare(
287
-			$token,
288
-			$remoteId,
289
-			$shareId,
290
-			$remote,
291
-			$shareWith,
292
-			$share->getPermissions()
293
-		);
294
-
295
-		return [$token, $remoteId];
296
-	}
297
-
298
-	/**
299
-	 * get federated share from the share_external table but exclude mounted link shares
300
-	 *
301
-	 * @param IShare $share
302
-	 * @return array
303
-	 * @throws ShareNotFound
304
-	 */
305
-	protected function getShareFromExternalShareTable(IShare $share) {
306
-		$query = $this->dbConnection->getQueryBuilder();
307
-		$query->select('*')->from($this->externalShareTable)
308
-			->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner())))
309
-			->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget())));
310
-		$result = $query->execute()->fetchAll();
311
-
312
-		if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) {
313
-			return $result[0];
314
-		}
315
-
316
-		throw new ShareNotFound('share not found in share_external table');
317
-	}
318
-
319
-	/**
320
-	 * add share to the database and return the ID
321
-	 *
322
-	 * @param int $itemSource
323
-	 * @param string $itemType
324
-	 * @param string $shareWith
325
-	 * @param string $sharedBy
326
-	 * @param string $uidOwner
327
-	 * @param int $permissions
328
-	 * @param string $token
329
-	 * @return int
330
-	 */
331
-	private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) {
332
-		$qb = $this->dbConnection->getQueryBuilder();
333
-		$qb->insert('share')
334
-			->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))
335
-			->setValue('item_type', $qb->createNamedParameter($itemType))
336
-			->setValue('item_source', $qb->createNamedParameter($itemSource))
337
-			->setValue('file_source', $qb->createNamedParameter($itemSource))
338
-			->setValue('share_with', $qb->createNamedParameter($shareWith))
339
-			->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
340
-			->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
341
-			->setValue('permissions', $qb->createNamedParameter($permissions))
342
-			->setValue('token', $qb->createNamedParameter($token))
343
-			->setValue('stime', $qb->createNamedParameter(time()));
344
-
345
-		/*
153
+        $alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0);
154
+        if (!empty($alreadyShared)) {
155
+            $message = 'Sharing %s failed, because this item is already shared with %s';
156
+            $message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith));
157
+            $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
158
+            throw new \Exception($message_t);
159
+        }
160
+
161
+
162
+        // don't allow federated shares if source and target server are the same
163
+        $cloudId = $this->cloudIdManager->resolveCloudId($shareWith);
164
+        $currentServer = $this->addressHandler->generateRemoteURL();
165
+        $currentUser = $sharedBy;
166
+        if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) {
167
+            $message = 'Not allowed to create a federated share with the same user.';
168
+            $message_t = $this->l->t('Not allowed to create a federated share with the same user');
169
+            $this->logger->debug($message, ['app' => 'Federated File Sharing']);
170
+            throw new \Exception($message_t);
171
+        }
172
+
173
+
174
+        $share->setSharedWith($cloudId->getId());
175
+
176
+        try {
177
+            $remoteShare = $this->getShareFromExternalShareTable($share);
178
+        } catch (ShareNotFound $e) {
179
+            $remoteShare = null;
180
+        }
181
+
182
+        if ($remoteShare) {
183
+            try {
184
+                $ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
185
+                $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time());
186
+                $share->setId($shareId);
187
+                list($token, $remoteId) = $this->askOwnerToReShare($shareWith, $share, $shareId);
188
+                // remote share was create successfully if we get a valid token as return
189
+                $send = is_string($token) && $token !== '';
190
+            } catch (\Exception $e) {
191
+                // fall back to old re-share behavior if the remote server
192
+                // doesn't support flat re-shares (was introduced with Nextcloud 9.1)
193
+                $this->removeShareFromTable($share);
194
+                $shareId = $this->createFederatedShare($share);
195
+            }
196
+            if ($send) {
197
+                $this->updateSuccessfulReshare($shareId, $token);
198
+                $this->storeRemoteId($shareId, $remoteId);
199
+            } else {
200
+                $this->removeShareFromTable($share);
201
+                $message_t = $this->l->t('File is already shared with %s', [$shareWith]);
202
+                throw new \Exception($message_t);
203
+            }
204
+
205
+        } else {
206
+            $shareId = $this->createFederatedShare($share);
207
+        }
208
+
209
+        $data = $this->getRawShare($shareId);
210
+        return $this->createShareObject($data);
211
+    }
212
+
213
+    /**
214
+     * create federated share and inform the recipient
215
+     *
216
+     * @param IShare $share
217
+     * @return int
218
+     * @throws ShareNotFound
219
+     * @throws \Exception
220
+     */
221
+    protected function createFederatedShare(IShare $share) {
222
+        $token = $this->tokenHandler->generateToken();
223
+        $shareId = $this->addShareToDB(
224
+            $share->getNodeId(),
225
+            $share->getNodeType(),
226
+            $share->getSharedWith(),
227
+            $share->getSharedBy(),
228
+            $share->getShareOwner(),
229
+            $share->getPermissions(),
230
+            $token
231
+        );
232
+
233
+        $failure = false;
234
+
235
+        try {
236
+            $sharedByFederatedId = $share->getSharedBy();
237
+            if ($this->userManager->userExists($sharedByFederatedId)) {
238
+                $cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL());
239
+                $sharedByFederatedId = $cloudId->getId();
240
+            }
241
+            $ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL());
242
+            $send = $this->notifications->sendRemoteShare(
243
+                $token,
244
+                $share->getSharedWith(),
245
+                $share->getNode()->getName(),
246
+                $shareId,
247
+                $share->getShareOwner(),
248
+                $ownerCloudId->getId(),
249
+                $share->getSharedBy(),
250
+                $sharedByFederatedId
251
+            );
252
+
253
+            if ($send === false) {
254
+                $failure = true;
255
+            }
256
+        } catch (\Exception $e) {
257
+            $this->logger->error('Failed to notify remote server of federated share, removing share (' . $e->getMessage() . ')');
258
+            $failure = true;
259
+        }
260
+
261
+        if($failure) {
262
+            $this->removeShareFromTableById($shareId);
263
+            $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable or uses a self-signed certificate.',
264
+                [$share->getNode()->getName(), $share->getSharedWith()]);
265
+            throw new \Exception($message_t);
266
+        }
267
+
268
+        return $shareId;
269
+
270
+    }
271
+
272
+    /**
273
+     * @param string $shareWith
274
+     * @param IShare $share
275
+     * @param string $shareId internal share Id
276
+     * @return array
277
+     * @throws \Exception
278
+     */
279
+    protected function askOwnerToReShare($shareWith, IShare $share, $shareId) {
280
+
281
+        $remoteShare = $this->getShareFromExternalShareTable($share);
282
+        $token = $remoteShare['share_token'];
283
+        $remoteId = $remoteShare['remote_id'];
284
+        $remote = $remoteShare['remote'];
285
+
286
+        list($token, $remoteId) = $this->notifications->requestReShare(
287
+            $token,
288
+            $remoteId,
289
+            $shareId,
290
+            $remote,
291
+            $shareWith,
292
+            $share->getPermissions()
293
+        );
294
+
295
+        return [$token, $remoteId];
296
+    }
297
+
298
+    /**
299
+     * get federated share from the share_external table but exclude mounted link shares
300
+     *
301
+     * @param IShare $share
302
+     * @return array
303
+     * @throws ShareNotFound
304
+     */
305
+    protected function getShareFromExternalShareTable(IShare $share) {
306
+        $query = $this->dbConnection->getQueryBuilder();
307
+        $query->select('*')->from($this->externalShareTable)
308
+            ->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner())))
309
+            ->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget())));
310
+        $result = $query->execute()->fetchAll();
311
+
312
+        if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) {
313
+            return $result[0];
314
+        }
315
+
316
+        throw new ShareNotFound('share not found in share_external table');
317
+    }
318
+
319
+    /**
320
+     * add share to the database and return the ID
321
+     *
322
+     * @param int $itemSource
323
+     * @param string $itemType
324
+     * @param string $shareWith
325
+     * @param string $sharedBy
326
+     * @param string $uidOwner
327
+     * @param int $permissions
328
+     * @param string $token
329
+     * @return int
330
+     */
331
+    private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) {
332
+        $qb = $this->dbConnection->getQueryBuilder();
333
+        $qb->insert('share')
334
+            ->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))
335
+            ->setValue('item_type', $qb->createNamedParameter($itemType))
336
+            ->setValue('item_source', $qb->createNamedParameter($itemSource))
337
+            ->setValue('file_source', $qb->createNamedParameter($itemSource))
338
+            ->setValue('share_with', $qb->createNamedParameter($shareWith))
339
+            ->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
340
+            ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
341
+            ->setValue('permissions', $qb->createNamedParameter($permissions))
342
+            ->setValue('token', $qb->createNamedParameter($token))
343
+            ->setValue('stime', $qb->createNamedParameter(time()));
344
+
345
+        /*
346 346
 		 * Added to fix https://github.com/owncloud/core/issues/22215
347 347
 		 * Can be removed once we get rid of ajax/share.php
348 348
 		 */
349
-		$qb->setValue('file_target', $qb->createNamedParameter(''));
350
-
351
-		$qb->execute();
352
-		$id = $qb->getLastInsertId();
353
-
354
-		return (int)$id;
355
-	}
356
-
357
-	/**
358
-	 * Update a share
359
-	 *
360
-	 * @param IShare $share
361
-	 * @return IShare The share object
362
-	 */
363
-	public function update(IShare $share) {
364
-		/*
349
+        $qb->setValue('file_target', $qb->createNamedParameter(''));
350
+
351
+        $qb->execute();
352
+        $id = $qb->getLastInsertId();
353
+
354
+        return (int)$id;
355
+    }
356
+
357
+    /**
358
+     * Update a share
359
+     *
360
+     * @param IShare $share
361
+     * @return IShare The share object
362
+     */
363
+    public function update(IShare $share) {
364
+        /*
365 365
 		 * We allow updating the permissions of federated shares
366 366
 		 */
367
-		$qb = $this->dbConnection->getQueryBuilder();
368
-			$qb->update('share')
369
-				->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
370
-				->set('permissions', $qb->createNamedParameter($share->getPermissions()))
371
-				->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
372
-				->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
373
-				->execute();
374
-
375
-		// send the updated permission to the owner/initiator, if they are not the same
376
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
377
-			$this->sendPermissionUpdate($share);
378
-		}
379
-
380
-		return $share;
381
-	}
382
-
383
-	/**
384
-	 * send the updated permission to the owner/initiator, if they are not the same
385
-	 *
386
-	 * @param IShare $share
387
-	 * @throws ShareNotFound
388
-	 * @throws \OC\HintException
389
-	 */
390
-	protected function sendPermissionUpdate(IShare $share) {
391
-		$remoteId = $this->getRemoteId($share);
392
-		// if the local user is the owner we send the permission change to the initiator
393
-		if ($this->userManager->userExists($share->getShareOwner())) {
394
-			list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
395
-		} else { // ... if not we send the permission change to the owner
396
-			list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
397
-		}
398
-		$this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions());
399
-	}
400
-
401
-
402
-	/**
403
-	 * update successful reShare with the correct token
404
-	 *
405
-	 * @param int $shareId
406
-	 * @param string $token
407
-	 */
408
-	protected function updateSuccessfulReShare($shareId, $token) {
409
-		$query = $this->dbConnection->getQueryBuilder();
410
-		$query->update('share')
411
-			->where($query->expr()->eq('id', $query->createNamedParameter($shareId)))
412
-			->set('token', $query->createNamedParameter($token))
413
-			->execute();
414
-	}
415
-
416
-	/**
417
-	 * store remote ID in federated reShare table
418
-	 *
419
-	 * @param $shareId
420
-	 * @param $remoteId
421
-	 */
422
-	public function storeRemoteId($shareId, $remoteId) {
423
-		$query = $this->dbConnection->getQueryBuilder();
424
-		$query->insert('federated_reshares')
425
-			->values(
426
-				[
427
-					'share_id' =>  $query->createNamedParameter($shareId),
428
-					'remote_id' => $query->createNamedParameter($remoteId),
429
-				]
430
-			);
431
-		$query->execute();
432
-	}
433
-
434
-	/**
435
-	 * get share ID on remote server for federated re-shares
436
-	 *
437
-	 * @param IShare $share
438
-	 * @return int
439
-	 * @throws ShareNotFound
440
-	 */
441
-	public function getRemoteId(IShare $share) {
442
-		$query = $this->dbConnection->getQueryBuilder();
443
-		$query->select('remote_id')->from('federated_reshares')
444
-			->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId())));
445
-		$data = $query->execute()->fetch();
446
-
447
-		if (!is_array($data) || !isset($data['remote_id'])) {
448
-			throw new ShareNotFound();
449
-		}
450
-
451
-		return (int)$data['remote_id'];
452
-	}
453
-
454
-	/**
455
-	 * @inheritdoc
456
-	 */
457
-	public function move(IShare $share, $recipient) {
458
-		/*
367
+        $qb = $this->dbConnection->getQueryBuilder();
368
+            $qb->update('share')
369
+                ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
370
+                ->set('permissions', $qb->createNamedParameter($share->getPermissions()))
371
+                ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
372
+                ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
373
+                ->execute();
374
+
375
+        // send the updated permission to the owner/initiator, if they are not the same
376
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
377
+            $this->sendPermissionUpdate($share);
378
+        }
379
+
380
+        return $share;
381
+    }
382
+
383
+    /**
384
+     * send the updated permission to the owner/initiator, if they are not the same
385
+     *
386
+     * @param IShare $share
387
+     * @throws ShareNotFound
388
+     * @throws \OC\HintException
389
+     */
390
+    protected function sendPermissionUpdate(IShare $share) {
391
+        $remoteId = $this->getRemoteId($share);
392
+        // if the local user is the owner we send the permission change to the initiator
393
+        if ($this->userManager->userExists($share->getShareOwner())) {
394
+            list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
395
+        } else { // ... if not we send the permission change to the owner
396
+            list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
397
+        }
398
+        $this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions());
399
+    }
400
+
401
+
402
+    /**
403
+     * update successful reShare with the correct token
404
+     *
405
+     * @param int $shareId
406
+     * @param string $token
407
+     */
408
+    protected function updateSuccessfulReShare($shareId, $token) {
409
+        $query = $this->dbConnection->getQueryBuilder();
410
+        $query->update('share')
411
+            ->where($query->expr()->eq('id', $query->createNamedParameter($shareId)))
412
+            ->set('token', $query->createNamedParameter($token))
413
+            ->execute();
414
+    }
415
+
416
+    /**
417
+     * store remote ID in federated reShare table
418
+     *
419
+     * @param $shareId
420
+     * @param $remoteId
421
+     */
422
+    public function storeRemoteId($shareId, $remoteId) {
423
+        $query = $this->dbConnection->getQueryBuilder();
424
+        $query->insert('federated_reshares')
425
+            ->values(
426
+                [
427
+                    'share_id' =>  $query->createNamedParameter($shareId),
428
+                    'remote_id' => $query->createNamedParameter($remoteId),
429
+                ]
430
+            );
431
+        $query->execute();
432
+    }
433
+
434
+    /**
435
+     * get share ID on remote server for federated re-shares
436
+     *
437
+     * @param IShare $share
438
+     * @return int
439
+     * @throws ShareNotFound
440
+     */
441
+    public function getRemoteId(IShare $share) {
442
+        $query = $this->dbConnection->getQueryBuilder();
443
+        $query->select('remote_id')->from('federated_reshares')
444
+            ->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId())));
445
+        $data = $query->execute()->fetch();
446
+
447
+        if (!is_array($data) || !isset($data['remote_id'])) {
448
+            throw new ShareNotFound();
449
+        }
450
+
451
+        return (int)$data['remote_id'];
452
+    }
453
+
454
+    /**
455
+     * @inheritdoc
456
+     */
457
+    public function move(IShare $share, $recipient) {
458
+        /*
459 459
 		 * This function does nothing yet as it is just for outgoing
460 460
 		 * federated shares.
461 461
 		 */
462
-		return $share;
463
-	}
464
-
465
-	/**
466
-	 * Get all children of this share
467
-	 *
468
-	 * @param IShare $parent
469
-	 * @return IShare[]
470
-	 */
471
-	public function getChildren(IShare $parent) {
472
-		$children = [];
473
-
474
-		$qb = $this->dbConnection->getQueryBuilder();
475
-		$qb->select('*')
476
-			->from('share')
477
-			->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
478
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
479
-			->orderBy('id');
480
-
481
-		$cursor = $qb->execute();
482
-		while($data = $cursor->fetch()) {
483
-			$children[] = $this->createShareObject($data);
484
-		}
485
-		$cursor->closeCursor();
486
-
487
-		return $children;
488
-	}
489
-
490
-	/**
491
-	 * Delete a share (owner unShares the file)
492
-	 *
493
-	 * @param IShare $share
494
-	 */
495
-	public function delete(IShare $share) {
496
-
497
-		list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith());
498
-
499
-		$isOwner = false;
500
-
501
-		$this->removeShareFromTable($share);
502
-
503
-		// if the local user is the owner we can send the unShare request directly...
504
-		if ($this->userManager->userExists($share->getShareOwner())) {
505
-			$this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken());
506
-			$this->revokeShare($share, true);
507
-			$isOwner = true;
508
-		} else { // ... if not we need to correct ID for the unShare request
509
-			$remoteId = $this->getRemoteId($share);
510
-			$this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken());
511
-			$this->revokeShare($share, false);
512
-		}
513
-
514
-		// send revoke notification to the other user, if initiator and owner are not the same user
515
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
516
-			$remoteId = $this->getRemoteId($share);
517
-			if ($isOwner) {
518
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
519
-			} else {
520
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
521
-			}
522
-			$this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
523
-		}
524
-	}
525
-
526
-	/**
527
-	 * in case of a re-share we need to send the other use (initiator or owner)
528
-	 * a message that the file was unshared
529
-	 *
530
-	 * @param IShare $share
531
-	 * @param bool $isOwner the user can either be the owner or the user who re-sahred it
532
-	 * @throws ShareNotFound
533
-	 * @throws \OC\HintException
534
-	 */
535
-	protected function revokeShare($share, $isOwner) {
536
-		// also send a unShare request to the initiator, if this is a different user than the owner
537
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
538
-			if ($isOwner) {
539
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
540
-			} else {
541
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
542
-			}
543
-			$remoteId = $this->getRemoteId($share);
544
-			$this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
545
-		}
546
-	}
547
-
548
-	/**
549
-	 * remove share from table
550
-	 *
551
-	 * @param IShare $share
552
-	 */
553
-	public function removeShareFromTable(IShare $share) {
554
-		$this->removeShareFromTableById($share->getId());
555
-	}
556
-
557
-	/**
558
-	 * remove share from table
559
-	 *
560
-	 * @param string $shareId
561
-	 */
562
-	private function removeShareFromTableById($shareId) {
563
-		$qb = $this->dbConnection->getQueryBuilder();
564
-		$qb->delete('share')
565
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
566
-		$qb->execute();
567
-
568
-		$qb->delete('federated_reshares')
569
-			->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId)));
570
-		$qb->execute();
571
-	}
572
-
573
-	/**
574
-	 * @inheritdoc
575
-	 */
576
-	public function deleteFromSelf(IShare $share, $recipient) {
577
-		// nothing to do here. Technically deleteFromSelf in the context of federated
578
-		// shares is a umount of a external storage. This is handled here
579
-		// apps/files_sharing/lib/external/manager.php
580
-		// TODO move this code over to this app
581
-		return;
582
-	}
583
-
584
-
585
-	public function getSharesInFolder($userId, Folder $node, $reshares) {
586
-		$qb = $this->dbConnection->getQueryBuilder();
587
-		$qb->select('*')
588
-			->from('share', 's')
589
-			->andWhere($qb->expr()->orX(
590
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
591
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
592
-			))
593
-			->andWhere(
594
-				$qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))
595
-			);
596
-
597
-		/**
598
-		 * Reshares for this user are shares where they are the owner.
599
-		 */
600
-		if ($reshares === false) {
601
-			$qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
602
-		} else {
603
-			$qb->andWhere(
604
-				$qb->expr()->orX(
605
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
606
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
607
-				)
608
-			);
609
-		}
610
-
611
-		$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
612
-		$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
613
-
614
-		$qb->orderBy('id');
615
-
616
-		$cursor = $qb->execute();
617
-		$shares = [];
618
-		while ($data = $cursor->fetch()) {
619
-			$shares[$data['fileid']][] = $this->createShareObject($data);
620
-		}
621
-		$cursor->closeCursor();
622
-
623
-		return $shares;
624
-	}
625
-
626
-	/**
627
-	 * @inheritdoc
628
-	 */
629
-	public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
630
-		$qb = $this->dbConnection->getQueryBuilder();
631
-		$qb->select('*')
632
-			->from('share');
633
-
634
-		$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
635
-
636
-		/**
637
-		 * Reshares for this user are shares where they are the owner.
638
-		 */
639
-		if ($reshares === false) {
640
-			//Special case for old shares created via the web UI
641
-			$or1 = $qb->expr()->andX(
642
-				$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
643
-				$qb->expr()->isNull('uid_initiator')
644
-			);
645
-
646
-			$qb->andWhere(
647
-				$qb->expr()->orX(
648
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
649
-					$or1
650
-				)
651
-			);
652
-		} else {
653
-			$qb->andWhere(
654
-				$qb->expr()->orX(
655
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
656
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
657
-				)
658
-			);
659
-		}
660
-
661
-		if ($node !== null) {
662
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
663
-		}
664
-
665
-		if ($limit !== -1) {
666
-			$qb->setMaxResults($limit);
667
-		}
668
-
669
-		$qb->setFirstResult($offset);
670
-		$qb->orderBy('id');
671
-
672
-		$cursor = $qb->execute();
673
-		$shares = [];
674
-		while($data = $cursor->fetch()) {
675
-			$shares[] = $this->createShareObject($data);
676
-		}
677
-		$cursor->closeCursor();
678
-
679
-		return $shares;
680
-	}
681
-
682
-	/**
683
-	 * @inheritdoc
684
-	 */
685
-	public function getShareById($id, $recipientId = null) {
686
-		$qb = $this->dbConnection->getQueryBuilder();
687
-
688
-		$qb->select('*')
689
-			->from('share')
690
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
691
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
692
-
693
-		$cursor = $qb->execute();
694
-		$data = $cursor->fetch();
695
-		$cursor->closeCursor();
696
-
697
-		if ($data === false) {
698
-			throw new ShareNotFound();
699
-		}
700
-
701
-		try {
702
-			$share = $this->createShareObject($data);
703
-		} catch (InvalidShare $e) {
704
-			throw new ShareNotFound();
705
-		}
706
-
707
-		return $share;
708
-	}
709
-
710
-	/**
711
-	 * Get shares for a given path
712
-	 *
713
-	 * @param \OCP\Files\Node $path
714
-	 * @return IShare[]
715
-	 */
716
-	public function getSharesByPath(Node $path) {
717
-		$qb = $this->dbConnection->getQueryBuilder();
718
-
719
-		$cursor = $qb->select('*')
720
-			->from('share')
721
-			->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
722
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
723
-			->execute();
724
-
725
-		$shares = [];
726
-		while($data = $cursor->fetch()) {
727
-			$shares[] = $this->createShareObject($data);
728
-		}
729
-		$cursor->closeCursor();
730
-
731
-		return $shares;
732
-	}
733
-
734
-	/**
735
-	 * @inheritdoc
736
-	 */
737
-	public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
738
-		/** @var IShare[] $shares */
739
-		$shares = [];
740
-
741
-		//Get shares directly with this user
742
-		$qb = $this->dbConnection->getQueryBuilder();
743
-		$qb->select('*')
744
-			->from('share');
745
-
746
-		// Order by id
747
-		$qb->orderBy('id');
748
-
749
-		// Set limit and offset
750
-		if ($limit !== -1) {
751
-			$qb->setMaxResults($limit);
752
-		}
753
-		$qb->setFirstResult($offset);
754
-
755
-		$qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
756
-		$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
757
-
758
-		// Filter by node if provided
759
-		if ($node !== null) {
760
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
761
-		}
762
-
763
-		$cursor = $qb->execute();
764
-
765
-		while($data = $cursor->fetch()) {
766
-			$shares[] = $this->createShareObject($data);
767
-		}
768
-		$cursor->closeCursor();
769
-
770
-
771
-		return $shares;
772
-	}
773
-
774
-	/**
775
-	 * Get a share by token
776
-	 *
777
-	 * @param string $token
778
-	 * @return IShare
779
-	 * @throws ShareNotFound
780
-	 */
781
-	public function getShareByToken($token) {
782
-		$qb = $this->dbConnection->getQueryBuilder();
783
-
784
-		$cursor = $qb->select('*')
785
-			->from('share')
786
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
787
-			->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
788
-			->execute();
789
-
790
-		$data = $cursor->fetch();
791
-
792
-		if ($data === false) {
793
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
794
-		}
795
-
796
-		try {
797
-			$share = $this->createShareObject($data);
798
-		} catch (InvalidShare $e) {
799
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
800
-		}
801
-
802
-		return $share;
803
-	}
804
-
805
-	/**
806
-	 * get database row of a give share
807
-	 *
808
-	 * @param $id
809
-	 * @return array
810
-	 * @throws ShareNotFound
811
-	 */
812
-	private function getRawShare($id) {
813
-
814
-		// Now fetch the inserted share and create a complete share object
815
-		$qb = $this->dbConnection->getQueryBuilder();
816
-		$qb->select('*')
817
-			->from('share')
818
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
819
-
820
-		$cursor = $qb->execute();
821
-		$data = $cursor->fetch();
822
-		$cursor->closeCursor();
823
-
824
-		if ($data === false) {
825
-			throw new ShareNotFound;
826
-		}
827
-
828
-		return $data;
829
-	}
830
-
831
-	/**
832
-	 * Create a share object from an database row
833
-	 *
834
-	 * @param array $data
835
-	 * @return IShare
836
-	 * @throws InvalidShare
837
-	 * @throws ShareNotFound
838
-	 */
839
-	private function createShareObject($data) {
840
-
841
-		$share = new Share($this->rootFolder, $this->userManager);
842
-		$share->setId((int)$data['id'])
843
-			->setShareType((int)$data['share_type'])
844
-			->setPermissions((int)$data['permissions'])
845
-			->setTarget($data['file_target'])
846
-			->setMailSend((bool)$data['mail_send'])
847
-			->setToken($data['token']);
848
-
849
-		$shareTime = new \DateTime();
850
-		$shareTime->setTimestamp((int)$data['stime']);
851
-		$share->setShareTime($shareTime);
852
-		$share->setSharedWith($data['share_with']);
853
-
854
-		if ($data['uid_initiator'] !== null) {
855
-			$share->setShareOwner($data['uid_owner']);
856
-			$share->setSharedBy($data['uid_initiator']);
857
-		} else {
858
-			//OLD SHARE
859
-			$share->setSharedBy($data['uid_owner']);
860
-			$path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
861
-
862
-			$owner = $path->getOwner();
863
-			$share->setShareOwner($owner->getUID());
864
-		}
865
-
866
-		$share->setNodeId((int)$data['file_source']);
867
-		$share->setNodeType($data['item_type']);
868
-
869
-		$share->setProviderId($this->identifier());
870
-
871
-		return $share;
872
-	}
873
-
874
-	/**
875
-	 * Get the node with file $id for $user
876
-	 *
877
-	 * @param string $userId
878
-	 * @param int $id
879
-	 * @return \OCP\Files\File|\OCP\Files\Folder
880
-	 * @throws InvalidShare
881
-	 */
882
-	private function getNode($userId, $id) {
883
-		try {
884
-			$userFolder = $this->rootFolder->getUserFolder($userId);
885
-		} catch (NotFoundException $e) {
886
-			throw new InvalidShare();
887
-		}
888
-
889
-		$nodes = $userFolder->getById($id);
890
-
891
-		if (empty($nodes)) {
892
-			throw new InvalidShare();
893
-		}
894
-
895
-		return $nodes[0];
896
-	}
897
-
898
-	/**
899
-	 * A user is deleted from the system
900
-	 * So clean up the relevant shares.
901
-	 *
902
-	 * @param string $uid
903
-	 * @param int $shareType
904
-	 */
905
-	public function userDeleted($uid, $shareType) {
906
-		//TODO: probabaly a good idea to send unshare info to remote servers
907
-
908
-		$qb = $this->dbConnection->getQueryBuilder();
909
-
910
-		$qb->delete('share')
911
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)))
912
-			->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
913
-			->execute();
914
-	}
915
-
916
-	/**
917
-	 * This provider does not handle groups
918
-	 *
919
-	 * @param string $gid
920
-	 */
921
-	public function groupDeleted($gid) {
922
-		// We don't handle groups here
923
-		return;
924
-	}
925
-
926
-	/**
927
-	 * This provider does not handle groups
928
-	 *
929
-	 * @param string $uid
930
-	 * @param string $gid
931
-	 */
932
-	public function userDeletedFromGroup($uid, $gid) {
933
-		// We don't handle groups here
934
-		return;
935
-	}
936
-
937
-	/**
938
-	 * check if users from other Nextcloud instances are allowed to mount public links share by this instance
939
-	 *
940
-	 * @return bool
941
-	 */
942
-	public function isOutgoingServer2serverShareEnabled() {
943
-		$result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes');
944
-		return ($result === 'yes');
945
-	}
946
-
947
-	/**
948
-	 * check if users are allowed to mount public links from other Nextclouds
949
-	 *
950
-	 * @return bool
951
-	 */
952
-	public function isIncomingServer2serverShareEnabled() {
953
-		$result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes');
954
-		return ($result === 'yes');
955
-	}
956
-
957
-	/**
958
-	 * Check if querying sharees on the lookup server is enabled
959
-	 *
960
-	 * @return bool
961
-	 */
962
-	public function isLookupServerQueriesEnabled() {
963
-		$result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no');
964
-		return ($result === 'yes');
965
-	}
966
-
967
-
968
-	/**
969
-	 * Check if it is allowed to publish user specific data to the lookup server
970
-	 *
971
-	 * @return bool
972
-	 */
973
-	public function isLookupServerUploadEnabled() {
974
-		$result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes');
975
-		return ($result === 'yes');
976
-	}
462
+        return $share;
463
+    }
464
+
465
+    /**
466
+     * Get all children of this share
467
+     *
468
+     * @param IShare $parent
469
+     * @return IShare[]
470
+     */
471
+    public function getChildren(IShare $parent) {
472
+        $children = [];
473
+
474
+        $qb = $this->dbConnection->getQueryBuilder();
475
+        $qb->select('*')
476
+            ->from('share')
477
+            ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
478
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
479
+            ->orderBy('id');
480
+
481
+        $cursor = $qb->execute();
482
+        while($data = $cursor->fetch()) {
483
+            $children[] = $this->createShareObject($data);
484
+        }
485
+        $cursor->closeCursor();
486
+
487
+        return $children;
488
+    }
489
+
490
+    /**
491
+     * Delete a share (owner unShares the file)
492
+     *
493
+     * @param IShare $share
494
+     */
495
+    public function delete(IShare $share) {
496
+
497
+        list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith());
498
+
499
+        $isOwner = false;
500
+
501
+        $this->removeShareFromTable($share);
502
+
503
+        // if the local user is the owner we can send the unShare request directly...
504
+        if ($this->userManager->userExists($share->getShareOwner())) {
505
+            $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken());
506
+            $this->revokeShare($share, true);
507
+            $isOwner = true;
508
+        } else { // ... if not we need to correct ID for the unShare request
509
+            $remoteId = $this->getRemoteId($share);
510
+            $this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken());
511
+            $this->revokeShare($share, false);
512
+        }
513
+
514
+        // send revoke notification to the other user, if initiator and owner are not the same user
515
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
516
+            $remoteId = $this->getRemoteId($share);
517
+            if ($isOwner) {
518
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
519
+            } else {
520
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
521
+            }
522
+            $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
523
+        }
524
+    }
525
+
526
+    /**
527
+     * in case of a re-share we need to send the other use (initiator or owner)
528
+     * a message that the file was unshared
529
+     *
530
+     * @param IShare $share
531
+     * @param bool $isOwner the user can either be the owner or the user who re-sahred it
532
+     * @throws ShareNotFound
533
+     * @throws \OC\HintException
534
+     */
535
+    protected function revokeShare($share, $isOwner) {
536
+        // also send a unShare request to the initiator, if this is a different user than the owner
537
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
538
+            if ($isOwner) {
539
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
540
+            } else {
541
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
542
+            }
543
+            $remoteId = $this->getRemoteId($share);
544
+            $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
545
+        }
546
+    }
547
+
548
+    /**
549
+     * remove share from table
550
+     *
551
+     * @param IShare $share
552
+     */
553
+    public function removeShareFromTable(IShare $share) {
554
+        $this->removeShareFromTableById($share->getId());
555
+    }
556
+
557
+    /**
558
+     * remove share from table
559
+     *
560
+     * @param string $shareId
561
+     */
562
+    private function removeShareFromTableById($shareId) {
563
+        $qb = $this->dbConnection->getQueryBuilder();
564
+        $qb->delete('share')
565
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
566
+        $qb->execute();
567
+
568
+        $qb->delete('federated_reshares')
569
+            ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId)));
570
+        $qb->execute();
571
+    }
572
+
573
+    /**
574
+     * @inheritdoc
575
+     */
576
+    public function deleteFromSelf(IShare $share, $recipient) {
577
+        // nothing to do here. Technically deleteFromSelf in the context of federated
578
+        // shares is a umount of a external storage. This is handled here
579
+        // apps/files_sharing/lib/external/manager.php
580
+        // TODO move this code over to this app
581
+        return;
582
+    }
583
+
584
+
585
+    public function getSharesInFolder($userId, Folder $node, $reshares) {
586
+        $qb = $this->dbConnection->getQueryBuilder();
587
+        $qb->select('*')
588
+            ->from('share', 's')
589
+            ->andWhere($qb->expr()->orX(
590
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
591
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
592
+            ))
593
+            ->andWhere(
594
+                $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))
595
+            );
596
+
597
+        /**
598
+         * Reshares for this user are shares where they are the owner.
599
+         */
600
+        if ($reshares === false) {
601
+            $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
602
+        } else {
603
+            $qb->andWhere(
604
+                $qb->expr()->orX(
605
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
606
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
607
+                )
608
+            );
609
+        }
610
+
611
+        $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
612
+        $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
613
+
614
+        $qb->orderBy('id');
615
+
616
+        $cursor = $qb->execute();
617
+        $shares = [];
618
+        while ($data = $cursor->fetch()) {
619
+            $shares[$data['fileid']][] = $this->createShareObject($data);
620
+        }
621
+        $cursor->closeCursor();
622
+
623
+        return $shares;
624
+    }
625
+
626
+    /**
627
+     * @inheritdoc
628
+     */
629
+    public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
630
+        $qb = $this->dbConnection->getQueryBuilder();
631
+        $qb->select('*')
632
+            ->from('share');
633
+
634
+        $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
635
+
636
+        /**
637
+         * Reshares for this user are shares where they are the owner.
638
+         */
639
+        if ($reshares === false) {
640
+            //Special case for old shares created via the web UI
641
+            $or1 = $qb->expr()->andX(
642
+                $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
643
+                $qb->expr()->isNull('uid_initiator')
644
+            );
645
+
646
+            $qb->andWhere(
647
+                $qb->expr()->orX(
648
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
649
+                    $or1
650
+                )
651
+            );
652
+        } else {
653
+            $qb->andWhere(
654
+                $qb->expr()->orX(
655
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
656
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
657
+                )
658
+            );
659
+        }
660
+
661
+        if ($node !== null) {
662
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
663
+        }
664
+
665
+        if ($limit !== -1) {
666
+            $qb->setMaxResults($limit);
667
+        }
668
+
669
+        $qb->setFirstResult($offset);
670
+        $qb->orderBy('id');
671
+
672
+        $cursor = $qb->execute();
673
+        $shares = [];
674
+        while($data = $cursor->fetch()) {
675
+            $shares[] = $this->createShareObject($data);
676
+        }
677
+        $cursor->closeCursor();
678
+
679
+        return $shares;
680
+    }
681
+
682
+    /**
683
+     * @inheritdoc
684
+     */
685
+    public function getShareById($id, $recipientId = null) {
686
+        $qb = $this->dbConnection->getQueryBuilder();
687
+
688
+        $qb->select('*')
689
+            ->from('share')
690
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
691
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
692
+
693
+        $cursor = $qb->execute();
694
+        $data = $cursor->fetch();
695
+        $cursor->closeCursor();
696
+
697
+        if ($data === false) {
698
+            throw new ShareNotFound();
699
+        }
700
+
701
+        try {
702
+            $share = $this->createShareObject($data);
703
+        } catch (InvalidShare $e) {
704
+            throw new ShareNotFound();
705
+        }
706
+
707
+        return $share;
708
+    }
709
+
710
+    /**
711
+     * Get shares for a given path
712
+     *
713
+     * @param \OCP\Files\Node $path
714
+     * @return IShare[]
715
+     */
716
+    public function getSharesByPath(Node $path) {
717
+        $qb = $this->dbConnection->getQueryBuilder();
718
+
719
+        $cursor = $qb->select('*')
720
+            ->from('share')
721
+            ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
722
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
723
+            ->execute();
724
+
725
+        $shares = [];
726
+        while($data = $cursor->fetch()) {
727
+            $shares[] = $this->createShareObject($data);
728
+        }
729
+        $cursor->closeCursor();
730
+
731
+        return $shares;
732
+    }
733
+
734
+    /**
735
+     * @inheritdoc
736
+     */
737
+    public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
738
+        /** @var IShare[] $shares */
739
+        $shares = [];
740
+
741
+        //Get shares directly with this user
742
+        $qb = $this->dbConnection->getQueryBuilder();
743
+        $qb->select('*')
744
+            ->from('share');
745
+
746
+        // Order by id
747
+        $qb->orderBy('id');
748
+
749
+        // Set limit and offset
750
+        if ($limit !== -1) {
751
+            $qb->setMaxResults($limit);
752
+        }
753
+        $qb->setFirstResult($offset);
754
+
755
+        $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
756
+        $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
757
+
758
+        // Filter by node if provided
759
+        if ($node !== null) {
760
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
761
+        }
762
+
763
+        $cursor = $qb->execute();
764
+
765
+        while($data = $cursor->fetch()) {
766
+            $shares[] = $this->createShareObject($data);
767
+        }
768
+        $cursor->closeCursor();
769
+
770
+
771
+        return $shares;
772
+    }
773
+
774
+    /**
775
+     * Get a share by token
776
+     *
777
+     * @param string $token
778
+     * @return IShare
779
+     * @throws ShareNotFound
780
+     */
781
+    public function getShareByToken($token) {
782
+        $qb = $this->dbConnection->getQueryBuilder();
783
+
784
+        $cursor = $qb->select('*')
785
+            ->from('share')
786
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
787
+            ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
788
+            ->execute();
789
+
790
+        $data = $cursor->fetch();
791
+
792
+        if ($data === false) {
793
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
794
+        }
795
+
796
+        try {
797
+            $share = $this->createShareObject($data);
798
+        } catch (InvalidShare $e) {
799
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
800
+        }
801
+
802
+        return $share;
803
+    }
804
+
805
+    /**
806
+     * get database row of a give share
807
+     *
808
+     * @param $id
809
+     * @return array
810
+     * @throws ShareNotFound
811
+     */
812
+    private function getRawShare($id) {
813
+
814
+        // Now fetch the inserted share and create a complete share object
815
+        $qb = $this->dbConnection->getQueryBuilder();
816
+        $qb->select('*')
817
+            ->from('share')
818
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
819
+
820
+        $cursor = $qb->execute();
821
+        $data = $cursor->fetch();
822
+        $cursor->closeCursor();
823
+
824
+        if ($data === false) {
825
+            throw new ShareNotFound;
826
+        }
827
+
828
+        return $data;
829
+    }
830
+
831
+    /**
832
+     * Create a share object from an database row
833
+     *
834
+     * @param array $data
835
+     * @return IShare
836
+     * @throws InvalidShare
837
+     * @throws ShareNotFound
838
+     */
839
+    private function createShareObject($data) {
840
+
841
+        $share = new Share($this->rootFolder, $this->userManager);
842
+        $share->setId((int)$data['id'])
843
+            ->setShareType((int)$data['share_type'])
844
+            ->setPermissions((int)$data['permissions'])
845
+            ->setTarget($data['file_target'])
846
+            ->setMailSend((bool)$data['mail_send'])
847
+            ->setToken($data['token']);
848
+
849
+        $shareTime = new \DateTime();
850
+        $shareTime->setTimestamp((int)$data['stime']);
851
+        $share->setShareTime($shareTime);
852
+        $share->setSharedWith($data['share_with']);
853
+
854
+        if ($data['uid_initiator'] !== null) {
855
+            $share->setShareOwner($data['uid_owner']);
856
+            $share->setSharedBy($data['uid_initiator']);
857
+        } else {
858
+            //OLD SHARE
859
+            $share->setSharedBy($data['uid_owner']);
860
+            $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
861
+
862
+            $owner = $path->getOwner();
863
+            $share->setShareOwner($owner->getUID());
864
+        }
865
+
866
+        $share->setNodeId((int)$data['file_source']);
867
+        $share->setNodeType($data['item_type']);
868
+
869
+        $share->setProviderId($this->identifier());
870
+
871
+        return $share;
872
+    }
873
+
874
+    /**
875
+     * Get the node with file $id for $user
876
+     *
877
+     * @param string $userId
878
+     * @param int $id
879
+     * @return \OCP\Files\File|\OCP\Files\Folder
880
+     * @throws InvalidShare
881
+     */
882
+    private function getNode($userId, $id) {
883
+        try {
884
+            $userFolder = $this->rootFolder->getUserFolder($userId);
885
+        } catch (NotFoundException $e) {
886
+            throw new InvalidShare();
887
+        }
888
+
889
+        $nodes = $userFolder->getById($id);
890
+
891
+        if (empty($nodes)) {
892
+            throw new InvalidShare();
893
+        }
894
+
895
+        return $nodes[0];
896
+    }
897
+
898
+    /**
899
+     * A user is deleted from the system
900
+     * So clean up the relevant shares.
901
+     *
902
+     * @param string $uid
903
+     * @param int $shareType
904
+     */
905
+    public function userDeleted($uid, $shareType) {
906
+        //TODO: probabaly a good idea to send unshare info to remote servers
907
+
908
+        $qb = $this->dbConnection->getQueryBuilder();
909
+
910
+        $qb->delete('share')
911
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)))
912
+            ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
913
+            ->execute();
914
+    }
915
+
916
+    /**
917
+     * This provider does not handle groups
918
+     *
919
+     * @param string $gid
920
+     */
921
+    public function groupDeleted($gid) {
922
+        // We don't handle groups here
923
+        return;
924
+    }
925
+
926
+    /**
927
+     * This provider does not handle groups
928
+     *
929
+     * @param string $uid
930
+     * @param string $gid
931
+     */
932
+    public function userDeletedFromGroup($uid, $gid) {
933
+        // We don't handle groups here
934
+        return;
935
+    }
936
+
937
+    /**
938
+     * check if users from other Nextcloud instances are allowed to mount public links share by this instance
939
+     *
940
+     * @return bool
941
+     */
942
+    public function isOutgoingServer2serverShareEnabled() {
943
+        $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes');
944
+        return ($result === 'yes');
945
+    }
946
+
947
+    /**
948
+     * check if users are allowed to mount public links from other Nextclouds
949
+     *
950
+     * @return bool
951
+     */
952
+    public function isIncomingServer2serverShareEnabled() {
953
+        $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes');
954
+        return ($result === 'yes');
955
+    }
956
+
957
+    /**
958
+     * Check if querying sharees on the lookup server is enabled
959
+     *
960
+     * @return bool
961
+     */
962
+    public function isLookupServerQueriesEnabled() {
963
+        $result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no');
964
+        return ($result === 'yes');
965
+    }
966
+
967
+
968
+    /**
969
+     * Check if it is allowed to publish user specific data to the lookup server
970
+     *
971
+     * @return bool
972
+     */
973
+    public function isLookupServerUploadEnabled() {
974
+        $result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes');
975
+        return ($result === 'yes');
976
+    }
977 977
 }
Please login to merge, or discard this patch.
apps/files_trashbin/lib/Trashbin.php 1 patch
Indentation   +937 added lines, -937 removed lines patch added patch discarded remove patch
@@ -47,941 +47,941 @@
 block discarded – undo
47 47
 
48 48
 class Trashbin {
49 49
 
50
-	// unit: percentage; 50% of available disk space/quota
51
-	const DEFAULTMAXSIZE = 50;
52
-
53
-	/**
54
-	 * Whether versions have already be rescanned during this PHP request
55
-	 *
56
-	 * @var bool
57
-	 */
58
-	private static $scannedVersions = false;
59
-
60
-	/**
61
-	 * Ensure we don't need to scan the file during the move to trash
62
-	 * by triggering the scan in the pre-hook
63
-	 *
64
-	 * @param array $params
65
-	 */
66
-	public static function ensureFileScannedHook($params) {
67
-		try {
68
-			self::getUidAndFilename($params['path']);
69
-		} catch (NotFoundException $e) {
70
-			// nothing to scan for non existing files
71
-		}
72
-	}
73
-
74
-	/**
75
-	 * get the UID of the owner of the file and the path to the file relative to
76
-	 * owners files folder
77
-	 *
78
-	 * @param string $filename
79
-	 * @return array
80
-	 * @throws \OC\User\NoUserException
81
-	 */
82
-	public static function getUidAndFilename($filename) {
83
-		$uid = Filesystem::getOwner($filename);
84
-		$userManager = \OC::$server->getUserManager();
85
-		// if the user with the UID doesn't exists, e.g. because the UID points
86
-		// to a remote user with a federated cloud ID we use the current logged-in
87
-		// user. We need a valid local user to move the file to the right trash bin
88
-		if (!$userManager->userExists($uid)) {
89
-			$uid = User::getUser();
90
-		}
91
-		if (!$uid) {
92
-			// no owner, usually because of share link from ext storage
93
-			return [null, null];
94
-		}
95
-		Filesystem::initMountPoints($uid);
96
-		if ($uid != User::getUser()) {
97
-			$info = Filesystem::getFileInfo($filename);
98
-			$ownerView = new View('/' . $uid . '/files');
99
-			try {
100
-				$filename = $ownerView->getPath($info['fileid']);
101
-			} catch (NotFoundException $e) {
102
-				$filename = null;
103
-			}
104
-		}
105
-		return [$uid, $filename];
106
-	}
107
-
108
-	/**
109
-	 * get original location of files for user
110
-	 *
111
-	 * @param string $user
112
-	 * @return array (filename => array (timestamp => original location))
113
-	 */
114
-	public static function getLocations($user) {
115
-		$query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
116
-			. ' FROM `*PREFIX*files_trash` WHERE `user`=?');
117
-		$result = $query->execute(array($user));
118
-		$array = array();
119
-		while ($row = $result->fetchRow()) {
120
-			if (isset($array[$row['id']])) {
121
-				$array[$row['id']][$row['timestamp']] = $row['location'];
122
-			} else {
123
-				$array[$row['id']] = array($row['timestamp'] => $row['location']);
124
-			}
125
-		}
126
-		return $array;
127
-	}
128
-
129
-	/**
130
-	 * get original location of file
131
-	 *
132
-	 * @param string $user
133
-	 * @param string $filename
134
-	 * @param string $timestamp
135
-	 * @return string original location
136
-	 */
137
-	public static function getLocation($user, $filename, $timestamp) {
138
-		$query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
139
-			. ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
140
-		$result = $query->execute(array($user, $filename, $timestamp))->fetchAll();
141
-		if (isset($result[0]['location'])) {
142
-			return $result[0]['location'];
143
-		} else {
144
-			return false;
145
-		}
146
-	}
147
-
148
-	private static function setUpTrash($user) {
149
-		$view = new View('/' . $user);
150
-		if (!$view->is_dir('files_trashbin')) {
151
-			$view->mkdir('files_trashbin');
152
-		}
153
-		if (!$view->is_dir('files_trashbin/files')) {
154
-			$view->mkdir('files_trashbin/files');
155
-		}
156
-		if (!$view->is_dir('files_trashbin/versions')) {
157
-			$view->mkdir('files_trashbin/versions');
158
-		}
159
-		if (!$view->is_dir('files_trashbin/keys')) {
160
-			$view->mkdir('files_trashbin/keys');
161
-		}
162
-	}
163
-
164
-
165
-	/**
166
-	 * copy file to owners trash
167
-	 *
168
-	 * @param string $sourcePath
169
-	 * @param string $owner
170
-	 * @param string $targetPath
171
-	 * @param $user
172
-	 * @param integer $timestamp
173
-	 */
174
-	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
175
-		self::setUpTrash($owner);
176
-
177
-		$targetFilename = basename($targetPath);
178
-		$targetLocation = dirname($targetPath);
179
-
180
-		$sourceFilename = basename($sourcePath);
181
-
182
-		$view = new View('/');
183
-
184
-		$target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
185
-		$source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
186
-		self::copy_recursive($source, $target, $view);
187
-
188
-
189
-		if ($view->file_exists($target)) {
190
-			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
191
-			$result = $query->execute(array($targetFilename, $timestamp, $targetLocation, $user));
192
-			if (!$result) {
193
-				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated for the files owner', \OCP\Util::ERROR);
194
-			}
195
-		}
196
-	}
197
-
198
-
199
-	/**
200
-	 * move file to the trash bin
201
-	 *
202
-	 * @param string $file_path path to the deleted file/directory relative to the files root directory
203
-	 * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
204
-	 *
205
-	 * @return bool
206
-	 */
207
-	public static function move2trash($file_path, $ownerOnly = false) {
208
-		// get the user for which the filesystem is setup
209
-		$root = Filesystem::getRoot();
210
-		list(, $user) = explode('/', $root);
211
-		list($owner, $ownerPath) = self::getUidAndFilename($file_path);
212
-
213
-		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
214
-		if (is_null($owner)) {
215
-			$owner = $user;
216
-			$ownerPath = $file_path;
217
-		}
218
-
219
-		$ownerView = new View('/' . $owner);
220
-		// file has been deleted in between
221
-		if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
222
-			return true;
223
-		}
224
-
225
-		self::setUpTrash($user);
226
-		if ($owner !== $user) {
227
-			// also setup for owner
228
-			self::setUpTrash($owner);
229
-		}
230
-
231
-		$path_parts = pathinfo($ownerPath);
232
-
233
-		$filename = $path_parts['basename'];
234
-		$location = $path_parts['dirname'];
235
-		$timestamp = time();
236
-
237
-		// disable proxy to prevent recursive calls
238
-		$trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
239
-
240
-		/** @var \OC\Files\Storage\Storage $trashStorage */
241
-		list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath);
242
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
243
-		list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath);
244
-		try {
245
-			$moveSuccessful = true;
246
-			if ($trashStorage->file_exists($trashInternalPath)) {
247
-				$trashStorage->unlink($trashInternalPath);
248
-			}
249
-			$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
250
-		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
251
-			$moveSuccessful = false;
252
-			if ($trashStorage->file_exists($trashInternalPath)) {
253
-				$trashStorage->unlink($trashInternalPath);
254
-			}
255
-			\OCP\Util::writeLog('files_trashbin', 'Couldn\'t move ' . $file_path . ' to the trash bin', \OCP\Util::ERROR);
256
-		}
257
-
258
-		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
259
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
260
-				$sourceStorage->rmdir($sourceInternalPath);
261
-			} else {
262
-				$sourceStorage->unlink($sourceInternalPath);
263
-			}
264
-			return false;
265
-		}
266
-
267
-		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
268
-
269
-		if ($moveSuccessful) {
270
-			$query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
271
-			$result = $query->execute(array($filename, $timestamp, $location, $owner));
272
-			if (!$result) {
273
-				\OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated', \OCP\Util::ERROR);
274
-			}
275
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', array('filePath' => Filesystem::normalizePath($file_path),
276
-				'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)));
277
-
278
-			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
279
-
280
-			// if owner !== user we need to also add a copy to the users trash
281
-			if ($user !== $owner && $ownerOnly === false) {
282
-				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
283
-			}
284
-		}
285
-
286
-		self::scheduleExpire($user);
287
-
288
-		// if owner !== user we also need to update the owners trash size
289
-		if ($owner !== $user) {
290
-			self::scheduleExpire($owner);
291
-		}
292
-
293
-		return $moveSuccessful;
294
-	}
295
-
296
-	/**
297
-	 * Move file versions to trash so that they can be restored later
298
-	 *
299
-	 * @param string $filename of deleted file
300
-	 * @param string $owner owner user id
301
-	 * @param string $ownerPath path relative to the owner's home storage
302
-	 * @param integer $timestamp when the file was deleted
303
-	 */
304
-	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
305
-		if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
306
-
307
-			$user = User::getUser();
308
-			$rootView = new View('/');
309
-
310
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
311
-				if ($owner !== $user) {
312
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
313
-				}
314
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
315
-			} else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
316
-
317
-				foreach ($versions as $v) {
318
-					if ($owner !== $user) {
319
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
320
-					}
321
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
322
-				}
323
-			}
324
-		}
325
-	}
326
-
327
-	/**
328
-	 * Move a file or folder on storage level
329
-	 *
330
-	 * @param View $view
331
-	 * @param string $source
332
-	 * @param string $target
333
-	 * @return bool
334
-	 */
335
-	private static function move(View $view, $source, $target) {
336
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
337
-		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
338
-		/** @var \OC\Files\Storage\Storage $targetStorage */
339
-		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
340
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
341
-
342
-		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
343
-		if ($result) {
344
-			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
345
-		}
346
-		return $result;
347
-	}
348
-
349
-	/**
350
-	 * Copy a file or folder on storage level
351
-	 *
352
-	 * @param View $view
353
-	 * @param string $source
354
-	 * @param string $target
355
-	 * @return bool
356
-	 */
357
-	private static function copy(View $view, $source, $target) {
358
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
359
-		list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
360
-		/** @var \OC\Files\Storage\Storage $targetStorage */
361
-		list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
362
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
363
-
364
-		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
365
-		if ($result) {
366
-			$targetStorage->getUpdater()->update($targetInternalPath);
367
-		}
368
-		return $result;
369
-	}
370
-
371
-	/**
372
-	 * Restore a file or folder from trash bin
373
-	 *
374
-	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
375
-	 * including the timestamp suffix ".d12345678"
376
-	 * @param string $filename name of the file/folder
377
-	 * @param int $timestamp time when the file/folder was deleted
378
-	 *
379
-	 * @return bool true on success, false otherwise
380
-	 */
381
-	public static function restore($file, $filename, $timestamp) {
382
-		$user = User::getUser();
383
-		$view = new View('/' . $user);
384
-
385
-		$location = '';
386
-		if ($timestamp) {
387
-			$location = self::getLocation($user, $filename, $timestamp);
388
-			if ($location === false) {
389
-				\OCP\Util::writeLog('files_trashbin', 'trash bin database inconsistent!', \OCP\Util::ERROR);
390
-			} else {
391
-				// if location no longer exists, restore file in the root directory
392
-				if ($location !== '/' &&
393
-					(!$view->is_dir('files/' . $location) ||
394
-						!$view->isCreatable('files/' . $location))
395
-				) {
396
-					$location = '';
397
-				}
398
-			}
399
-		}
400
-
401
-		// we need a  extension in case a file/dir with the same name already exists
402
-		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
403
-
404
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
405
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
406
-		if (!$view->file_exists($source)) {
407
-			return false;
408
-		}
409
-		$mtime = $view->filemtime($source);
410
-
411
-		// restore file
412
-		$restoreResult = $view->rename($source, $target);
413
-
414
-		// handle the restore result
415
-		if ($restoreResult) {
416
-			$fakeRoot = $view->getRoot();
417
-			$view->chroot('/' . $user . '/files');
418
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
419
-			$view->chroot($fakeRoot);
420
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', array('filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
421
-				'trashPath' => Filesystem::normalizePath($file)));
422
-
423
-			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
424
-
425
-			if ($timestamp) {
426
-				$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
427
-				$query->execute(array($user, $filename, $timestamp));
428
-			}
429
-
430
-			return true;
431
-		}
432
-
433
-		return false;
434
-	}
435
-
436
-	/**
437
-	 * restore versions from trash bin
438
-	 *
439
-	 * @param View $view file view
440
-	 * @param string $file complete path to file
441
-	 * @param string $filename name of file once it was deleted
442
-	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
443
-	 * @param string $location location if file
444
-	 * @param int $timestamp deletion time
445
-	 * @return false|null
446
-	 */
447
-	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
448
-
449
-		if (\OCP\App::isEnabled('files_versions')) {
450
-
451
-			$user = User::getUser();
452
-			$rootView = new View('/');
453
-
454
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
455
-
456
-			list($owner, $ownerPath) = self::getUidAndFilename($target);
457
-
458
-			// file has been deleted in between
459
-			if (empty($ownerPath)) {
460
-				return false;
461
-			}
462
-
463
-			if ($timestamp) {
464
-				$versionedFile = $filename;
465
-			} else {
466
-				$versionedFile = $file;
467
-			}
468
-
469
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
470
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
471
-			} else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
472
-				foreach ($versions as $v) {
473
-					if ($timestamp) {
474
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
475
-					} else {
476
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
477
-					}
478
-				}
479
-			}
480
-		}
481
-	}
482
-
483
-	/**
484
-	 * delete all files from the trash
485
-	 */
486
-	public static function deleteAll() {
487
-		$user = User::getUser();
488
-		$view = new View('/' . $user);
489
-		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
490
-
491
-		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
492
-		$filePaths = array();
493
-		foreach($fileInfos as $fileInfo){
494
-			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
495
-		}
496
-		unset($fileInfos); // save memory
497
-
498
-		// Bulk PreDelete-Hook
499
-		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', array('paths' => $filePaths));
500
-
501
-		// Single-File Hooks
502
-		foreach($filePaths as $path){
503
-			self::emitTrashbinPreDelete($path);
504
-		}
505
-
506
-		// actual file deletion
507
-		$view->deleteAll('files_trashbin');
508
-		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
509
-		$query->execute(array($user));
510
-
511
-		// Bulk PostDelete-Hook
512
-		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', array('paths' => $filePaths));
513
-
514
-		// Single-File Hooks
515
-		foreach($filePaths as $path){
516
-			self::emitTrashbinPostDelete($path);
517
-		}
518
-
519
-		$view->mkdir('files_trashbin');
520
-		$view->mkdir('files_trashbin/files');
521
-
522
-		return true;
523
-	}
524
-
525
-	/**
526
-	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
527
-	 * @param string $path
528
-	 */
529
-	protected static function emitTrashbinPreDelete($path){
530
-		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', array('path' => $path));
531
-	}
532
-
533
-	/**
534
-	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
535
-	 * @param string $path
536
-	 */
537
-	protected static function emitTrashbinPostDelete($path){
538
-		\OC_Hook::emit('\OCP\Trashbin', 'delete', array('path' => $path));
539
-	}
540
-
541
-	/**
542
-	 * delete file from trash bin permanently
543
-	 *
544
-	 * @param string $filename path to the file
545
-	 * @param string $user
546
-	 * @param int $timestamp of deletion time
547
-	 *
548
-	 * @return int size of deleted files
549
-	 */
550
-	public static function delete($filename, $user, $timestamp = null) {
551
-		$view = new View('/' . $user);
552
-		$size = 0;
553
-
554
-		if ($timestamp) {
555
-			$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
556
-			$query->execute(array($user, $filename, $timestamp));
557
-			$file = $filename . '.d' . $timestamp;
558
-		} else {
559
-			$file = $filename;
560
-		}
561
-
562
-		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
563
-
564
-		if ($view->is_dir('/files_trashbin/files/' . $file)) {
565
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
566
-		} else {
567
-			$size += $view->filesize('/files_trashbin/files/' . $file);
568
-		}
569
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
570
-		$view->unlink('/files_trashbin/files/' . $file);
571
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
572
-
573
-		return $size;
574
-	}
575
-
576
-	/**
577
-	 * @param View $view
578
-	 * @param string $file
579
-	 * @param string $filename
580
-	 * @param integer|null $timestamp
581
-	 * @param string $user
582
-	 * @return int
583
-	 */
584
-	private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
585
-		$size = 0;
586
-		if (\OCP\App::isEnabled('files_versions')) {
587
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
588
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
589
-				$view->unlink('files_trashbin/versions/' . $file);
590
-			} else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
591
-				foreach ($versions as $v) {
592
-					if ($timestamp) {
593
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
594
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
595
-					} else {
596
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
597
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
598
-					}
599
-				}
600
-			}
601
-		}
602
-		return $size;
603
-	}
604
-
605
-	/**
606
-	 * check to see whether a file exists in trashbin
607
-	 *
608
-	 * @param string $filename path to the file
609
-	 * @param int $timestamp of deletion time
610
-	 * @return bool true if file exists, otherwise false
611
-	 */
612
-	public static function file_exists($filename, $timestamp = null) {
613
-		$user = User::getUser();
614
-		$view = new View('/' . $user);
615
-
616
-		if ($timestamp) {
617
-			$filename = $filename . '.d' . $timestamp;
618
-		} else {
619
-			$filename = $filename;
620
-		}
621
-
622
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
623
-		return $view->file_exists($target);
624
-	}
625
-
626
-	/**
627
-	 * deletes used space for trash bin in db if user was deleted
628
-	 *
629
-	 * @param string $uid id of deleted user
630
-	 * @return bool result of db delete operation
631
-	 */
632
-	public static function deleteUser($uid) {
633
-		$query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
634
-		return $query->execute(array($uid));
635
-	}
636
-
637
-	/**
638
-	 * calculate remaining free space for trash bin
639
-	 *
640
-	 * @param integer $trashbinSize current size of the trash bin
641
-	 * @param string $user
642
-	 * @return int available free space for trash bin
643
-	 */
644
-	private static function calculateFreeSpace($trashbinSize, $user) {
645
-		$softQuota = true;
646
-		$userObject = \OC::$server->getUserManager()->get($user);
647
-		if(is_null($userObject)) {
648
-			return 0;
649
-		}
650
-		$quota = $userObject->getQuota();
651
-		if ($quota === null || $quota === 'none') {
652
-			$quota = Filesystem::free_space('/');
653
-			$softQuota = false;
654
-			// inf or unknown free space
655
-			if ($quota < 0) {
656
-				$quota = PHP_INT_MAX;
657
-			}
658
-		} else {
659
-			$quota = \OCP\Util::computerFileSize($quota);
660
-		}
661
-
662
-		// calculate available space for trash bin
663
-		// subtract size of files and current trash bin size from quota
664
-		if ($softQuota) {
665
-			$userFolder = \OC::$server->getUserFolder($user);
666
-			if(is_null($userFolder)) {
667
-				return 0;
668
-			}
669
-			$free = $quota - $userFolder->getSize(); // remaining free space for user
670
-			if ($free > 0) {
671
-				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
672
-			} else {
673
-				$availableSpace = $free - $trashbinSize;
674
-			}
675
-		} else {
676
-			$availableSpace = $quota;
677
-		}
678
-
679
-		return $availableSpace;
680
-	}
681
-
682
-	/**
683
-	 * resize trash bin if necessary after a new file was added to Nextcloud
684
-	 *
685
-	 * @param string $user user id
686
-	 */
687
-	public static function resizeTrash($user) {
688
-
689
-		$size = self::getTrashbinSize($user);
690
-
691
-		$freeSpace = self::calculateFreeSpace($size, $user);
692
-
693
-		if ($freeSpace < 0) {
694
-			self::scheduleExpire($user);
695
-		}
696
-	}
697
-
698
-	/**
699
-	 * clean up the trash bin
700
-	 *
701
-	 * @param string $user
702
-	 */
703
-	public static function expire($user) {
704
-		$trashBinSize = self::getTrashbinSize($user);
705
-		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
706
-
707
-		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
708
-
709
-		// delete all files older then $retention_obligation
710
-		list($delSize, $count) = self::deleteExpiredFiles($dirContent, $user);
711
-
712
-		$availableSpace += $delSize;
713
-
714
-		// delete files from trash until we meet the trash bin size limit again
715
-		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
716
-	}
717
-
718
-	/**
719
-	 * @param string $user
720
-	 */
721
-	private static function scheduleExpire($user) {
722
-		// let the admin disable auto expire
723
-		$application = new Application();
724
-		$expiration = $application->getContainer()->query('Expiration');
725
-		if ($expiration->isEnabled()) {
726
-			\OC::$server->getCommandBus()->push(new Expire($user));
727
-		}
728
-	}
729
-
730
-	/**
731
-	 * if the size limit for the trash bin is reached, we delete the oldest
732
-	 * files in the trash bin until we meet the limit again
733
-	 *
734
-	 * @param array $files
735
-	 * @param string $user
736
-	 * @param int $availableSpace available disc space
737
-	 * @return int size of deleted files
738
-	 */
739
-	protected static function deleteFiles($files, $user, $availableSpace) {
740
-		$application = new Application();
741
-		$expiration = $application->getContainer()->query('Expiration');
742
-		$size = 0;
743
-
744
-		if ($availableSpace < 0) {
745
-			foreach ($files as $file) {
746
-				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
747
-					$tmp = self::delete($file['name'], $user, $file['mtime']);
748
-					\OCP\Util::writeLog('files_trashbin', 'remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', \OCP\Util::INFO);
749
-					$availableSpace += $tmp;
750
-					$size += $tmp;
751
-				} else {
752
-					break;
753
-				}
754
-			}
755
-		}
756
-		return $size;
757
-	}
758
-
759
-	/**
760
-	 * delete files older then max storage time
761
-	 *
762
-	 * @param array $files list of files sorted by mtime
763
-	 * @param string $user
764
-	 * @return integer[] size of deleted files and number of deleted files
765
-	 */
766
-	public static function deleteExpiredFiles($files, $user) {
767
-		$application = new Application();
768
-		$expiration = $application->getContainer()->query('Expiration');
769
-		$size = 0;
770
-		$count = 0;
771
-		foreach ($files as $file) {
772
-			$timestamp = $file['mtime'];
773
-			$filename = $file['name'];
774
-			if ($expiration->isExpired($timestamp)) {
775
-				$count++;
776
-				$size += self::delete($filename, $user, $timestamp);
777
-				\OC::$server->getLogger()->info(
778
-					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
779
-					['app' => 'files_trashbin']
780
-				);
781
-			} else {
782
-				break;
783
-			}
784
-		}
785
-
786
-		return array($size, $count);
787
-	}
788
-
789
-	/**
790
-	 * recursive copy to copy a whole directory
791
-	 *
792
-	 * @param string $source source path, relative to the users files directory
793
-	 * @param string $destination destination path relative to the users root directoy
794
-	 * @param View $view file view for the users root directory
795
-	 * @return int
796
-	 * @throws Exceptions\CopyRecursiveException
797
-	 */
798
-	private static function copy_recursive($source, $destination, View $view) {
799
-		$size = 0;
800
-		if ($view->is_dir($source)) {
801
-			$view->mkdir($destination);
802
-			$view->touch($destination, $view->filemtime($source));
803
-			foreach ($view->getDirectoryContent($source) as $i) {
804
-				$pathDir = $source . '/' . $i['name'];
805
-				if ($view->is_dir($pathDir)) {
806
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
807
-				} else {
808
-					$size += $view->filesize($pathDir);
809
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
810
-					if (!$result) {
811
-						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
812
-					}
813
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
814
-				}
815
-			}
816
-		} else {
817
-			$size += $view->filesize($source);
818
-			$result = $view->copy($source, $destination);
819
-			if (!$result) {
820
-				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
821
-			}
822
-			$view->touch($destination, $view->filemtime($source));
823
-		}
824
-		return $size;
825
-	}
826
-
827
-	/**
828
-	 * find all versions which belong to the file we want to restore
829
-	 *
830
-	 * @param string $filename name of the file which should be restored
831
-	 * @param int $timestamp timestamp when the file was deleted
832
-	 * @return array
833
-	 */
834
-	private static function getVersionsFromTrash($filename, $timestamp, $user) {
835
-		$view = new View('/' . $user . '/files_trashbin/versions');
836
-		$versions = array();
837
-
838
-		//force rescan of versions, local storage may not have updated the cache
839
-		if (!self::$scannedVersions) {
840
-			/** @var \OC\Files\Storage\Storage $storage */
841
-			list($storage,) = $view->resolvePath('/');
842
-			$storage->getScanner()->scan('files_trashbin/versions');
843
-			self::$scannedVersions = true;
844
-		}
845
-
846
-		if ($timestamp) {
847
-			// fetch for old versions
848
-			$matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
849
-			$offset = -strlen($timestamp) - 2;
850
-		} else {
851
-			$matches = $view->searchRaw($filename . '.v%');
852
-		}
853
-
854
-		if (is_array($matches)) {
855
-			foreach ($matches as $ma) {
856
-				if ($timestamp) {
857
-					$parts = explode('.v', substr($ma['path'], 0, $offset));
858
-					$versions[] = (end($parts));
859
-				} else {
860
-					$parts = explode('.v', $ma);
861
-					$versions[] = (end($parts));
862
-				}
863
-			}
864
-		}
865
-		return $versions;
866
-	}
867
-
868
-	/**
869
-	 * find unique extension for restored file if a file with the same name already exists
870
-	 *
871
-	 * @param string $location where the file should be restored
872
-	 * @param string $filename name of the file
873
-	 * @param View $view filesystem view relative to users root directory
874
-	 * @return string with unique extension
875
-	 */
876
-	private static function getUniqueFilename($location, $filename, View $view) {
877
-		$ext = pathinfo($filename, PATHINFO_EXTENSION);
878
-		$name = pathinfo($filename, PATHINFO_FILENAME);
879
-		$l = \OC::$server->getL10N('files_trashbin');
880
-
881
-		$location = '/' . trim($location, '/');
882
-
883
-		// if extension is not empty we set a dot in front of it
884
-		if ($ext !== '') {
885
-			$ext = '.' . $ext;
886
-		}
887
-
888
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
889
-			$i = 2;
890
-			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
891
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
892
-				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
893
-				$i++;
894
-			}
895
-
896
-			return $uniqueName;
897
-		}
898
-
899
-		return $filename;
900
-	}
901
-
902
-	/**
903
-	 * get the size from a given root folder
904
-	 *
905
-	 * @param View $view file view on the root folder
906
-	 * @return integer size of the folder
907
-	 */
908
-	private static function calculateSize($view) {
909
-		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
910
-		if (!file_exists($root)) {
911
-			return 0;
912
-		}
913
-		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
914
-		$size = 0;
915
-
916
-		/**
917
-		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
918
-		 * This bug is fixed in PHP 5.5.9 or before
919
-		 * See #8376
920
-		 */
921
-		$iterator->rewind();
922
-		while ($iterator->valid()) {
923
-			$path = $iterator->current();
924
-			$relpath = substr($path, strlen($root) - 1);
925
-			if (!$view->is_dir($relpath)) {
926
-				$size += $view->filesize($relpath);
927
-			}
928
-			$iterator->next();
929
-		}
930
-		return $size;
931
-	}
932
-
933
-	/**
934
-	 * get current size of trash bin from a given user
935
-	 *
936
-	 * @param string $user user who owns the trash bin
937
-	 * @return integer trash bin size
938
-	 */
939
-	private static function getTrashbinSize($user) {
940
-		$view = new View('/' . $user);
941
-		$fileInfo = $view->getFileInfo('/files_trashbin');
942
-		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
943
-	}
944
-
945
-	/**
946
-	 * register hooks
947
-	 */
948
-	public static function registerHooks() {
949
-		// create storage wrapper on setup
950
-		\OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Files_Trashbin\Storage', 'setupStorage');
951
-		//Listen to delete user signal
952
-		\OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\Files_Trashbin\Hooks', 'deleteUser_hook');
953
-		//Listen to post write hook
954
-		\OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook');
955
-		// pre and post-rename, disable trash logic for the copy+unlink case
956
-		\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook');
957
-		\OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Trashbin\Storage', 'preRenameHook');
958
-		\OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Trashbin\Storage', 'postRenameHook');
959
-	}
960
-
961
-	/**
962
-	 * check if trash bin is empty for a given user
963
-	 *
964
-	 * @param string $user
965
-	 * @return bool
966
-	 */
967
-	public static function isEmpty($user) {
968
-
969
-		$view = new View('/' . $user . '/files_trashbin');
970
-		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
971
-			while ($file = readdir($dh)) {
972
-				if (!Filesystem::isIgnoredDir($file)) {
973
-					return false;
974
-				}
975
-			}
976
-		}
977
-		return true;
978
-	}
979
-
980
-	/**
981
-	 * @param $path
982
-	 * @return string
983
-	 */
984
-	public static function preview_icon($path) {
985
-		return \OCP\Util::linkToRoute('core_ajax_trashbin_preview', array('x' => 32, 'y' => 32, 'file' => $path));
986
-	}
50
+    // unit: percentage; 50% of available disk space/quota
51
+    const DEFAULTMAXSIZE = 50;
52
+
53
+    /**
54
+     * Whether versions have already be rescanned during this PHP request
55
+     *
56
+     * @var bool
57
+     */
58
+    private static $scannedVersions = false;
59
+
60
+    /**
61
+     * Ensure we don't need to scan the file during the move to trash
62
+     * by triggering the scan in the pre-hook
63
+     *
64
+     * @param array $params
65
+     */
66
+    public static function ensureFileScannedHook($params) {
67
+        try {
68
+            self::getUidAndFilename($params['path']);
69
+        } catch (NotFoundException $e) {
70
+            // nothing to scan for non existing files
71
+        }
72
+    }
73
+
74
+    /**
75
+     * get the UID of the owner of the file and the path to the file relative to
76
+     * owners files folder
77
+     *
78
+     * @param string $filename
79
+     * @return array
80
+     * @throws \OC\User\NoUserException
81
+     */
82
+    public static function getUidAndFilename($filename) {
83
+        $uid = Filesystem::getOwner($filename);
84
+        $userManager = \OC::$server->getUserManager();
85
+        // if the user with the UID doesn't exists, e.g. because the UID points
86
+        // to a remote user with a federated cloud ID we use the current logged-in
87
+        // user. We need a valid local user to move the file to the right trash bin
88
+        if (!$userManager->userExists($uid)) {
89
+            $uid = User::getUser();
90
+        }
91
+        if (!$uid) {
92
+            // no owner, usually because of share link from ext storage
93
+            return [null, null];
94
+        }
95
+        Filesystem::initMountPoints($uid);
96
+        if ($uid != User::getUser()) {
97
+            $info = Filesystem::getFileInfo($filename);
98
+            $ownerView = new View('/' . $uid . '/files');
99
+            try {
100
+                $filename = $ownerView->getPath($info['fileid']);
101
+            } catch (NotFoundException $e) {
102
+                $filename = null;
103
+            }
104
+        }
105
+        return [$uid, $filename];
106
+    }
107
+
108
+    /**
109
+     * get original location of files for user
110
+     *
111
+     * @param string $user
112
+     * @return array (filename => array (timestamp => original location))
113
+     */
114
+    public static function getLocations($user) {
115
+        $query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
116
+            . ' FROM `*PREFIX*files_trash` WHERE `user`=?');
117
+        $result = $query->execute(array($user));
118
+        $array = array();
119
+        while ($row = $result->fetchRow()) {
120
+            if (isset($array[$row['id']])) {
121
+                $array[$row['id']][$row['timestamp']] = $row['location'];
122
+            } else {
123
+                $array[$row['id']] = array($row['timestamp'] => $row['location']);
124
+            }
125
+        }
126
+        return $array;
127
+    }
128
+
129
+    /**
130
+     * get original location of file
131
+     *
132
+     * @param string $user
133
+     * @param string $filename
134
+     * @param string $timestamp
135
+     * @return string original location
136
+     */
137
+    public static function getLocation($user, $filename, $timestamp) {
138
+        $query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
139
+            . ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
140
+        $result = $query->execute(array($user, $filename, $timestamp))->fetchAll();
141
+        if (isset($result[0]['location'])) {
142
+            return $result[0]['location'];
143
+        } else {
144
+            return false;
145
+        }
146
+    }
147
+
148
+    private static function setUpTrash($user) {
149
+        $view = new View('/' . $user);
150
+        if (!$view->is_dir('files_trashbin')) {
151
+            $view->mkdir('files_trashbin');
152
+        }
153
+        if (!$view->is_dir('files_trashbin/files')) {
154
+            $view->mkdir('files_trashbin/files');
155
+        }
156
+        if (!$view->is_dir('files_trashbin/versions')) {
157
+            $view->mkdir('files_trashbin/versions');
158
+        }
159
+        if (!$view->is_dir('files_trashbin/keys')) {
160
+            $view->mkdir('files_trashbin/keys');
161
+        }
162
+    }
163
+
164
+
165
+    /**
166
+     * copy file to owners trash
167
+     *
168
+     * @param string $sourcePath
169
+     * @param string $owner
170
+     * @param string $targetPath
171
+     * @param $user
172
+     * @param integer $timestamp
173
+     */
174
+    private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
175
+        self::setUpTrash($owner);
176
+
177
+        $targetFilename = basename($targetPath);
178
+        $targetLocation = dirname($targetPath);
179
+
180
+        $sourceFilename = basename($sourcePath);
181
+
182
+        $view = new View('/');
183
+
184
+        $target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
185
+        $source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
186
+        self::copy_recursive($source, $target, $view);
187
+
188
+
189
+        if ($view->file_exists($target)) {
190
+            $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
191
+            $result = $query->execute(array($targetFilename, $timestamp, $targetLocation, $user));
192
+            if (!$result) {
193
+                \OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated for the files owner', \OCP\Util::ERROR);
194
+            }
195
+        }
196
+    }
197
+
198
+
199
+    /**
200
+     * move file to the trash bin
201
+     *
202
+     * @param string $file_path path to the deleted file/directory relative to the files root directory
203
+     * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
204
+     *
205
+     * @return bool
206
+     */
207
+    public static function move2trash($file_path, $ownerOnly = false) {
208
+        // get the user for which the filesystem is setup
209
+        $root = Filesystem::getRoot();
210
+        list(, $user) = explode('/', $root);
211
+        list($owner, $ownerPath) = self::getUidAndFilename($file_path);
212
+
213
+        // if no owner found (ex: ext storage + share link), will use the current user's trashbin then
214
+        if (is_null($owner)) {
215
+            $owner = $user;
216
+            $ownerPath = $file_path;
217
+        }
218
+
219
+        $ownerView = new View('/' . $owner);
220
+        // file has been deleted in between
221
+        if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
222
+            return true;
223
+        }
224
+
225
+        self::setUpTrash($user);
226
+        if ($owner !== $user) {
227
+            // also setup for owner
228
+            self::setUpTrash($owner);
229
+        }
230
+
231
+        $path_parts = pathinfo($ownerPath);
232
+
233
+        $filename = $path_parts['basename'];
234
+        $location = $path_parts['dirname'];
235
+        $timestamp = time();
236
+
237
+        // disable proxy to prevent recursive calls
238
+        $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
239
+
240
+        /** @var \OC\Files\Storage\Storage $trashStorage */
241
+        list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath);
242
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
243
+        list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath);
244
+        try {
245
+            $moveSuccessful = true;
246
+            if ($trashStorage->file_exists($trashInternalPath)) {
247
+                $trashStorage->unlink($trashInternalPath);
248
+            }
249
+            $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
250
+        } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
251
+            $moveSuccessful = false;
252
+            if ($trashStorage->file_exists($trashInternalPath)) {
253
+                $trashStorage->unlink($trashInternalPath);
254
+            }
255
+            \OCP\Util::writeLog('files_trashbin', 'Couldn\'t move ' . $file_path . ' to the trash bin', \OCP\Util::ERROR);
256
+        }
257
+
258
+        if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
259
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
260
+                $sourceStorage->rmdir($sourceInternalPath);
261
+            } else {
262
+                $sourceStorage->unlink($sourceInternalPath);
263
+            }
264
+            return false;
265
+        }
266
+
267
+        $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
268
+
269
+        if ($moveSuccessful) {
270
+            $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
271
+            $result = $query->execute(array($filename, $timestamp, $location, $owner));
272
+            if (!$result) {
273
+                \OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated', \OCP\Util::ERROR);
274
+            }
275
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', array('filePath' => Filesystem::normalizePath($file_path),
276
+                'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)));
277
+
278
+            self::retainVersions($filename, $owner, $ownerPath, $timestamp);
279
+
280
+            // if owner !== user we need to also add a copy to the users trash
281
+            if ($user !== $owner && $ownerOnly === false) {
282
+                self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
283
+            }
284
+        }
285
+
286
+        self::scheduleExpire($user);
287
+
288
+        // if owner !== user we also need to update the owners trash size
289
+        if ($owner !== $user) {
290
+            self::scheduleExpire($owner);
291
+        }
292
+
293
+        return $moveSuccessful;
294
+    }
295
+
296
+    /**
297
+     * Move file versions to trash so that they can be restored later
298
+     *
299
+     * @param string $filename of deleted file
300
+     * @param string $owner owner user id
301
+     * @param string $ownerPath path relative to the owner's home storage
302
+     * @param integer $timestamp when the file was deleted
303
+     */
304
+    private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
305
+        if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
306
+
307
+            $user = User::getUser();
308
+            $rootView = new View('/');
309
+
310
+            if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
311
+                if ($owner !== $user) {
312
+                    self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
313
+                }
314
+                self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
315
+            } else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
316
+
317
+                foreach ($versions as $v) {
318
+                    if ($owner !== $user) {
319
+                        self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
320
+                    }
321
+                    self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
322
+                }
323
+            }
324
+        }
325
+    }
326
+
327
+    /**
328
+     * Move a file or folder on storage level
329
+     *
330
+     * @param View $view
331
+     * @param string $source
332
+     * @param string $target
333
+     * @return bool
334
+     */
335
+    private static function move(View $view, $source, $target) {
336
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
337
+        list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
338
+        /** @var \OC\Files\Storage\Storage $targetStorage */
339
+        list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
340
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
341
+
342
+        $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
343
+        if ($result) {
344
+            $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
345
+        }
346
+        return $result;
347
+    }
348
+
349
+    /**
350
+     * Copy a file or folder on storage level
351
+     *
352
+     * @param View $view
353
+     * @param string $source
354
+     * @param string $target
355
+     * @return bool
356
+     */
357
+    private static function copy(View $view, $source, $target) {
358
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
359
+        list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source);
360
+        /** @var \OC\Files\Storage\Storage $targetStorage */
361
+        list($targetStorage, $targetInternalPath) = $view->resolvePath($target);
362
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
363
+
364
+        $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
365
+        if ($result) {
366
+            $targetStorage->getUpdater()->update($targetInternalPath);
367
+        }
368
+        return $result;
369
+    }
370
+
371
+    /**
372
+     * Restore a file or folder from trash bin
373
+     *
374
+     * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
375
+     * including the timestamp suffix ".d12345678"
376
+     * @param string $filename name of the file/folder
377
+     * @param int $timestamp time when the file/folder was deleted
378
+     *
379
+     * @return bool true on success, false otherwise
380
+     */
381
+    public static function restore($file, $filename, $timestamp) {
382
+        $user = User::getUser();
383
+        $view = new View('/' . $user);
384
+
385
+        $location = '';
386
+        if ($timestamp) {
387
+            $location = self::getLocation($user, $filename, $timestamp);
388
+            if ($location === false) {
389
+                \OCP\Util::writeLog('files_trashbin', 'trash bin database inconsistent!', \OCP\Util::ERROR);
390
+            } else {
391
+                // if location no longer exists, restore file in the root directory
392
+                if ($location !== '/' &&
393
+                    (!$view->is_dir('files/' . $location) ||
394
+                        !$view->isCreatable('files/' . $location))
395
+                ) {
396
+                    $location = '';
397
+                }
398
+            }
399
+        }
400
+
401
+        // we need a  extension in case a file/dir with the same name already exists
402
+        $uniqueFilename = self::getUniqueFilename($location, $filename, $view);
403
+
404
+        $source = Filesystem::normalizePath('files_trashbin/files/' . $file);
405
+        $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
406
+        if (!$view->file_exists($source)) {
407
+            return false;
408
+        }
409
+        $mtime = $view->filemtime($source);
410
+
411
+        // restore file
412
+        $restoreResult = $view->rename($source, $target);
413
+
414
+        // handle the restore result
415
+        if ($restoreResult) {
416
+            $fakeRoot = $view->getRoot();
417
+            $view->chroot('/' . $user . '/files');
418
+            $view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
419
+            $view->chroot($fakeRoot);
420
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', array('filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
421
+                'trashPath' => Filesystem::normalizePath($file)));
422
+
423
+            self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
424
+
425
+            if ($timestamp) {
426
+                $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
427
+                $query->execute(array($user, $filename, $timestamp));
428
+            }
429
+
430
+            return true;
431
+        }
432
+
433
+        return false;
434
+    }
435
+
436
+    /**
437
+     * restore versions from trash bin
438
+     *
439
+     * @param View $view file view
440
+     * @param string $file complete path to file
441
+     * @param string $filename name of file once it was deleted
442
+     * @param string $uniqueFilename new file name to restore the file without overwriting existing files
443
+     * @param string $location location if file
444
+     * @param int $timestamp deletion time
445
+     * @return false|null
446
+     */
447
+    private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
448
+
449
+        if (\OCP\App::isEnabled('files_versions')) {
450
+
451
+            $user = User::getUser();
452
+            $rootView = new View('/');
453
+
454
+            $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
455
+
456
+            list($owner, $ownerPath) = self::getUidAndFilename($target);
457
+
458
+            // file has been deleted in between
459
+            if (empty($ownerPath)) {
460
+                return false;
461
+            }
462
+
463
+            if ($timestamp) {
464
+                $versionedFile = $filename;
465
+            } else {
466
+                $versionedFile = $file;
467
+            }
468
+
469
+            if ($view->is_dir('/files_trashbin/versions/' . $file)) {
470
+                $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
471
+            } else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
472
+                foreach ($versions as $v) {
473
+                    if ($timestamp) {
474
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
475
+                    } else {
476
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
477
+                    }
478
+                }
479
+            }
480
+        }
481
+    }
482
+
483
+    /**
484
+     * delete all files from the trash
485
+     */
486
+    public static function deleteAll() {
487
+        $user = User::getUser();
488
+        $view = new View('/' . $user);
489
+        $fileInfos = $view->getDirectoryContent('files_trashbin/files');
490
+
491
+        // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
492
+        $filePaths = array();
493
+        foreach($fileInfos as $fileInfo){
494
+            $filePaths[] = $view->getRelativePath($fileInfo->getPath());
495
+        }
496
+        unset($fileInfos); // save memory
497
+
498
+        // Bulk PreDelete-Hook
499
+        \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', array('paths' => $filePaths));
500
+
501
+        // Single-File Hooks
502
+        foreach($filePaths as $path){
503
+            self::emitTrashbinPreDelete($path);
504
+        }
505
+
506
+        // actual file deletion
507
+        $view->deleteAll('files_trashbin');
508
+        $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
509
+        $query->execute(array($user));
510
+
511
+        // Bulk PostDelete-Hook
512
+        \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', array('paths' => $filePaths));
513
+
514
+        // Single-File Hooks
515
+        foreach($filePaths as $path){
516
+            self::emitTrashbinPostDelete($path);
517
+        }
518
+
519
+        $view->mkdir('files_trashbin');
520
+        $view->mkdir('files_trashbin/files');
521
+
522
+        return true;
523
+    }
524
+
525
+    /**
526
+     * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
527
+     * @param string $path
528
+     */
529
+    protected static function emitTrashbinPreDelete($path){
530
+        \OC_Hook::emit('\OCP\Trashbin', 'preDelete', array('path' => $path));
531
+    }
532
+
533
+    /**
534
+     * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
535
+     * @param string $path
536
+     */
537
+    protected static function emitTrashbinPostDelete($path){
538
+        \OC_Hook::emit('\OCP\Trashbin', 'delete', array('path' => $path));
539
+    }
540
+
541
+    /**
542
+     * delete file from trash bin permanently
543
+     *
544
+     * @param string $filename path to the file
545
+     * @param string $user
546
+     * @param int $timestamp of deletion time
547
+     *
548
+     * @return int size of deleted files
549
+     */
550
+    public static function delete($filename, $user, $timestamp = null) {
551
+        $view = new View('/' . $user);
552
+        $size = 0;
553
+
554
+        if ($timestamp) {
555
+            $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
556
+            $query->execute(array($user, $filename, $timestamp));
557
+            $file = $filename . '.d' . $timestamp;
558
+        } else {
559
+            $file = $filename;
560
+        }
561
+
562
+        $size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
563
+
564
+        if ($view->is_dir('/files_trashbin/files/' . $file)) {
565
+            $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
566
+        } else {
567
+            $size += $view->filesize('/files_trashbin/files/' . $file);
568
+        }
569
+        self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
570
+        $view->unlink('/files_trashbin/files/' . $file);
571
+        self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
572
+
573
+        return $size;
574
+    }
575
+
576
+    /**
577
+     * @param View $view
578
+     * @param string $file
579
+     * @param string $filename
580
+     * @param integer|null $timestamp
581
+     * @param string $user
582
+     * @return int
583
+     */
584
+    private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
585
+        $size = 0;
586
+        if (\OCP\App::isEnabled('files_versions')) {
587
+            if ($view->is_dir('files_trashbin/versions/' . $file)) {
588
+                $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
589
+                $view->unlink('files_trashbin/versions/' . $file);
590
+            } else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
591
+                foreach ($versions as $v) {
592
+                    if ($timestamp) {
593
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
594
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
595
+                    } else {
596
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
597
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
598
+                    }
599
+                }
600
+            }
601
+        }
602
+        return $size;
603
+    }
604
+
605
+    /**
606
+     * check to see whether a file exists in trashbin
607
+     *
608
+     * @param string $filename path to the file
609
+     * @param int $timestamp of deletion time
610
+     * @return bool true if file exists, otherwise false
611
+     */
612
+    public static function file_exists($filename, $timestamp = null) {
613
+        $user = User::getUser();
614
+        $view = new View('/' . $user);
615
+
616
+        if ($timestamp) {
617
+            $filename = $filename . '.d' . $timestamp;
618
+        } else {
619
+            $filename = $filename;
620
+        }
621
+
622
+        $target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
623
+        return $view->file_exists($target);
624
+    }
625
+
626
+    /**
627
+     * deletes used space for trash bin in db if user was deleted
628
+     *
629
+     * @param string $uid id of deleted user
630
+     * @return bool result of db delete operation
631
+     */
632
+    public static function deleteUser($uid) {
633
+        $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
634
+        return $query->execute(array($uid));
635
+    }
636
+
637
+    /**
638
+     * calculate remaining free space for trash bin
639
+     *
640
+     * @param integer $trashbinSize current size of the trash bin
641
+     * @param string $user
642
+     * @return int available free space for trash bin
643
+     */
644
+    private static function calculateFreeSpace($trashbinSize, $user) {
645
+        $softQuota = true;
646
+        $userObject = \OC::$server->getUserManager()->get($user);
647
+        if(is_null($userObject)) {
648
+            return 0;
649
+        }
650
+        $quota = $userObject->getQuota();
651
+        if ($quota === null || $quota === 'none') {
652
+            $quota = Filesystem::free_space('/');
653
+            $softQuota = false;
654
+            // inf or unknown free space
655
+            if ($quota < 0) {
656
+                $quota = PHP_INT_MAX;
657
+            }
658
+        } else {
659
+            $quota = \OCP\Util::computerFileSize($quota);
660
+        }
661
+
662
+        // calculate available space for trash bin
663
+        // subtract size of files and current trash bin size from quota
664
+        if ($softQuota) {
665
+            $userFolder = \OC::$server->getUserFolder($user);
666
+            if(is_null($userFolder)) {
667
+                return 0;
668
+            }
669
+            $free = $quota - $userFolder->getSize(); // remaining free space for user
670
+            if ($free > 0) {
671
+                $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
672
+            } else {
673
+                $availableSpace = $free - $trashbinSize;
674
+            }
675
+        } else {
676
+            $availableSpace = $quota;
677
+        }
678
+
679
+        return $availableSpace;
680
+    }
681
+
682
+    /**
683
+     * resize trash bin if necessary after a new file was added to Nextcloud
684
+     *
685
+     * @param string $user user id
686
+     */
687
+    public static function resizeTrash($user) {
688
+
689
+        $size = self::getTrashbinSize($user);
690
+
691
+        $freeSpace = self::calculateFreeSpace($size, $user);
692
+
693
+        if ($freeSpace < 0) {
694
+            self::scheduleExpire($user);
695
+        }
696
+    }
697
+
698
+    /**
699
+     * clean up the trash bin
700
+     *
701
+     * @param string $user
702
+     */
703
+    public static function expire($user) {
704
+        $trashBinSize = self::getTrashbinSize($user);
705
+        $availableSpace = self::calculateFreeSpace($trashBinSize, $user);
706
+
707
+        $dirContent = Helper::getTrashFiles('/', $user, 'mtime');
708
+
709
+        // delete all files older then $retention_obligation
710
+        list($delSize, $count) = self::deleteExpiredFiles($dirContent, $user);
711
+
712
+        $availableSpace += $delSize;
713
+
714
+        // delete files from trash until we meet the trash bin size limit again
715
+        self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
716
+    }
717
+
718
+    /**
719
+     * @param string $user
720
+     */
721
+    private static function scheduleExpire($user) {
722
+        // let the admin disable auto expire
723
+        $application = new Application();
724
+        $expiration = $application->getContainer()->query('Expiration');
725
+        if ($expiration->isEnabled()) {
726
+            \OC::$server->getCommandBus()->push(new Expire($user));
727
+        }
728
+    }
729
+
730
+    /**
731
+     * if the size limit for the trash bin is reached, we delete the oldest
732
+     * files in the trash bin until we meet the limit again
733
+     *
734
+     * @param array $files
735
+     * @param string $user
736
+     * @param int $availableSpace available disc space
737
+     * @return int size of deleted files
738
+     */
739
+    protected static function deleteFiles($files, $user, $availableSpace) {
740
+        $application = new Application();
741
+        $expiration = $application->getContainer()->query('Expiration');
742
+        $size = 0;
743
+
744
+        if ($availableSpace < 0) {
745
+            foreach ($files as $file) {
746
+                if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
747
+                    $tmp = self::delete($file['name'], $user, $file['mtime']);
748
+                    \OCP\Util::writeLog('files_trashbin', 'remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', \OCP\Util::INFO);
749
+                    $availableSpace += $tmp;
750
+                    $size += $tmp;
751
+                } else {
752
+                    break;
753
+                }
754
+            }
755
+        }
756
+        return $size;
757
+    }
758
+
759
+    /**
760
+     * delete files older then max storage time
761
+     *
762
+     * @param array $files list of files sorted by mtime
763
+     * @param string $user
764
+     * @return integer[] size of deleted files and number of deleted files
765
+     */
766
+    public static function deleteExpiredFiles($files, $user) {
767
+        $application = new Application();
768
+        $expiration = $application->getContainer()->query('Expiration');
769
+        $size = 0;
770
+        $count = 0;
771
+        foreach ($files as $file) {
772
+            $timestamp = $file['mtime'];
773
+            $filename = $file['name'];
774
+            if ($expiration->isExpired($timestamp)) {
775
+                $count++;
776
+                $size += self::delete($filename, $user, $timestamp);
777
+                \OC::$server->getLogger()->info(
778
+                    'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
779
+                    ['app' => 'files_trashbin']
780
+                );
781
+            } else {
782
+                break;
783
+            }
784
+        }
785
+
786
+        return array($size, $count);
787
+    }
788
+
789
+    /**
790
+     * recursive copy to copy a whole directory
791
+     *
792
+     * @param string $source source path, relative to the users files directory
793
+     * @param string $destination destination path relative to the users root directoy
794
+     * @param View $view file view for the users root directory
795
+     * @return int
796
+     * @throws Exceptions\CopyRecursiveException
797
+     */
798
+    private static function copy_recursive($source, $destination, View $view) {
799
+        $size = 0;
800
+        if ($view->is_dir($source)) {
801
+            $view->mkdir($destination);
802
+            $view->touch($destination, $view->filemtime($source));
803
+            foreach ($view->getDirectoryContent($source) as $i) {
804
+                $pathDir = $source . '/' . $i['name'];
805
+                if ($view->is_dir($pathDir)) {
806
+                    $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
807
+                } else {
808
+                    $size += $view->filesize($pathDir);
809
+                    $result = $view->copy($pathDir, $destination . '/' . $i['name']);
810
+                    if (!$result) {
811
+                        throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
812
+                    }
813
+                    $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
814
+                }
815
+            }
816
+        } else {
817
+            $size += $view->filesize($source);
818
+            $result = $view->copy($source, $destination);
819
+            if (!$result) {
820
+                throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
821
+            }
822
+            $view->touch($destination, $view->filemtime($source));
823
+        }
824
+        return $size;
825
+    }
826
+
827
+    /**
828
+     * find all versions which belong to the file we want to restore
829
+     *
830
+     * @param string $filename name of the file which should be restored
831
+     * @param int $timestamp timestamp when the file was deleted
832
+     * @return array
833
+     */
834
+    private static function getVersionsFromTrash($filename, $timestamp, $user) {
835
+        $view = new View('/' . $user . '/files_trashbin/versions');
836
+        $versions = array();
837
+
838
+        //force rescan of versions, local storage may not have updated the cache
839
+        if (!self::$scannedVersions) {
840
+            /** @var \OC\Files\Storage\Storage $storage */
841
+            list($storage,) = $view->resolvePath('/');
842
+            $storage->getScanner()->scan('files_trashbin/versions');
843
+            self::$scannedVersions = true;
844
+        }
845
+
846
+        if ($timestamp) {
847
+            // fetch for old versions
848
+            $matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
849
+            $offset = -strlen($timestamp) - 2;
850
+        } else {
851
+            $matches = $view->searchRaw($filename . '.v%');
852
+        }
853
+
854
+        if (is_array($matches)) {
855
+            foreach ($matches as $ma) {
856
+                if ($timestamp) {
857
+                    $parts = explode('.v', substr($ma['path'], 0, $offset));
858
+                    $versions[] = (end($parts));
859
+                } else {
860
+                    $parts = explode('.v', $ma);
861
+                    $versions[] = (end($parts));
862
+                }
863
+            }
864
+        }
865
+        return $versions;
866
+    }
867
+
868
+    /**
869
+     * find unique extension for restored file if a file with the same name already exists
870
+     *
871
+     * @param string $location where the file should be restored
872
+     * @param string $filename name of the file
873
+     * @param View $view filesystem view relative to users root directory
874
+     * @return string with unique extension
875
+     */
876
+    private static function getUniqueFilename($location, $filename, View $view) {
877
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
878
+        $name = pathinfo($filename, PATHINFO_FILENAME);
879
+        $l = \OC::$server->getL10N('files_trashbin');
880
+
881
+        $location = '/' . trim($location, '/');
882
+
883
+        // if extension is not empty we set a dot in front of it
884
+        if ($ext !== '') {
885
+            $ext = '.' . $ext;
886
+        }
887
+
888
+        if ($view->file_exists('files' . $location . '/' . $filename)) {
889
+            $i = 2;
890
+            $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
891
+            while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
892
+                $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
893
+                $i++;
894
+            }
895
+
896
+            return $uniqueName;
897
+        }
898
+
899
+        return $filename;
900
+    }
901
+
902
+    /**
903
+     * get the size from a given root folder
904
+     *
905
+     * @param View $view file view on the root folder
906
+     * @return integer size of the folder
907
+     */
908
+    private static function calculateSize($view) {
909
+        $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
910
+        if (!file_exists($root)) {
911
+            return 0;
912
+        }
913
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
914
+        $size = 0;
915
+
916
+        /**
917
+         * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
918
+         * This bug is fixed in PHP 5.5.9 or before
919
+         * See #8376
920
+         */
921
+        $iterator->rewind();
922
+        while ($iterator->valid()) {
923
+            $path = $iterator->current();
924
+            $relpath = substr($path, strlen($root) - 1);
925
+            if (!$view->is_dir($relpath)) {
926
+                $size += $view->filesize($relpath);
927
+            }
928
+            $iterator->next();
929
+        }
930
+        return $size;
931
+    }
932
+
933
+    /**
934
+     * get current size of trash bin from a given user
935
+     *
936
+     * @param string $user user who owns the trash bin
937
+     * @return integer trash bin size
938
+     */
939
+    private static function getTrashbinSize($user) {
940
+        $view = new View('/' . $user);
941
+        $fileInfo = $view->getFileInfo('/files_trashbin');
942
+        return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
943
+    }
944
+
945
+    /**
946
+     * register hooks
947
+     */
948
+    public static function registerHooks() {
949
+        // create storage wrapper on setup
950
+        \OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Files_Trashbin\Storage', 'setupStorage');
951
+        //Listen to delete user signal
952
+        \OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\Files_Trashbin\Hooks', 'deleteUser_hook');
953
+        //Listen to post write hook
954
+        \OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook');
955
+        // pre and post-rename, disable trash logic for the copy+unlink case
956
+        \OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook');
957
+        \OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Trashbin\Storage', 'preRenameHook');
958
+        \OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Trashbin\Storage', 'postRenameHook');
959
+    }
960
+
961
+    /**
962
+     * check if trash bin is empty for a given user
963
+     *
964
+     * @param string $user
965
+     * @return bool
966
+     */
967
+    public static function isEmpty($user) {
968
+
969
+        $view = new View('/' . $user . '/files_trashbin');
970
+        if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
971
+            while ($file = readdir($dh)) {
972
+                if (!Filesystem::isIgnoredDir($file)) {
973
+                    return false;
974
+                }
975
+            }
976
+        }
977
+        return true;
978
+    }
979
+
980
+    /**
981
+     * @param $path
982
+     * @return string
983
+     */
984
+    public static function preview_icon($path) {
985
+        return \OCP\Util::linkToRoute('core_ajax_trashbin_preview', array('x' => 32, 'y' => 32, 'file' => $path));
986
+    }
987 987
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/ILDAPWrapper.php 1 patch
Indentation   +186 added lines, -186 removed lines patch added patch discarded remove patch
@@ -29,192 +29,192 @@
 block discarded – undo
29 29
 
30 30
 interface ILDAPWrapper {
31 31
 
32
-	//LDAP functions in use
33
-
34
-	/**
35
-	 * Bind to LDAP directory
36
-	 * @param resource $link LDAP link resource
37
-	 * @param string $dn an RDN to log in with
38
-	 * @param string $password the password
39
-	 * @return bool true on success, false otherwise
40
-	 *
41
-	 * with $dn and $password as null a anonymous bind is attempted.
42
-	 */
43
-	public function bind($link, $dn, $password);
44
-
45
-	/**
46
-	 * connect to an LDAP server
47
-	 * @param string $host The host to connect to
48
-	 * @param string $port The port to connect to
49
-	 * @return mixed a link resource on success, otherwise false
50
-	 */
51
-	public function connect($host, $port);
52
-
53
-	/**
54
-	 * Send LDAP pagination control
55
-	 * @param resource $link LDAP link resource
56
-	 * @param int $pageSize number of results per page
57
-	 * @param bool $isCritical Indicates whether the pagination is critical of not.
58
-	 * @param string $cookie structure sent by LDAP server
59
-	 * @return bool true on success, false otherwise
60
-	 */
61
-	public function controlPagedResult($link, $pageSize, $isCritical, $cookie);
62
-
63
-	/**
64
-	 * Retrieve the LDAP pagination cookie
65
-	 * @param resource $link LDAP link resource
66
-	 * @param resource $result LDAP result resource
67
-	 * @param string $cookie structure sent by LDAP server
68
-	 * @return bool true on success, false otherwise
69
-	 *
70
-	 * Corresponds to ldap_control_paged_result_response
71
-	 */
72
-	public function controlPagedResultResponse($link, $result, &$cookie);
73
-
74
-	/**
75
-	 * Count the number of entries in a search
76
-	 * @param resource $link LDAP link resource
77
-	 * @param resource $result LDAP result resource
78
-	 * @return int|false number of results on success, false otherwise
79
-	 */
80
-	public function countEntries($link, $result);
81
-
82
-	/**
83
-	 * Return the LDAP error number of the last LDAP command
84
-	 * @param resource $link LDAP link resource
85
-	 * @return string error message as string
86
-	 */
87
-	public function errno($link);
88
-
89
-	/**
90
-	 * Return the LDAP error message of the last LDAP command
91
-	 * @param resource $link LDAP link resource
92
-	 * @return int error code as integer
93
-	 */
94
-	public function error($link);
95
-
96
-	/**
97
-	 * Splits DN into its component parts
98
-	 * @param string $dn
99
-	 * @param int @withAttrib
100
-	 * @return array|false
101
-	 * @link http://www.php.net/manual/en/function.ldap-explode-dn.php
102
-	 */
103
-	public function explodeDN($dn, $withAttrib);
104
-
105
-	/**
106
-	 * Return first result id
107
-	 * @param resource $link LDAP link resource
108
-	 * @param resource $result LDAP result resource
109
-	 * @return Resource an LDAP search result resource
110
-	 * */
111
-	public function firstEntry($link, $result);
112
-
113
-	/**
114
-	 * Get attributes from a search result entry
115
-	 * @param resource $link LDAP link resource
116
-	 * @param resource $result LDAP result resource
117
-	 * @return array containing the results, false on error
118
-	 * */
119
-	public function getAttributes($link, $result);
120
-
121
-	/**
122
-	 * Get the DN of a result entry
123
-	 * @param resource $link LDAP link resource
124
-	 * @param resource $result LDAP result resource
125
-	 * @return string containing the DN, false on error
126
-	 */
127
-	public function getDN($link, $result);
128
-
129
-	/**
130
-	 * Get all result entries
131
-	 * @param resource $link LDAP link resource
132
-	 * @param resource $result LDAP result resource
133
-	 * @return array containing the results, false on error
134
-	 */
135
-	public function getEntries($link, $result);
136
-
137
-	/**
138
-	 * Return next result id
139
-	 * @param resource $link LDAP link resource
140
-	 * @param resource $result LDAP entry result resource
141
-	 * @return resource an LDAP search result resource
142
-	 * */
143
-	public function nextEntry($link, $result);
144
-
145
-	/**
146
-	 * Read an entry
147
-	 * @param resource $link LDAP link resource
148
-	 * @param array $baseDN The DN of the entry to read from
149
-	 * @param string $filter An LDAP filter
150
-	 * @param array $attr array of the attributes to read
151
-	 * @return resource an LDAP search result resource
152
-	 */
153
-	public function read($link, $baseDN, $filter, $attr);
154
-
155
-	/**
156
-	 * Search LDAP tree
157
-	 * @param resource $link LDAP link resource
158
-	 * @param string $baseDN The DN of the entry to read from
159
-	 * @param string $filter An LDAP filter
160
-	 * @param array $attr array of the attributes to read
161
-	 * @param int $attrsOnly optional, 1 if only attribute types shall be returned
162
-	 * @param int $limit optional, limits the result entries
163
-	 * @return resource|false an LDAP search result resource, false on error
164
-	 */
165
-	public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = 0);
32
+    //LDAP functions in use
33
+
34
+    /**
35
+     * Bind to LDAP directory
36
+     * @param resource $link LDAP link resource
37
+     * @param string $dn an RDN to log in with
38
+     * @param string $password the password
39
+     * @return bool true on success, false otherwise
40
+     *
41
+     * with $dn and $password as null a anonymous bind is attempted.
42
+     */
43
+    public function bind($link, $dn, $password);
44
+
45
+    /**
46
+     * connect to an LDAP server
47
+     * @param string $host The host to connect to
48
+     * @param string $port The port to connect to
49
+     * @return mixed a link resource on success, otherwise false
50
+     */
51
+    public function connect($host, $port);
52
+
53
+    /**
54
+     * Send LDAP pagination control
55
+     * @param resource $link LDAP link resource
56
+     * @param int $pageSize number of results per page
57
+     * @param bool $isCritical Indicates whether the pagination is critical of not.
58
+     * @param string $cookie structure sent by LDAP server
59
+     * @return bool true on success, false otherwise
60
+     */
61
+    public function controlPagedResult($link, $pageSize, $isCritical, $cookie);
62
+
63
+    /**
64
+     * Retrieve the LDAP pagination cookie
65
+     * @param resource $link LDAP link resource
66
+     * @param resource $result LDAP result resource
67
+     * @param string $cookie structure sent by LDAP server
68
+     * @return bool true on success, false otherwise
69
+     *
70
+     * Corresponds to ldap_control_paged_result_response
71
+     */
72
+    public function controlPagedResultResponse($link, $result, &$cookie);
73
+
74
+    /**
75
+     * Count the number of entries in a search
76
+     * @param resource $link LDAP link resource
77
+     * @param resource $result LDAP result resource
78
+     * @return int|false number of results on success, false otherwise
79
+     */
80
+    public function countEntries($link, $result);
81
+
82
+    /**
83
+     * Return the LDAP error number of the last LDAP command
84
+     * @param resource $link LDAP link resource
85
+     * @return string error message as string
86
+     */
87
+    public function errno($link);
88
+
89
+    /**
90
+     * Return the LDAP error message of the last LDAP command
91
+     * @param resource $link LDAP link resource
92
+     * @return int error code as integer
93
+     */
94
+    public function error($link);
95
+
96
+    /**
97
+     * Splits DN into its component parts
98
+     * @param string $dn
99
+     * @param int @withAttrib
100
+     * @return array|false
101
+     * @link http://www.php.net/manual/en/function.ldap-explode-dn.php
102
+     */
103
+    public function explodeDN($dn, $withAttrib);
104
+
105
+    /**
106
+     * Return first result id
107
+     * @param resource $link LDAP link resource
108
+     * @param resource $result LDAP result resource
109
+     * @return Resource an LDAP search result resource
110
+     * */
111
+    public function firstEntry($link, $result);
112
+
113
+    /**
114
+     * Get attributes from a search result entry
115
+     * @param resource $link LDAP link resource
116
+     * @param resource $result LDAP result resource
117
+     * @return array containing the results, false on error
118
+     * */
119
+    public function getAttributes($link, $result);
120
+
121
+    /**
122
+     * Get the DN of a result entry
123
+     * @param resource $link LDAP link resource
124
+     * @param resource $result LDAP result resource
125
+     * @return string containing the DN, false on error
126
+     */
127
+    public function getDN($link, $result);
128
+
129
+    /**
130
+     * Get all result entries
131
+     * @param resource $link LDAP link resource
132
+     * @param resource $result LDAP result resource
133
+     * @return array containing the results, false on error
134
+     */
135
+    public function getEntries($link, $result);
136
+
137
+    /**
138
+     * Return next result id
139
+     * @param resource $link LDAP link resource
140
+     * @param resource $result LDAP entry result resource
141
+     * @return resource an LDAP search result resource
142
+     * */
143
+    public function nextEntry($link, $result);
144
+
145
+    /**
146
+     * Read an entry
147
+     * @param resource $link LDAP link resource
148
+     * @param array $baseDN The DN of the entry to read from
149
+     * @param string $filter An LDAP filter
150
+     * @param array $attr array of the attributes to read
151
+     * @return resource an LDAP search result resource
152
+     */
153
+    public function read($link, $baseDN, $filter, $attr);
154
+
155
+    /**
156
+     * Search LDAP tree
157
+     * @param resource $link LDAP link resource
158
+     * @param string $baseDN The DN of the entry to read from
159
+     * @param string $filter An LDAP filter
160
+     * @param array $attr array of the attributes to read
161
+     * @param int $attrsOnly optional, 1 if only attribute types shall be returned
162
+     * @param int $limit optional, limits the result entries
163
+     * @return resource|false an LDAP search result resource, false on error
164
+     */
165
+    public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = 0);
166 166
 	
167
-	/**
168
-	 * Replace the value of a userPassword by $password
169
-	 * @param resource $link LDAP link resource
170
-	 * @param string $userDN the DN of the user whose password is to be replaced
171
-	 * @param string $password the new value for the userPassword
172
-	 * @return bool true on success, false otherwise
173
-	 */
174
-	public function modReplace($link, $userDN, $password);
175
-
176
-	/**
177
-	 * Sets the value of the specified option to be $value
178
-	 * @param resource $link LDAP link resource
179
-	 * @param string $option a defined LDAP Server option
180
-	 * @param int $value the new value for the option
181
-	 * @return bool true on success, false otherwise
182
-	 */
183
-	public function setOption($link, $option, $value);
184
-
185
-	/**
186
-	 * establish Start TLS
187
-	 * @param resource $link LDAP link resource
188
-	 * @return bool true on success, false otherwise
189
-	 */
190
-	public function startTls($link);
191
-
192
-	/**
193
-	 * Unbind from LDAP directory
194
-	 * @param resource $link LDAP link resource
195
-	 * @return bool true on success, false otherwise
196
-	 */
197
-	public function unbind($link);
198
-
199
-	//additional required methods in Nextcloud
200
-
201
-	/**
202
-	 * Checks whether the server supports LDAP
203
-	 * @return bool true if it the case, false otherwise
204
-	 * */
205
-	public function areLDAPFunctionsAvailable();
206
-
207
-	/**
208
-	 * Checks whether PHP supports LDAP Paged Results
209
-	 * @return bool true if it the case, false otherwise
210
-	 * */
211
-	public function hasPagedResultSupport();
212
-
213
-	/**
214
-	 * Checks whether the submitted parameter is a resource
215
-	 * @param resource $resource the resource variable to check
216
-	 * @return bool true if it is a resource, false otherwise
217
-	 */
218
-	public function isResource($resource);
167
+    /**
168
+     * Replace the value of a userPassword by $password
169
+     * @param resource $link LDAP link resource
170
+     * @param string $userDN the DN of the user whose password is to be replaced
171
+     * @param string $password the new value for the userPassword
172
+     * @return bool true on success, false otherwise
173
+     */
174
+    public function modReplace($link, $userDN, $password);
175
+
176
+    /**
177
+     * Sets the value of the specified option to be $value
178
+     * @param resource $link LDAP link resource
179
+     * @param string $option a defined LDAP Server option
180
+     * @param int $value the new value for the option
181
+     * @return bool true on success, false otherwise
182
+     */
183
+    public function setOption($link, $option, $value);
184
+
185
+    /**
186
+     * establish Start TLS
187
+     * @param resource $link LDAP link resource
188
+     * @return bool true on success, false otherwise
189
+     */
190
+    public function startTls($link);
191
+
192
+    /**
193
+     * Unbind from LDAP directory
194
+     * @param resource $link LDAP link resource
195
+     * @return bool true on success, false otherwise
196
+     */
197
+    public function unbind($link);
198
+
199
+    //additional required methods in Nextcloud
200
+
201
+    /**
202
+     * Checks whether the server supports LDAP
203
+     * @return bool true if it the case, false otherwise
204
+     * */
205
+    public function areLDAPFunctionsAvailable();
206
+
207
+    /**
208
+     * Checks whether PHP supports LDAP Paged Results
209
+     * @return bool true if it the case, false otherwise
210
+     * */
211
+    public function hasPagedResultSupport();
212
+
213
+    /**
214
+     * Checks whether the submitted parameter is a resource
215
+     * @param resource $resource the resource variable to check
216
+     * @return bool true if it is a resource, false otherwise
217
+     */
218
+    public function isResource($resource);
219 219
 
220 220
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Command/ShowRemnants.php 1 patch
Indentation   +52 added lines, -52 removed lines patch added patch discarded remove patch
@@ -35,61 +35,61 @@
 block discarded – undo
35 35
 use OCP\IDateTimeFormatter;
36 36
 
37 37
 class ShowRemnants extends Command {
38
-	/** @var \OCA\User_LDAP\User\DeletedUsersIndex */
39
-	protected $dui;
38
+    /** @var \OCA\User_LDAP\User\DeletedUsersIndex */
39
+    protected $dui;
40 40
 
41
-	/** @var \OCP\IDateTimeFormatter */
42
-	protected $dateFormatter;
41
+    /** @var \OCP\IDateTimeFormatter */
42
+    protected $dateFormatter;
43 43
 
44
-	/**
45
-	 * @param DeletedUsersIndex $dui
46
-	 * @param IDateTimeFormatter $dateFormatter
47
-	 */
48
-	public function __construct(DeletedUsersIndex $dui, IDateTimeFormatter $dateFormatter) {
49
-		$this->dui = $dui;
50
-		$this->dateFormatter = $dateFormatter;
51
-		parent::__construct();
52
-	}
44
+    /**
45
+     * @param DeletedUsersIndex $dui
46
+     * @param IDateTimeFormatter $dateFormatter
47
+     */
48
+    public function __construct(DeletedUsersIndex $dui, IDateTimeFormatter $dateFormatter) {
49
+        $this->dui = $dui;
50
+        $this->dateFormatter = $dateFormatter;
51
+        parent::__construct();
52
+    }
53 53
 
54
-	protected function configure() {
55
-		$this
56
-			->setName('ldap:show-remnants')
57
-			->setDescription('shows which users are not available on LDAP anymore, but have remnants in Nextcloud.')
58
-			->addOption('json', null, InputOption::VALUE_NONE, 'return JSON array instead of pretty table.');
59
-	}
54
+    protected function configure() {
55
+        $this
56
+            ->setName('ldap:show-remnants')
57
+            ->setDescription('shows which users are not available on LDAP anymore, but have remnants in Nextcloud.')
58
+            ->addOption('json', null, InputOption::VALUE_NONE, 'return JSON array instead of pretty table.');
59
+    }
60 60
 
61
-	/**
62
-	 * executes the command, i.e. creeates and outputs a table of LDAP users marked as deleted
63
-	 *
64
-	 * {@inheritdoc}
65
-	 */
66
-	protected function execute(InputInterface $input, OutputInterface $output) {
67
-		/** @var \Symfony\Component\Console\Helper\Table $table */
68
-		$table = new Table($output);
69
-		$table->setHeaders(array(
70
-			'Nextcloud name', 'Display Name', 'LDAP UID', 'LDAP DN', 'Last Login',
71
-			'Dir', 'Sharer'));
72
-		$rows = array();
73
-		$resultSet = $this->dui->getUsers();
74
-		foreach($resultSet as $user) {
75
-			$hAS = $user->getHasActiveShares() ? 'Y' : 'N';
76
-			$lastLogin = ($user->getLastLogin() > 0) ?
77
-				$this->dateFormatter->formatDate($user->getLastLogin()) : '-';
78
-			$rows[] = array('ocName'      => $user->getOCName(),
79
-							'displayName' => $user->getDisplayName(),
80
-							'uid'         => $user->getUID(),
81
-							'dn'          => $user->getDN(),
82
-							'lastLogin'   => $lastLogin,
83
-							'homePath'    => $user->getHomePath(),
84
-							'sharer'      => $hAS
85
-			);
86
-		}
61
+    /**
62
+     * executes the command, i.e. creeates and outputs a table of LDAP users marked as deleted
63
+     *
64
+     * {@inheritdoc}
65
+     */
66
+    protected function execute(InputInterface $input, OutputInterface $output) {
67
+        /** @var \Symfony\Component\Console\Helper\Table $table */
68
+        $table = new Table($output);
69
+        $table->setHeaders(array(
70
+            'Nextcloud name', 'Display Name', 'LDAP UID', 'LDAP DN', 'Last Login',
71
+            'Dir', 'Sharer'));
72
+        $rows = array();
73
+        $resultSet = $this->dui->getUsers();
74
+        foreach($resultSet as $user) {
75
+            $hAS = $user->getHasActiveShares() ? 'Y' : 'N';
76
+            $lastLogin = ($user->getLastLogin() > 0) ?
77
+                $this->dateFormatter->formatDate($user->getLastLogin()) : '-';
78
+            $rows[] = array('ocName'      => $user->getOCName(),
79
+                            'displayName' => $user->getDisplayName(),
80
+                            'uid'         => $user->getUID(),
81
+                            'dn'          => $user->getDN(),
82
+                            'lastLogin'   => $lastLogin,
83
+                            'homePath'    => $user->getHomePath(),
84
+                            'sharer'      => $hAS
85
+            );
86
+        }
87 87
 
88
-		if ($input->getOption('json')) {
89
-			$output->writeln(json_encode($rows));			
90
-		} else {
91
-			$table->setRows($rows);
92
-			$table->render($output);
93
-		}
94
-	}
88
+        if ($input->getOption('json')) {
89
+            $output->writeln(json_encode($rows));			
90
+        } else {
91
+            $table->setRows($rows);
92
+            $table->render($output);
93
+        }
94
+    }
95 95
 }
Please login to merge, or discard this patch.