Completed
Push — master ( d34320...71c2e9 )
by
unknown
27:32
created
lib/private/Files/Storage/Wrapper/Availability.php 1 patch
Indentation   +265 added lines, -265 removed lines patch added patch discarded remove patch
@@ -18,269 +18,269 @@
 block discarded – undo
18 18
  * Throws a StorageNotAvailableException for storages with known failures
19 19
  */
20 20
 class Availability extends Wrapper {
21
-	public const RECHECK_TTL_SEC = 600; // 10 minutes
22
-
23
-	/** @var IConfig */
24
-	protected $config;
25
-	protected ?bool $available = null;
26
-
27
-	public function __construct(array $parameters) {
28
-		$this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class);
29
-		parent::__construct($parameters);
30
-	}
31
-
32
-	public static function shouldRecheck($availability): bool {
33
-		if (!$availability['available']) {
34
-			// trigger a recheck if TTL reached
35
-			if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) {
36
-				return true;
37
-			}
38
-		}
39
-		return false;
40
-	}
41
-
42
-	/**
43
-	 * Only called if availability === false
44
-	 */
45
-	private function updateAvailability(): bool {
46
-		// reset availability to false so that multiple requests don't recheck concurrently
47
-		$this->setAvailability(false);
48
-		try {
49
-			$result = $this->test();
50
-		} catch (\Exception $e) {
51
-			$result = false;
52
-		}
53
-		$this->setAvailability($result);
54
-		return $result;
55
-	}
56
-
57
-	private function isAvailable(): bool {
58
-		if (is_null($this->available)) {
59
-			$availability = $this->getAvailability();
60
-			if (self::shouldRecheck($availability)) {
61
-				return $this->updateAvailability();
62
-			}
63
-			$this->available = $availability['available'];
64
-		}
65
-		return $this->available;
66
-	}
67
-
68
-	/**
69
-	 * @throws StorageNotAvailableException
70
-	 */
71
-	private function checkAvailability(): void {
72
-		if (!$this->isAvailable()) {
73
-			throw new StorageNotAvailableException();
74
-		}
75
-	}
76
-
77
-	/**
78
-	 * Handles availability checks and delegates method calls dynamically
79
-	 */
80
-	private function handleAvailability(string $method, mixed ...$args): mixed {
81
-		$this->checkAvailability();
82
-		try {
83
-			return call_user_func_array([parent::class, $method], $args);
84
-		} catch (StorageNotAvailableException $e) {
85
-			$this->setUnavailable($e);
86
-			return false;
87
-		}
88
-	}
89
-
90
-	public function mkdir(string $path): bool {
91
-		return $this->handleAvailability('mkdir', $path);
92
-	}
93
-
94
-	public function rmdir(string $path): bool {
95
-		return $this->handleAvailability('rmdir', $path);
96
-	}
97
-
98
-	public function opendir(string $path) {
99
-		return $this->handleAvailability('opendir', $path);
100
-	}
101
-
102
-	public function is_dir(string $path): bool {
103
-		return $this->handleAvailability('is_dir', $path);
104
-	}
105
-
106
-	public function is_file(string $path): bool {
107
-		return $this->handleAvailability('is_file', $path);
108
-	}
109
-
110
-	public function stat(string $path): array|false {
111
-		return $this->handleAvailability('stat', $path);
112
-	}
113
-
114
-	public function filetype(string $path): string|false {
115
-		return $this->handleAvailability('filetype', $path);
116
-	}
117
-
118
-	public function filesize(string $path): int|float|false {
119
-		return $this->handleAvailability('filesize', $path);
120
-	}
121
-
122
-	public function isCreatable(string $path): bool {
123
-		return $this->handleAvailability('isCreatable', $path);
124
-	}
125
-
126
-	public function isReadable(string $path): bool {
127
-		return $this->handleAvailability('isReadable', $path);
128
-	}
129
-
130
-	public function isUpdatable(string $path): bool {
131
-		return $this->handleAvailability('isUpdatable', $path);
132
-	}
133
-
134
-	public function isDeletable(string $path): bool {
135
-		return $this->handleAvailability('isDeletable', $path);
136
-	}
137
-
138
-	public function isSharable(string $path): bool {
139
-		return $this->handleAvailability('isSharable', $path);
140
-	}
141
-
142
-	public function getPermissions(string $path): int {
143
-		return $this->handleAvailability('getPermissions', $path);
144
-	}
145
-
146
-	public function file_exists(string $path): bool {
147
-		if ($path === '') {
148
-			return true;
149
-		}
150
-		return $this->handleAvailability('file_exists', $path);
151
-	}
152
-
153
-	public function filemtime(string $path): int|false {
154
-		return $this->handleAvailability('filemtime', $path);
155
-	}
156
-
157
-	public function file_get_contents(string $path): string|false {
158
-		return $this->handleAvailability('file_get_contents', $path);
159
-	}
160
-
161
-	public function file_put_contents(string $path, mixed $data): int|float|false {
162
-		return $this->handleAvailability('file_put_contents', $path, $data);
163
-	}
164
-
165
-	public function unlink(string $path): bool {
166
-		return $this->handleAvailability('unlink', $path);
167
-	}
168
-
169
-	public function rename(string $source, string $target): bool {
170
-		return $this->handleAvailability('rename', $source, $target);
171
-	}
172
-
173
-	public function copy(string $source, string $target): bool {
174
-		return $this->handleAvailability('copy', $source, $target);
175
-	}
176
-
177
-	public function fopen(string $path, string $mode) {
178
-		return $this->handleAvailability('fopen', $path, $mode);
179
-	}
180
-
181
-	public function getMimeType(string $path): string|false {
182
-		return $this->handleAvailability('getMimeType', $path);
183
-	}
184
-
185
-	public function hash(string $type, string $path, bool $raw = false): string|false {
186
-		return $this->handleAvailability('hash', $type, $path, $raw);
187
-	}
188
-
189
-	public function free_space(string $path): int|float|false {
190
-		return $this->handleAvailability('free_space', $path);
191
-	}
192
-
193
-	public function touch(string $path, ?int $mtime = null): bool {
194
-		return $this->handleAvailability('touch', $path, $mtime);
195
-	}
196
-
197
-	public function getLocalFile(string $path): string|false {
198
-		return $this->handleAvailability('getLocalFile', $path);
199
-	}
200
-
201
-	public function hasUpdated(string $path, int $time): bool {
202
-		if (!$this->isAvailable()) {
203
-			return false;
204
-		}
205
-		try {
206
-			return parent::hasUpdated($path, $time);
207
-		} catch (StorageNotAvailableException $e) {
208
-			// set unavailable but don't rethrow
209
-			$this->setUnavailable(null);
210
-			return false;
211
-		}
212
-	}
213
-
214
-	public function getOwner(string $path): string|false {
215
-		try {
216
-			return parent::getOwner($path);
217
-		} catch (StorageNotAvailableException $e) {
218
-			$this->setUnavailable($e);
219
-			return false;
220
-		}
221
-	}
222
-
223
-	public function getETag(string $path): string|false {
224
-		return $this->handleAvailability('getETag', $path);
225
-	}
226
-
227
-	public function getDirectDownload(string $path): array|false {
228
-		return $this->handleAvailability('getDirectDownload', $path);
229
-	}
230
-
231
-	public function getDirectDownloadById(string $fileId): array|false {
232
-		return $this->handleAvailability('getDirectDownloadById', $fileId);
233
-	}
234
-
235
-	public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
236
-		return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
237
-	}
238
-
239
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
240
-		return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
241
-	}
242
-
243
-	public function getMetaData(string $path): ?array {
244
-		$this->checkAvailability();
245
-		try {
246
-			return parent::getMetaData($path);
247
-		} catch (StorageNotAvailableException $e) {
248
-			$this->setUnavailable($e);
249
-			return null;
250
-		}
251
-	}
252
-
253
-	/**
254
-	 * @template T of StorageNotAvailableException|null
255
-	 * @param T $e
256
-	 * @psalm-return (T is null ? void : never)
257
-	 * @throws StorageNotAvailableException
258
-	 */
259
-	protected function setUnavailable(?StorageNotAvailableException $e): void {
260
-		$delay = self::RECHECK_TTL_SEC;
261
-		if ($e instanceof StorageAuthException) {
262
-			$delay = max(
263
-				// 30min
264
-				$this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800),
265
-				self::RECHECK_TTL_SEC
266
-			);
267
-		}
268
-		$this->available = false;
269
-		$this->getStorageCache()->setAvailability(false, $delay);
270
-		if ($e !== null) {
271
-			throw $e;
272
-		}
273
-	}
274
-
275
-
276
-
277
-	public function getDirectoryContent(string $directory): \Traversable {
278
-		$this->checkAvailability();
279
-		try {
280
-			return parent::getDirectoryContent($directory);
281
-		} catch (StorageNotAvailableException $e) {
282
-			$this->setUnavailable($e);
283
-			return new \EmptyIterator();
284
-		}
285
-	}
21
+    public const RECHECK_TTL_SEC = 600; // 10 minutes
22
+
23
+    /** @var IConfig */
24
+    protected $config;
25
+    protected ?bool $available = null;
26
+
27
+    public function __construct(array $parameters) {
28
+        $this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class);
29
+        parent::__construct($parameters);
30
+    }
31
+
32
+    public static function shouldRecheck($availability): bool {
33
+        if (!$availability['available']) {
34
+            // trigger a recheck if TTL reached
35
+            if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) {
36
+                return true;
37
+            }
38
+        }
39
+        return false;
40
+    }
41
+
42
+    /**
43
+     * Only called if availability === false
44
+     */
45
+    private function updateAvailability(): bool {
46
+        // reset availability to false so that multiple requests don't recheck concurrently
47
+        $this->setAvailability(false);
48
+        try {
49
+            $result = $this->test();
50
+        } catch (\Exception $e) {
51
+            $result = false;
52
+        }
53
+        $this->setAvailability($result);
54
+        return $result;
55
+    }
56
+
57
+    private function isAvailable(): bool {
58
+        if (is_null($this->available)) {
59
+            $availability = $this->getAvailability();
60
+            if (self::shouldRecheck($availability)) {
61
+                return $this->updateAvailability();
62
+            }
63
+            $this->available = $availability['available'];
64
+        }
65
+        return $this->available;
66
+    }
67
+
68
+    /**
69
+     * @throws StorageNotAvailableException
70
+     */
71
+    private function checkAvailability(): void {
72
+        if (!$this->isAvailable()) {
73
+            throw new StorageNotAvailableException();
74
+        }
75
+    }
76
+
77
+    /**
78
+     * Handles availability checks and delegates method calls dynamically
79
+     */
80
+    private function handleAvailability(string $method, mixed ...$args): mixed {
81
+        $this->checkAvailability();
82
+        try {
83
+            return call_user_func_array([parent::class, $method], $args);
84
+        } catch (StorageNotAvailableException $e) {
85
+            $this->setUnavailable($e);
86
+            return false;
87
+        }
88
+    }
89
+
90
+    public function mkdir(string $path): bool {
91
+        return $this->handleAvailability('mkdir', $path);
92
+    }
93
+
94
+    public function rmdir(string $path): bool {
95
+        return $this->handleAvailability('rmdir', $path);
96
+    }
97
+
98
+    public function opendir(string $path) {
99
+        return $this->handleAvailability('opendir', $path);
100
+    }
101
+
102
+    public function is_dir(string $path): bool {
103
+        return $this->handleAvailability('is_dir', $path);
104
+    }
105
+
106
+    public function is_file(string $path): bool {
107
+        return $this->handleAvailability('is_file', $path);
108
+    }
109
+
110
+    public function stat(string $path): array|false {
111
+        return $this->handleAvailability('stat', $path);
112
+    }
113
+
114
+    public function filetype(string $path): string|false {
115
+        return $this->handleAvailability('filetype', $path);
116
+    }
117
+
118
+    public function filesize(string $path): int|float|false {
119
+        return $this->handleAvailability('filesize', $path);
120
+    }
121
+
122
+    public function isCreatable(string $path): bool {
123
+        return $this->handleAvailability('isCreatable', $path);
124
+    }
125
+
126
+    public function isReadable(string $path): bool {
127
+        return $this->handleAvailability('isReadable', $path);
128
+    }
129
+
130
+    public function isUpdatable(string $path): bool {
131
+        return $this->handleAvailability('isUpdatable', $path);
132
+    }
133
+
134
+    public function isDeletable(string $path): bool {
135
+        return $this->handleAvailability('isDeletable', $path);
136
+    }
137
+
138
+    public function isSharable(string $path): bool {
139
+        return $this->handleAvailability('isSharable', $path);
140
+    }
141
+
142
+    public function getPermissions(string $path): int {
143
+        return $this->handleAvailability('getPermissions', $path);
144
+    }
145
+
146
+    public function file_exists(string $path): bool {
147
+        if ($path === '') {
148
+            return true;
149
+        }
150
+        return $this->handleAvailability('file_exists', $path);
151
+    }
152
+
153
+    public function filemtime(string $path): int|false {
154
+        return $this->handleAvailability('filemtime', $path);
155
+    }
156
+
157
+    public function file_get_contents(string $path): string|false {
158
+        return $this->handleAvailability('file_get_contents', $path);
159
+    }
160
+
161
+    public function file_put_contents(string $path, mixed $data): int|float|false {
162
+        return $this->handleAvailability('file_put_contents', $path, $data);
163
+    }
164
+
165
+    public function unlink(string $path): bool {
166
+        return $this->handleAvailability('unlink', $path);
167
+    }
168
+
169
+    public function rename(string $source, string $target): bool {
170
+        return $this->handleAvailability('rename', $source, $target);
171
+    }
172
+
173
+    public function copy(string $source, string $target): bool {
174
+        return $this->handleAvailability('copy', $source, $target);
175
+    }
176
+
177
+    public function fopen(string $path, string $mode) {
178
+        return $this->handleAvailability('fopen', $path, $mode);
179
+    }
180
+
181
+    public function getMimeType(string $path): string|false {
182
+        return $this->handleAvailability('getMimeType', $path);
183
+    }
184
+
185
+    public function hash(string $type, string $path, bool $raw = false): string|false {
186
+        return $this->handleAvailability('hash', $type, $path, $raw);
187
+    }
188
+
189
+    public function free_space(string $path): int|float|false {
190
+        return $this->handleAvailability('free_space', $path);
191
+    }
192
+
193
+    public function touch(string $path, ?int $mtime = null): bool {
194
+        return $this->handleAvailability('touch', $path, $mtime);
195
+    }
196
+
197
+    public function getLocalFile(string $path): string|false {
198
+        return $this->handleAvailability('getLocalFile', $path);
199
+    }
200
+
201
+    public function hasUpdated(string $path, int $time): bool {
202
+        if (!$this->isAvailable()) {
203
+            return false;
204
+        }
205
+        try {
206
+            return parent::hasUpdated($path, $time);
207
+        } catch (StorageNotAvailableException $e) {
208
+            // set unavailable but don't rethrow
209
+            $this->setUnavailable(null);
210
+            return false;
211
+        }
212
+    }
213
+
214
+    public function getOwner(string $path): string|false {
215
+        try {
216
+            return parent::getOwner($path);
217
+        } catch (StorageNotAvailableException $e) {
218
+            $this->setUnavailable($e);
219
+            return false;
220
+        }
221
+    }
222
+
223
+    public function getETag(string $path): string|false {
224
+        return $this->handleAvailability('getETag', $path);
225
+    }
226
+
227
+    public function getDirectDownload(string $path): array|false {
228
+        return $this->handleAvailability('getDirectDownload', $path);
229
+    }
230
+
231
+    public function getDirectDownloadById(string $fileId): array|false {
232
+        return $this->handleAvailability('getDirectDownloadById', $fileId);
233
+    }
234
+
235
+    public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
236
+        return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
237
+    }
238
+
239
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
240
+        return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
241
+    }
242
+
243
+    public function getMetaData(string $path): ?array {
244
+        $this->checkAvailability();
245
+        try {
246
+            return parent::getMetaData($path);
247
+        } catch (StorageNotAvailableException $e) {
248
+            $this->setUnavailable($e);
249
+            return null;
250
+        }
251
+    }
252
+
253
+    /**
254
+     * @template T of StorageNotAvailableException|null
255
+     * @param T $e
256
+     * @psalm-return (T is null ? void : never)
257
+     * @throws StorageNotAvailableException
258
+     */
259
+    protected function setUnavailable(?StorageNotAvailableException $e): void {
260
+        $delay = self::RECHECK_TTL_SEC;
261
+        if ($e instanceof StorageAuthException) {
262
+            $delay = max(
263
+                // 30min
264
+                $this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800),
265
+                self::RECHECK_TTL_SEC
266
+            );
267
+        }
268
+        $this->available = false;
269
+        $this->getStorageCache()->setAvailability(false, $delay);
270
+        if ($e !== null) {
271
+            throw $e;
272
+        }
273
+    }
274
+
275
+
276
+
277
+    public function getDirectoryContent(string $directory): \Traversable {
278
+        $this->checkAvailability();
279
+        try {
280
+            return parent::getDirectoryContent($directory);
281
+        } catch (StorageNotAvailableException $e) {
282
+            $this->setUnavailable($e);
283
+            return new \EmptyIterator();
284
+        }
285
+    }
286 286
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/FailedStorage.php 1 patch
Indentation   +159 added lines, -159 removed lines patch added patch discarded remove patch
@@ -16,180 +16,180 @@
 block discarded – undo
16 16
  * Storage placeholder to represent a missing precondition, storage unavailable
17 17
  */
18 18
 class FailedStorage extends Common {
19
-	/** @var \Exception */
20
-	protected $e;
21
-
22
-	/**
23
-	 * @param array $parameters ['exception' => \Exception]
24
-	 */
25
-	public function __construct(array $parameters) {
26
-		$this->e = $parameters['exception'];
27
-		if (!$this->e) {
28
-			throw new \InvalidArgumentException('Missing "exception" argument in FailedStorage constructor');
29
-		}
30
-	}
31
-
32
-	public function getId(): string {
33
-		// we can't return anything sane here
34
-		return 'failedstorage';
35
-	}
36
-
37
-	public function mkdir(string $path): never {
38
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
39
-	}
40
-
41
-	public function rmdir(string $path): never {
42
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
43
-	}
44
-
45
-	public function opendir(string $path): never {
46
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
47
-	}
48
-
49
-	public function is_dir(string $path): never {
50
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
51
-	}
52
-
53
-	public function is_file(string $path): never {
54
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
55
-	}
56
-
57
-	public function stat(string $path): never {
58
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
59
-	}
60
-
61
-	public function filetype(string $path): never {
62
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
63
-	}
64
-
65
-	public function filesize(string $path): never {
66
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
67
-	}
68
-
69
-	public function isCreatable(string $path): never {
70
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
71
-	}
72
-
73
-	public function isReadable(string $path): never {
74
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
75
-	}
76
-
77
-	public function isUpdatable(string $path): never {
78
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
79
-	}
80
-
81
-	public function isDeletable(string $path): never {
82
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
83
-	}
84
-
85
-	public function isSharable(string $path): never {
86
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
87
-	}
88
-
89
-	public function getPermissions(string $path): never {
90
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
91
-	}
92
-
93
-	public function file_exists(string $path): never {
94
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
95
-	}
96
-
97
-	public function filemtime(string $path): never {
98
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
99
-	}
100
-
101
-	public function file_get_contents(string $path): never {
102
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
103
-	}
104
-
105
-	public function file_put_contents(string $path, mixed $data): never {
106
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
107
-	}
108
-
109
-	public function unlink(string $path): never {
110
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
111
-	}
112
-
113
-	public function rename(string $source, string $target): never {
114
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
115
-	}
116
-
117
-	public function copy(string $source, string $target): never {
118
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
119
-	}
120
-
121
-	public function fopen(string $path, string $mode): never {
122
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
123
-	}
124
-
125
-	public function getMimeType(string $path): never {
126
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
127
-	}
19
+    /** @var \Exception */
20
+    protected $e;
21
+
22
+    /**
23
+     * @param array $parameters ['exception' => \Exception]
24
+     */
25
+    public function __construct(array $parameters) {
26
+        $this->e = $parameters['exception'];
27
+        if (!$this->e) {
28
+            throw new \InvalidArgumentException('Missing "exception" argument in FailedStorage constructor');
29
+        }
30
+    }
31
+
32
+    public function getId(): string {
33
+        // we can't return anything sane here
34
+        return 'failedstorage';
35
+    }
36
+
37
+    public function mkdir(string $path): never {
38
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
39
+    }
40
+
41
+    public function rmdir(string $path): never {
42
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
43
+    }
44
+
45
+    public function opendir(string $path): never {
46
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
47
+    }
48
+
49
+    public function is_dir(string $path): never {
50
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
51
+    }
52
+
53
+    public function is_file(string $path): never {
54
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
55
+    }
56
+
57
+    public function stat(string $path): never {
58
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
59
+    }
60
+
61
+    public function filetype(string $path): never {
62
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
63
+    }
64
+
65
+    public function filesize(string $path): never {
66
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
67
+    }
68
+
69
+    public function isCreatable(string $path): never {
70
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
71
+    }
72
+
73
+    public function isReadable(string $path): never {
74
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
75
+    }
76
+
77
+    public function isUpdatable(string $path): never {
78
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
79
+    }
80
+
81
+    public function isDeletable(string $path): never {
82
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
83
+    }
84
+
85
+    public function isSharable(string $path): never {
86
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
87
+    }
88
+
89
+    public function getPermissions(string $path): never {
90
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
91
+    }
92
+
93
+    public function file_exists(string $path): never {
94
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
95
+    }
96
+
97
+    public function filemtime(string $path): never {
98
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
99
+    }
100
+
101
+    public function file_get_contents(string $path): never {
102
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
103
+    }
104
+
105
+    public function file_put_contents(string $path, mixed $data): never {
106
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
107
+    }
108
+
109
+    public function unlink(string $path): never {
110
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
111
+    }
112
+
113
+    public function rename(string $source, string $target): never {
114
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
115
+    }
116
+
117
+    public function copy(string $source, string $target): never {
118
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
119
+    }
120
+
121
+    public function fopen(string $path, string $mode): never {
122
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
123
+    }
124
+
125
+    public function getMimeType(string $path): never {
126
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
127
+    }
128 128
 
129
-	public function hash(string $type, string $path, bool $raw = false): never {
130
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
131
-	}
129
+    public function hash(string $type, string $path, bool $raw = false): never {
130
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
131
+    }
132 132
 
133
-	public function free_space(string $path): never {
134
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
135
-	}
133
+    public function free_space(string $path): never {
134
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
135
+    }
136 136
 
137
-	public function touch(string $path, ?int $mtime = null): never {
138
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
139
-	}
137
+    public function touch(string $path, ?int $mtime = null): never {
138
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
139
+    }
140 140
 
141
-	public function getLocalFile(string $path): never {
142
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
143
-	}
141
+    public function getLocalFile(string $path): never {
142
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
143
+    }
144 144
 
145
-	public function hasUpdated(string $path, int $time): never {
146
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
147
-	}
145
+    public function hasUpdated(string $path, int $time): never {
146
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
147
+    }
148 148
 
149
-	public function getETag(string $path): never {
150
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
151
-	}
149
+    public function getETag(string $path): never {
150
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
151
+    }
152 152
 
153
-	public function getDirectDownload(string $path): never {
154
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
155
-	}
153
+    public function getDirectDownload(string $path): never {
154
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
155
+    }
156 156
 
157
-	public function getDirectDownloadById(string $fileId): never {
158
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
159
-	}
157
+    public function getDirectDownloadById(string $fileId): never {
158
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
159
+    }
160 160
 
161
-	public function verifyPath(string $path, string $fileName): void {
162
-	}
161
+    public function verifyPath(string $path, string $fileName): void {
162
+    }
163 163
 
164
-	public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): never {
165
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
166
-	}
164
+    public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): never {
165
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
166
+    }
167 167
 
168
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): never {
169
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
170
-	}
168
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): never {
169
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
170
+    }
171 171
 
172
-	public function acquireLock(string $path, int $type, ILockingProvider $provider): never {
173
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
174
-	}
172
+    public function acquireLock(string $path, int $type, ILockingProvider $provider): never {
173
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
174
+    }
175 175
 
176
-	public function releaseLock(string $path, int $type, ILockingProvider $provider): never {
177
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
178
-	}
176
+    public function releaseLock(string $path, int $type, ILockingProvider $provider): never {
177
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
178
+    }
179 179
 
180
-	public function changeLock(string $path, int $type, ILockingProvider $provider): never {
181
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
182
-	}
180
+    public function changeLock(string $path, int $type, ILockingProvider $provider): never {
181
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
182
+    }
183 183
 
184
-	public function getAvailability(): never {
185
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
186
-	}
184
+    public function getAvailability(): never {
185
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
186
+    }
187 187
 
188
-	public function setAvailability(bool $isAvailable): never {
189
-		throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
190
-	}
188
+    public function setAvailability(bool $isAvailable): never {
189
+        throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e);
190
+    }
191 191
 
192
-	public function getCache(string $path = '', ?IStorage $storage = null): FailedCache {
193
-		return new FailedCache();
194
-	}
192
+    public function getCache(string $path = '', ?IStorage $storage = null): FailedCache {
193
+        return new FailedCache();
194
+    }
195 195
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/Common.php 1 patch
Indentation   +715 added lines, -715 removed lines patch added patch discarded remove patch
@@ -53,719 +53,719 @@
 block discarded – undo
53 53
  * in classes which extend it, e.g. $this->stat() .
54 54
  */
55 55
 abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage, IConstructableStorage {
56
-	use LocalTempFileTrait;
57
-
58
-	protected ?Cache $cache = null;
59
-	protected ?Scanner $scanner = null;
60
-	protected ?Watcher $watcher = null;
61
-	protected ?Propagator $propagator = null;
62
-	protected $storageCache;
63
-	protected ?Updater $updater = null;
64
-
65
-	protected array $mountOptions = [];
66
-	protected $owner = null;
67
-
68
-	private ?bool $shouldLogLocks = null;
69
-	private ?LoggerInterface $logger = null;
70
-	private ?IFilenameValidator $filenameValidator = null;
71
-
72
-	public function __construct(array $parameters) {
73
-	}
74
-
75
-	protected function remove(string $path): bool {
76
-		if ($this->file_exists($path)) {
77
-			if ($this->is_dir($path)) {
78
-				return $this->rmdir($path);
79
-			} elseif ($this->is_file($path)) {
80
-				return $this->unlink($path);
81
-			}
82
-		}
83
-		return false;
84
-	}
85
-
86
-	public function is_dir(string $path): bool {
87
-		return $this->filetype($path) === 'dir';
88
-	}
89
-
90
-	public function is_file(string $path): bool {
91
-		return $this->filetype($path) === 'file';
92
-	}
93
-
94
-	public function filesize(string $path): int|float|false {
95
-		if ($this->is_dir($path)) {
96
-			return 0; //by definition
97
-		} else {
98
-			$stat = $this->stat($path);
99
-			return isset($stat['size']) ? $stat['size'] : 0;
100
-		}
101
-	}
102
-
103
-	public function isReadable(string $path): bool {
104
-		// at least check whether it exists
105
-		// subclasses might want to implement this more thoroughly
106
-		return $this->file_exists($path);
107
-	}
108
-
109
-	public function isUpdatable(string $path): bool {
110
-		// at least check whether it exists
111
-		// subclasses might want to implement this more thoroughly
112
-		// a non-existing file/folder isn't updatable
113
-		return $this->file_exists($path);
114
-	}
115
-
116
-	public function isCreatable(string $path): bool {
117
-		if ($this->is_dir($path) && $this->isUpdatable($path)) {
118
-			return true;
119
-		}
120
-		return false;
121
-	}
122
-
123
-	public function isDeletable(string $path): bool {
124
-		if ($path === '' || $path === '/') {
125
-			return $this->isUpdatable($path);
126
-		}
127
-		$parent = dirname($path);
128
-		return $this->isUpdatable($parent) && $this->isUpdatable($path);
129
-	}
130
-
131
-	public function isSharable(string $path): bool {
132
-		return $this->isReadable($path);
133
-	}
134
-
135
-	public function getPermissions(string $path): int {
136
-		$permissions = 0;
137
-		if ($this->isCreatable($path)) {
138
-			$permissions |= \OCP\Constants::PERMISSION_CREATE;
139
-		}
140
-		if ($this->isReadable($path)) {
141
-			$permissions |= \OCP\Constants::PERMISSION_READ;
142
-		}
143
-		if ($this->isUpdatable($path)) {
144
-			$permissions |= \OCP\Constants::PERMISSION_UPDATE;
145
-		}
146
-		if ($this->isDeletable($path)) {
147
-			$permissions |= \OCP\Constants::PERMISSION_DELETE;
148
-		}
149
-		if ($this->isSharable($path)) {
150
-			$permissions |= \OCP\Constants::PERMISSION_SHARE;
151
-		}
152
-		return $permissions;
153
-	}
154
-
155
-	public function filemtime(string $path): int|false {
156
-		$stat = $this->stat($path);
157
-		if (isset($stat['mtime']) && $stat['mtime'] > 0) {
158
-			return $stat['mtime'];
159
-		} else {
160
-			return 0;
161
-		}
162
-	}
163
-
164
-	public function file_get_contents(string $path): string|false {
165
-		$handle = $this->fopen($path, 'r');
166
-		if (!$handle) {
167
-			return false;
168
-		}
169
-		$data = stream_get_contents($handle);
170
-		fclose($handle);
171
-		return $data;
172
-	}
173
-
174
-	public function file_put_contents(string $path, mixed $data): int|float|false {
175
-		$handle = $this->fopen($path, 'w');
176
-		if (!$handle) {
177
-			return false;
178
-		}
179
-		$this->removeCachedFile($path);
180
-		$count = fwrite($handle, $data);
181
-		fclose($handle);
182
-		return $count;
183
-	}
184
-
185
-	public function rename(string $source, string $target): bool {
186
-		$this->remove($target);
187
-
188
-		$this->removeCachedFile($source);
189
-		return $this->copy($source, $target) && $this->remove($source);
190
-	}
191
-
192
-	public function copy(string $source, string $target): bool {
193
-		if ($this->is_dir($source)) {
194
-			$this->remove($target);
195
-			$dir = $this->opendir($source);
196
-			$this->mkdir($target);
197
-			while (($file = readdir($dir)) !== false) {
198
-				if (!Filesystem::isIgnoredDir($file)) {
199
-					if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
200
-						closedir($dir);
201
-						return false;
202
-					}
203
-				}
204
-			}
205
-			closedir($dir);
206
-			return true;
207
-		} else {
208
-			$sourceStream = $this->fopen($source, 'r');
209
-			$targetStream = $this->fopen($target, 'w');
210
-			[, $result] = Files::streamCopy($sourceStream, $targetStream, true);
211
-			if (!$result) {
212
-				Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
213
-			}
214
-			$this->removeCachedFile($target);
215
-			return $result;
216
-		}
217
-	}
218
-
219
-	public function getMimeType(string $path): string|false {
220
-		if ($this->is_dir($path)) {
221
-			return 'httpd/unix-directory';
222
-		} elseif ($this->file_exists($path)) {
223
-			return \OC::$server->getMimeTypeDetector()->detectPath($path);
224
-		} else {
225
-			return false;
226
-		}
227
-	}
228
-
229
-	public function hash(string $type, string $path, bool $raw = false): string|false {
230
-		$fh = $this->fopen($path, 'rb');
231
-		if (!$fh) {
232
-			return false;
233
-		}
234
-		$ctx = hash_init($type);
235
-		hash_update_stream($ctx, $fh);
236
-		fclose($fh);
237
-		return hash_final($ctx, $raw);
238
-	}
239
-
240
-	public function getLocalFile(string $path): string|false {
241
-		return $this->getCachedFile($path);
242
-	}
243
-
244
-	private function addLocalFolder(string $path, string $target): void {
245
-		$dh = $this->opendir($path);
246
-		if (is_resource($dh)) {
247
-			while (($file = readdir($dh)) !== false) {
248
-				if (!Filesystem::isIgnoredDir($file)) {
249
-					if ($this->is_dir($path . '/' . $file)) {
250
-						mkdir($target . '/' . $file);
251
-						$this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
252
-					} else {
253
-						$tmp = $this->toTmpFile($path . '/' . $file);
254
-						rename($tmp, $target . '/' . $file);
255
-					}
256
-				}
257
-			}
258
-		}
259
-	}
260
-
261
-	protected function searchInDir(string $query, string $dir = ''): array {
262
-		$files = [];
263
-		$dh = $this->opendir($dir);
264
-		if (is_resource($dh)) {
265
-			while (($item = readdir($dh)) !== false) {
266
-				if (Filesystem::isIgnoredDir($item)) {
267
-					continue;
268
-				}
269
-				if (strstr(strtolower($item), strtolower($query)) !== false) {
270
-					$files[] = $dir . '/' . $item;
271
-				}
272
-				if ($this->is_dir($dir . '/' . $item)) {
273
-					$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
274
-				}
275
-			}
276
-		}
277
-		closedir($dh);
278
-		return $files;
279
-	}
280
-
281
-	/**
282
-	 * @inheritDoc
283
-	 * Check if a file or folder has been updated since $time
284
-	 *
285
-	 * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
286
-	 * the mtime should always return false here. As a result storage implementations that always return false expect
287
-	 * exclusive access to the backend and will not pick up files that have been added in a way that circumvents
288
-	 * Nextcloud filesystem.
289
-	 */
290
-	public function hasUpdated(string $path, int $time): bool {
291
-		return $this->filemtime($path) > $time;
292
-	}
293
-
294
-	protected function getCacheDependencies(): CacheDependencies {
295
-		static $dependencies = null;
296
-		if (!$dependencies) {
297
-			$dependencies = Server::get(CacheDependencies::class);
298
-		}
299
-		return $dependencies;
300
-	}
301
-
302
-	public function getCache(string $path = '', ?IStorage $storage = null): ICache {
303
-		if (!$storage) {
304
-			$storage = $this;
305
-		}
306
-		/** @var self $storage */
307
-		if (!isset($storage->cache)) {
308
-			$storage->cache = new Cache($storage, $this->getCacheDependencies());
309
-		}
310
-		return $storage->cache;
311
-	}
312
-
313
-	public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
314
-		if (!$storage) {
315
-			$storage = $this;
316
-		}
317
-		if (!$storage->instanceOfStorage(self::class)) {
318
-			throw new \InvalidArgumentException('Storage is not of the correct class');
319
-		}
320
-		if (!isset($storage->scanner)) {
321
-			$storage->scanner = new Scanner($storage);
322
-		}
323
-		return $storage->scanner;
324
-	}
325
-
326
-	public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
327
-		if (!$storage) {
328
-			$storage = $this;
329
-		}
330
-		if (!isset($this->watcher)) {
331
-			$this->watcher = new Watcher($storage);
332
-			$globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER);
333
-			$this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
334
-		}
335
-		return $this->watcher;
336
-	}
337
-
338
-	public function getPropagator(?IStorage $storage = null): IPropagator {
339
-		if (!$storage) {
340
-			$storage = $this;
341
-		}
342
-		if (!$storage->instanceOfStorage(self::class)) {
343
-			throw new \InvalidArgumentException('Storage is not of the correct class');
344
-		}
345
-		/** @var self $storage */
346
-		if (!isset($storage->propagator)) {
347
-			$config = Server::get(IConfig::class);
348
-			$storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]);
349
-		}
350
-		return $storage->propagator;
351
-	}
352
-
353
-	public function getUpdater(?IStorage $storage = null): IUpdater {
354
-		if (!$storage) {
355
-			$storage = $this;
356
-		}
357
-		if (!$storage->instanceOfStorage(self::class)) {
358
-			throw new \InvalidArgumentException('Storage is not of the correct class');
359
-		}
360
-		/** @var self $storage */
361
-		if (!isset($storage->updater)) {
362
-			$storage->updater = new Updater($storage);
363
-		}
364
-		return $storage->updater;
365
-	}
366
-
367
-	public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage {
368
-		/** @var Cache $cache */
369
-		$cache = $this->getCache(storage: $storage);
370
-		return $cache->getStorageCache();
371
-	}
372
-
373
-	public function getOwner(string $path): string|false {
374
-		if ($this->owner === null) {
375
-			$this->owner = \OC_User::getUser();
376
-		}
377
-
378
-		return $this->owner;
379
-	}
380
-
381
-	public function getETag(string $path): string|false {
382
-		return uniqid();
383
-	}
384
-
385
-	/**
386
-	 * clean a path, i.e. remove all redundant '.' and '..'
387
-	 * making sure that it can't point to higher than '/'
388
-	 *
389
-	 * @param string $path The path to clean
390
-	 * @return string cleaned path
391
-	 */
392
-	public function cleanPath(string $path): string {
393
-		if (strlen($path) == 0 || $path[0] != '/') {
394
-			$path = '/' . $path;
395
-		}
396
-
397
-		$output = [];
398
-		foreach (explode('/', $path) as $chunk) {
399
-			if ($chunk == '..') {
400
-				array_pop($output);
401
-			} elseif ($chunk == '.') {
402
-			} else {
403
-				$output[] = $chunk;
404
-			}
405
-		}
406
-		return implode('/', $output);
407
-	}
408
-
409
-	/**
410
-	 * Test a storage for availability
411
-	 */
412
-	public function test(): bool {
413
-		try {
414
-			if ($this->stat('')) {
415
-				return true;
416
-			}
417
-			Server::get(LoggerInterface::class)->info('External storage not available: stat() failed');
418
-			return false;
419
-		} catch (\Exception $e) {
420
-			Server::get(LoggerInterface::class)->warning(
421
-				'External storage not available: ' . $e->getMessage(),
422
-				['exception' => $e]
423
-			);
424
-			return false;
425
-		}
426
-	}
427
-
428
-	public function free_space(string $path): int|float|false {
429
-		return \OCP\Files\FileInfo::SPACE_UNKNOWN;
430
-	}
431
-
432
-	public function isLocal(): bool {
433
-		// the common implementation returns a temporary file by
434
-		// default, which is not local
435
-		return false;
436
-	}
437
-
438
-	/**
439
-	 * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
440
-	 */
441
-	public function instanceOfStorage(string $class): bool {
442
-		if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
443
-			// FIXME Temporary fix to keep existing checks working
444
-			$class = '\OCA\Files_Sharing\SharedStorage';
445
-		}
446
-		return is_a($this, $class);
447
-	}
448
-
449
-	#[Override]
450
-	public function getDirectDownload(string $path): array|false {
451
-		return false;
452
-	}
453
-
454
-	#[Override]
455
-	public function getDirectDownloadById(string $fileId): array|false {
456
-		return false;
457
-	}
458
-
459
-	public function verifyPath(string $path, string $fileName): void {
460
-		$this->getFilenameValidator()
461
-			->validateFilename($fileName);
462
-
463
-		// verify also the path is valid
464
-		if ($path && $path !== '/' && $path !== '.') {
465
-			try {
466
-				$this->verifyPath(dirname($path), basename($path));
467
-			} catch (InvalidPathException $e) {
468
-				// Ignore invalid file type exceptions on directories
469
-				if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) {
470
-					$l = \OCP\Util::getL10N('lib');
471
-					throw new InvalidPathException($l->t('Invalid parent path'), previous: $e);
472
-				}
473
-			}
474
-		}
475
-	}
476
-
477
-	/**
478
-	 * Get the filename validator
479
-	 * (cached for performance)
480
-	 */
481
-	protected function getFilenameValidator(): IFilenameValidator {
482
-		if ($this->filenameValidator === null) {
483
-			$this->filenameValidator = Server::get(IFilenameValidator::class);
484
-		}
485
-		return $this->filenameValidator;
486
-	}
487
-
488
-	public function setMountOptions(array $options): void {
489
-		$this->mountOptions = $options;
490
-	}
491
-
492
-	public function getMountOption(string $name, mixed $default = null): mixed {
493
-		return $this->mountOptions[$name] ?? $default;
494
-	}
495
-
496
-	public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
497
-		if ($sourceStorage === $this) {
498
-			return $this->copy($sourceInternalPath, $targetInternalPath);
499
-		}
500
-
501
-		if ($sourceStorage->is_dir($sourceInternalPath)) {
502
-			$dh = $sourceStorage->opendir($sourceInternalPath);
503
-			$result = $this->mkdir($targetInternalPath);
504
-			if (is_resource($dh)) {
505
-				$result = true;
506
-				while ($result && ($file = readdir($dh)) !== false) {
507
-					if (!Filesystem::isIgnoredDir($file)) {
508
-						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
509
-					}
510
-				}
511
-			}
512
-		} else {
513
-			$source = $sourceStorage->fopen($sourceInternalPath, 'r');
514
-			$result = false;
515
-			if ($source) {
516
-				try {
517
-					$this->writeStream($targetInternalPath, $source);
518
-					$result = true;
519
-				} catch (\Exception $e) {
520
-					Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
521
-				}
522
-			}
523
-
524
-			if ($result && $preserveMtime) {
525
-				$mtime = $sourceStorage->filemtime($sourceInternalPath);
526
-				$this->touch($targetInternalPath, is_int($mtime) ? $mtime : null);
527
-			}
528
-
529
-			if (!$result) {
530
-				// delete partially written target file
531
-				$this->unlink($targetInternalPath);
532
-				// delete cache entry that was created by fopen
533
-				$this->getCache()->remove($targetInternalPath);
534
-			}
535
-		}
536
-		return (bool)$result;
537
-	}
538
-
539
-	/**
540
-	 * Check if a storage is the same as the current one, including wrapped storages
541
-	 */
542
-	private function isSameStorage(IStorage $storage): bool {
543
-		while ($storage->instanceOfStorage(Wrapper::class)) {
544
-			/**
545
-			 * @var Wrapper $storage
546
-			 */
547
-			$storage = $storage->getWrapperStorage();
548
-		}
549
-
550
-		return $storage === $this;
551
-	}
552
-
553
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
554
-		if (
555
-			!$sourceStorage->instanceOfStorage(Encryption::class)
556
-			&& $this->isSameStorage($sourceStorage)
557
-		) {
558
-			// resolve any jailed paths
559
-			while ($sourceStorage->instanceOfStorage(Jail::class)) {
560
-				/**
561
-				 * @var Jail $sourceStorage
562
-				 */
563
-				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
564
-				$sourceStorage = $sourceStorage->getUnjailedStorage();
565
-			}
566
-
567
-			return $this->rename($sourceInternalPath, $targetInternalPath);
568
-		}
569
-
570
-		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
571
-			return false;
572
-		}
573
-
574
-		$result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true);
575
-		if ($result) {
576
-			if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
577
-				/** @var ObjectStoreStorage $sourceStorage */
578
-				$sourceStorage->setPreserveCacheOnDelete(true);
579
-			}
580
-			try {
581
-				if ($sourceStorage->is_dir($sourceInternalPath)) {
582
-					$result = $sourceStorage->rmdir($sourceInternalPath);
583
-				} else {
584
-					$result = $sourceStorage->unlink($sourceInternalPath);
585
-				}
586
-			} finally {
587
-				if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
588
-					/** @var ObjectStoreStorage $sourceStorage */
589
-					$sourceStorage->setPreserveCacheOnDelete(false);
590
-				}
591
-			}
592
-		}
593
-		return $result;
594
-	}
595
-
596
-	public function getMetaData(string $path): ?array {
597
-		if (Filesystem::isFileBlacklisted($path)) {
598
-			throw new ForbiddenException('Invalid path: ' . $path, false);
599
-		}
600
-
601
-		$permissions = $this->getPermissions($path);
602
-		if (!$permissions & \OCP\Constants::PERMISSION_READ) {
603
-			//can't read, nothing we can do
604
-			return null;
605
-		}
606
-
607
-		$data = [];
608
-		$data['mimetype'] = $this->getMimeType($path);
609
-		$data['mtime'] = $this->filemtime($path);
610
-		if ($data['mtime'] === false) {
611
-			$data['mtime'] = time();
612
-		}
613
-		if ($data['mimetype'] == 'httpd/unix-directory') {
614
-			$data['size'] = -1; //unknown
615
-		} else {
616
-			$data['size'] = $this->filesize($path);
617
-		}
618
-		$data['etag'] = $this->getETag($path);
619
-		$data['storage_mtime'] = $data['mtime'];
620
-		$data['permissions'] = $permissions;
621
-		$data['name'] = basename($path);
622
-
623
-		return $data;
624
-	}
625
-
626
-	public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
627
-		$logger = $this->getLockLogger();
628
-		if ($logger) {
629
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
630
-			$logger->info(
631
-				sprintf(
632
-					'acquire %s lock on "%s" on storage "%s"',
633
-					$typeString,
634
-					$path,
635
-					$this->getId()
636
-				),
637
-				[
638
-					'app' => 'locking',
639
-				]
640
-			);
641
-		}
642
-		try {
643
-			$provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path);
644
-		} catch (LockedException $e) {
645
-			$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
646
-			if ($logger) {
647
-				$logger->info($e->getMessage(), ['exception' => $e]);
648
-			}
649
-			throw $e;
650
-		}
651
-	}
652
-
653
-	public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
654
-		$logger = $this->getLockLogger();
655
-		if ($logger) {
656
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
657
-			$logger->info(
658
-				sprintf(
659
-					'release %s lock on "%s" on storage "%s"',
660
-					$typeString,
661
-					$path,
662
-					$this->getId()
663
-				),
664
-				[
665
-					'app' => 'locking',
666
-				]
667
-			);
668
-		}
669
-		try {
670
-			$provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
671
-		} catch (LockedException $e) {
672
-			$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
673
-			if ($logger) {
674
-				$logger->info($e->getMessage(), ['exception' => $e]);
675
-			}
676
-			throw $e;
677
-		}
678
-	}
679
-
680
-	public function changeLock(string $path, int $type, ILockingProvider $provider): void {
681
-		$logger = $this->getLockLogger();
682
-		if ($logger) {
683
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
684
-			$logger->info(
685
-				sprintf(
686
-					'change lock on "%s" to %s on storage "%s"',
687
-					$path,
688
-					$typeString,
689
-					$this->getId()
690
-				),
691
-				[
692
-					'app' => 'locking',
693
-				]
694
-			);
695
-		}
696
-		try {
697
-			$provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
698
-		} catch (LockedException $e) {
699
-			$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
700
-			if ($logger) {
701
-				$logger->info($e->getMessage(), ['exception' => $e]);
702
-			}
703
-			throw $e;
704
-		}
705
-	}
706
-
707
-	private function getLockLogger(): ?LoggerInterface {
708
-		if (is_null($this->shouldLogLocks)) {
709
-			$this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false);
710
-			$this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null;
711
-		}
712
-		return $this->logger;
713
-	}
714
-
715
-	/**
716
-	 * @return array{available: bool, last_checked: int}
717
-	 */
718
-	public function getAvailability(): array {
719
-		return $this->getStorageCache()->getAvailability();
720
-	}
721
-
722
-	public function setAvailability(bool $isAvailable): void {
723
-		$this->getStorageCache()->setAvailability($isAvailable);
724
-	}
725
-
726
-	public function setOwner(?string $user): void {
727
-		$this->owner = $user;
728
-	}
729
-
730
-	public function needsPartFile(): bool {
731
-		return true;
732
-	}
733
-
734
-	public function writeStream(string $path, $stream, ?int $size = null): int {
735
-		$target = $this->fopen($path, 'w');
736
-		if (!$target) {
737
-			throw new GenericFileException("Failed to open $path for writing");
738
-		}
739
-		try {
740
-			[$count, $result] = Files::streamCopy($stream, $target, true);
741
-			if (!$result) {
742
-				throw new GenericFileException('Failed to copy stream');
743
-			}
744
-		} finally {
745
-			fclose($target);
746
-			fclose($stream);
747
-		}
748
-		return $count;
749
-	}
750
-
751
-	public function getDirectoryContent(string $directory): \Traversable {
752
-		$dh = $this->opendir($directory);
753
-
754
-		if ($dh === false) {
755
-			throw new StorageNotAvailableException('Directory listing failed');
756
-		}
757
-
758
-		if (is_resource($dh)) {
759
-			$basePath = rtrim($directory, '/');
760
-			while (($file = readdir($dh)) !== false) {
761
-				if (!Filesystem::isIgnoredDir($file)) {
762
-					$childPath = $basePath . '/' . trim($file, '/');
763
-					$metadata = $this->getMetaData($childPath);
764
-					if ($metadata !== null) {
765
-						yield $metadata;
766
-					}
767
-				}
768
-			}
769
-		}
770
-	}
56
+    use LocalTempFileTrait;
57
+
58
+    protected ?Cache $cache = null;
59
+    protected ?Scanner $scanner = null;
60
+    protected ?Watcher $watcher = null;
61
+    protected ?Propagator $propagator = null;
62
+    protected $storageCache;
63
+    protected ?Updater $updater = null;
64
+
65
+    protected array $mountOptions = [];
66
+    protected $owner = null;
67
+
68
+    private ?bool $shouldLogLocks = null;
69
+    private ?LoggerInterface $logger = null;
70
+    private ?IFilenameValidator $filenameValidator = null;
71
+
72
+    public function __construct(array $parameters) {
73
+    }
74
+
75
+    protected function remove(string $path): bool {
76
+        if ($this->file_exists($path)) {
77
+            if ($this->is_dir($path)) {
78
+                return $this->rmdir($path);
79
+            } elseif ($this->is_file($path)) {
80
+                return $this->unlink($path);
81
+            }
82
+        }
83
+        return false;
84
+    }
85
+
86
+    public function is_dir(string $path): bool {
87
+        return $this->filetype($path) === 'dir';
88
+    }
89
+
90
+    public function is_file(string $path): bool {
91
+        return $this->filetype($path) === 'file';
92
+    }
93
+
94
+    public function filesize(string $path): int|float|false {
95
+        if ($this->is_dir($path)) {
96
+            return 0; //by definition
97
+        } else {
98
+            $stat = $this->stat($path);
99
+            return isset($stat['size']) ? $stat['size'] : 0;
100
+        }
101
+    }
102
+
103
+    public function isReadable(string $path): bool {
104
+        // at least check whether it exists
105
+        // subclasses might want to implement this more thoroughly
106
+        return $this->file_exists($path);
107
+    }
108
+
109
+    public function isUpdatable(string $path): bool {
110
+        // at least check whether it exists
111
+        // subclasses might want to implement this more thoroughly
112
+        // a non-existing file/folder isn't updatable
113
+        return $this->file_exists($path);
114
+    }
115
+
116
+    public function isCreatable(string $path): bool {
117
+        if ($this->is_dir($path) && $this->isUpdatable($path)) {
118
+            return true;
119
+        }
120
+        return false;
121
+    }
122
+
123
+    public function isDeletable(string $path): bool {
124
+        if ($path === '' || $path === '/') {
125
+            return $this->isUpdatable($path);
126
+        }
127
+        $parent = dirname($path);
128
+        return $this->isUpdatable($parent) && $this->isUpdatable($path);
129
+    }
130
+
131
+    public function isSharable(string $path): bool {
132
+        return $this->isReadable($path);
133
+    }
134
+
135
+    public function getPermissions(string $path): int {
136
+        $permissions = 0;
137
+        if ($this->isCreatable($path)) {
138
+            $permissions |= \OCP\Constants::PERMISSION_CREATE;
139
+        }
140
+        if ($this->isReadable($path)) {
141
+            $permissions |= \OCP\Constants::PERMISSION_READ;
142
+        }
143
+        if ($this->isUpdatable($path)) {
144
+            $permissions |= \OCP\Constants::PERMISSION_UPDATE;
145
+        }
146
+        if ($this->isDeletable($path)) {
147
+            $permissions |= \OCP\Constants::PERMISSION_DELETE;
148
+        }
149
+        if ($this->isSharable($path)) {
150
+            $permissions |= \OCP\Constants::PERMISSION_SHARE;
151
+        }
152
+        return $permissions;
153
+    }
154
+
155
+    public function filemtime(string $path): int|false {
156
+        $stat = $this->stat($path);
157
+        if (isset($stat['mtime']) && $stat['mtime'] > 0) {
158
+            return $stat['mtime'];
159
+        } else {
160
+            return 0;
161
+        }
162
+    }
163
+
164
+    public function file_get_contents(string $path): string|false {
165
+        $handle = $this->fopen($path, 'r');
166
+        if (!$handle) {
167
+            return false;
168
+        }
169
+        $data = stream_get_contents($handle);
170
+        fclose($handle);
171
+        return $data;
172
+    }
173
+
174
+    public function file_put_contents(string $path, mixed $data): int|float|false {
175
+        $handle = $this->fopen($path, 'w');
176
+        if (!$handle) {
177
+            return false;
178
+        }
179
+        $this->removeCachedFile($path);
180
+        $count = fwrite($handle, $data);
181
+        fclose($handle);
182
+        return $count;
183
+    }
184
+
185
+    public function rename(string $source, string $target): bool {
186
+        $this->remove($target);
187
+
188
+        $this->removeCachedFile($source);
189
+        return $this->copy($source, $target) && $this->remove($source);
190
+    }
191
+
192
+    public function copy(string $source, string $target): bool {
193
+        if ($this->is_dir($source)) {
194
+            $this->remove($target);
195
+            $dir = $this->opendir($source);
196
+            $this->mkdir($target);
197
+            while (($file = readdir($dir)) !== false) {
198
+                if (!Filesystem::isIgnoredDir($file)) {
199
+                    if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
200
+                        closedir($dir);
201
+                        return false;
202
+                    }
203
+                }
204
+            }
205
+            closedir($dir);
206
+            return true;
207
+        } else {
208
+            $sourceStream = $this->fopen($source, 'r');
209
+            $targetStream = $this->fopen($target, 'w');
210
+            [, $result] = Files::streamCopy($sourceStream, $targetStream, true);
211
+            if (!$result) {
212
+                Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
213
+            }
214
+            $this->removeCachedFile($target);
215
+            return $result;
216
+        }
217
+    }
218
+
219
+    public function getMimeType(string $path): string|false {
220
+        if ($this->is_dir($path)) {
221
+            return 'httpd/unix-directory';
222
+        } elseif ($this->file_exists($path)) {
223
+            return \OC::$server->getMimeTypeDetector()->detectPath($path);
224
+        } else {
225
+            return false;
226
+        }
227
+    }
228
+
229
+    public function hash(string $type, string $path, bool $raw = false): string|false {
230
+        $fh = $this->fopen($path, 'rb');
231
+        if (!$fh) {
232
+            return false;
233
+        }
234
+        $ctx = hash_init($type);
235
+        hash_update_stream($ctx, $fh);
236
+        fclose($fh);
237
+        return hash_final($ctx, $raw);
238
+    }
239
+
240
+    public function getLocalFile(string $path): string|false {
241
+        return $this->getCachedFile($path);
242
+    }
243
+
244
+    private function addLocalFolder(string $path, string $target): void {
245
+        $dh = $this->opendir($path);
246
+        if (is_resource($dh)) {
247
+            while (($file = readdir($dh)) !== false) {
248
+                if (!Filesystem::isIgnoredDir($file)) {
249
+                    if ($this->is_dir($path . '/' . $file)) {
250
+                        mkdir($target . '/' . $file);
251
+                        $this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
252
+                    } else {
253
+                        $tmp = $this->toTmpFile($path . '/' . $file);
254
+                        rename($tmp, $target . '/' . $file);
255
+                    }
256
+                }
257
+            }
258
+        }
259
+    }
260
+
261
+    protected function searchInDir(string $query, string $dir = ''): array {
262
+        $files = [];
263
+        $dh = $this->opendir($dir);
264
+        if (is_resource($dh)) {
265
+            while (($item = readdir($dh)) !== false) {
266
+                if (Filesystem::isIgnoredDir($item)) {
267
+                    continue;
268
+                }
269
+                if (strstr(strtolower($item), strtolower($query)) !== false) {
270
+                    $files[] = $dir . '/' . $item;
271
+                }
272
+                if ($this->is_dir($dir . '/' . $item)) {
273
+                    $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
274
+                }
275
+            }
276
+        }
277
+        closedir($dh);
278
+        return $files;
279
+    }
280
+
281
+    /**
282
+     * @inheritDoc
283
+     * Check if a file or folder has been updated since $time
284
+     *
285
+     * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
286
+     * the mtime should always return false here. As a result storage implementations that always return false expect
287
+     * exclusive access to the backend and will not pick up files that have been added in a way that circumvents
288
+     * Nextcloud filesystem.
289
+     */
290
+    public function hasUpdated(string $path, int $time): bool {
291
+        return $this->filemtime($path) > $time;
292
+    }
293
+
294
+    protected function getCacheDependencies(): CacheDependencies {
295
+        static $dependencies = null;
296
+        if (!$dependencies) {
297
+            $dependencies = Server::get(CacheDependencies::class);
298
+        }
299
+        return $dependencies;
300
+    }
301
+
302
+    public function getCache(string $path = '', ?IStorage $storage = null): ICache {
303
+        if (!$storage) {
304
+            $storage = $this;
305
+        }
306
+        /** @var self $storage */
307
+        if (!isset($storage->cache)) {
308
+            $storage->cache = new Cache($storage, $this->getCacheDependencies());
309
+        }
310
+        return $storage->cache;
311
+    }
312
+
313
+    public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
314
+        if (!$storage) {
315
+            $storage = $this;
316
+        }
317
+        if (!$storage->instanceOfStorage(self::class)) {
318
+            throw new \InvalidArgumentException('Storage is not of the correct class');
319
+        }
320
+        if (!isset($storage->scanner)) {
321
+            $storage->scanner = new Scanner($storage);
322
+        }
323
+        return $storage->scanner;
324
+    }
325
+
326
+    public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
327
+        if (!$storage) {
328
+            $storage = $this;
329
+        }
330
+        if (!isset($this->watcher)) {
331
+            $this->watcher = new Watcher($storage);
332
+            $globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER);
333
+            $this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
334
+        }
335
+        return $this->watcher;
336
+    }
337
+
338
+    public function getPropagator(?IStorage $storage = null): IPropagator {
339
+        if (!$storage) {
340
+            $storage = $this;
341
+        }
342
+        if (!$storage->instanceOfStorage(self::class)) {
343
+            throw new \InvalidArgumentException('Storage is not of the correct class');
344
+        }
345
+        /** @var self $storage */
346
+        if (!isset($storage->propagator)) {
347
+            $config = Server::get(IConfig::class);
348
+            $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]);
349
+        }
350
+        return $storage->propagator;
351
+    }
352
+
353
+    public function getUpdater(?IStorage $storage = null): IUpdater {
354
+        if (!$storage) {
355
+            $storage = $this;
356
+        }
357
+        if (!$storage->instanceOfStorage(self::class)) {
358
+            throw new \InvalidArgumentException('Storage is not of the correct class');
359
+        }
360
+        /** @var self $storage */
361
+        if (!isset($storage->updater)) {
362
+            $storage->updater = new Updater($storage);
363
+        }
364
+        return $storage->updater;
365
+    }
366
+
367
+    public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage {
368
+        /** @var Cache $cache */
369
+        $cache = $this->getCache(storage: $storage);
370
+        return $cache->getStorageCache();
371
+    }
372
+
373
+    public function getOwner(string $path): string|false {
374
+        if ($this->owner === null) {
375
+            $this->owner = \OC_User::getUser();
376
+        }
377
+
378
+        return $this->owner;
379
+    }
380
+
381
+    public function getETag(string $path): string|false {
382
+        return uniqid();
383
+    }
384
+
385
+    /**
386
+     * clean a path, i.e. remove all redundant '.' and '..'
387
+     * making sure that it can't point to higher than '/'
388
+     *
389
+     * @param string $path The path to clean
390
+     * @return string cleaned path
391
+     */
392
+    public function cleanPath(string $path): string {
393
+        if (strlen($path) == 0 || $path[0] != '/') {
394
+            $path = '/' . $path;
395
+        }
396
+
397
+        $output = [];
398
+        foreach (explode('/', $path) as $chunk) {
399
+            if ($chunk == '..') {
400
+                array_pop($output);
401
+            } elseif ($chunk == '.') {
402
+            } else {
403
+                $output[] = $chunk;
404
+            }
405
+        }
406
+        return implode('/', $output);
407
+    }
408
+
409
+    /**
410
+     * Test a storage for availability
411
+     */
412
+    public function test(): bool {
413
+        try {
414
+            if ($this->stat('')) {
415
+                return true;
416
+            }
417
+            Server::get(LoggerInterface::class)->info('External storage not available: stat() failed');
418
+            return false;
419
+        } catch (\Exception $e) {
420
+            Server::get(LoggerInterface::class)->warning(
421
+                'External storage not available: ' . $e->getMessage(),
422
+                ['exception' => $e]
423
+            );
424
+            return false;
425
+        }
426
+    }
427
+
428
+    public function free_space(string $path): int|float|false {
429
+        return \OCP\Files\FileInfo::SPACE_UNKNOWN;
430
+    }
431
+
432
+    public function isLocal(): bool {
433
+        // the common implementation returns a temporary file by
434
+        // default, which is not local
435
+        return false;
436
+    }
437
+
438
+    /**
439
+     * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
440
+     */
441
+    public function instanceOfStorage(string $class): bool {
442
+        if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
443
+            // FIXME Temporary fix to keep existing checks working
444
+            $class = '\OCA\Files_Sharing\SharedStorage';
445
+        }
446
+        return is_a($this, $class);
447
+    }
448
+
449
+    #[Override]
450
+    public function getDirectDownload(string $path): array|false {
451
+        return false;
452
+    }
453
+
454
+    #[Override]
455
+    public function getDirectDownloadById(string $fileId): array|false {
456
+        return false;
457
+    }
458
+
459
+    public function verifyPath(string $path, string $fileName): void {
460
+        $this->getFilenameValidator()
461
+            ->validateFilename($fileName);
462
+
463
+        // verify also the path is valid
464
+        if ($path && $path !== '/' && $path !== '.') {
465
+            try {
466
+                $this->verifyPath(dirname($path), basename($path));
467
+            } catch (InvalidPathException $e) {
468
+                // Ignore invalid file type exceptions on directories
469
+                if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) {
470
+                    $l = \OCP\Util::getL10N('lib');
471
+                    throw new InvalidPathException($l->t('Invalid parent path'), previous: $e);
472
+                }
473
+            }
474
+        }
475
+    }
476
+
477
+    /**
478
+     * Get the filename validator
479
+     * (cached for performance)
480
+     */
481
+    protected function getFilenameValidator(): IFilenameValidator {
482
+        if ($this->filenameValidator === null) {
483
+            $this->filenameValidator = Server::get(IFilenameValidator::class);
484
+        }
485
+        return $this->filenameValidator;
486
+    }
487
+
488
+    public function setMountOptions(array $options): void {
489
+        $this->mountOptions = $options;
490
+    }
491
+
492
+    public function getMountOption(string $name, mixed $default = null): mixed {
493
+        return $this->mountOptions[$name] ?? $default;
494
+    }
495
+
496
+    public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
497
+        if ($sourceStorage === $this) {
498
+            return $this->copy($sourceInternalPath, $targetInternalPath);
499
+        }
500
+
501
+        if ($sourceStorage->is_dir($sourceInternalPath)) {
502
+            $dh = $sourceStorage->opendir($sourceInternalPath);
503
+            $result = $this->mkdir($targetInternalPath);
504
+            if (is_resource($dh)) {
505
+                $result = true;
506
+                while ($result && ($file = readdir($dh)) !== false) {
507
+                    if (!Filesystem::isIgnoredDir($file)) {
508
+                        $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
509
+                    }
510
+                }
511
+            }
512
+        } else {
513
+            $source = $sourceStorage->fopen($sourceInternalPath, 'r');
514
+            $result = false;
515
+            if ($source) {
516
+                try {
517
+                    $this->writeStream($targetInternalPath, $source);
518
+                    $result = true;
519
+                } catch (\Exception $e) {
520
+                    Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
521
+                }
522
+            }
523
+
524
+            if ($result && $preserveMtime) {
525
+                $mtime = $sourceStorage->filemtime($sourceInternalPath);
526
+                $this->touch($targetInternalPath, is_int($mtime) ? $mtime : null);
527
+            }
528
+
529
+            if (!$result) {
530
+                // delete partially written target file
531
+                $this->unlink($targetInternalPath);
532
+                // delete cache entry that was created by fopen
533
+                $this->getCache()->remove($targetInternalPath);
534
+            }
535
+        }
536
+        return (bool)$result;
537
+    }
538
+
539
+    /**
540
+     * Check if a storage is the same as the current one, including wrapped storages
541
+     */
542
+    private function isSameStorage(IStorage $storage): bool {
543
+        while ($storage->instanceOfStorage(Wrapper::class)) {
544
+            /**
545
+             * @var Wrapper $storage
546
+             */
547
+            $storage = $storage->getWrapperStorage();
548
+        }
549
+
550
+        return $storage === $this;
551
+    }
552
+
553
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
554
+        if (
555
+            !$sourceStorage->instanceOfStorage(Encryption::class)
556
+            && $this->isSameStorage($sourceStorage)
557
+        ) {
558
+            // resolve any jailed paths
559
+            while ($sourceStorage->instanceOfStorage(Jail::class)) {
560
+                /**
561
+                 * @var Jail $sourceStorage
562
+                 */
563
+                $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
564
+                $sourceStorage = $sourceStorage->getUnjailedStorage();
565
+            }
566
+
567
+            return $this->rename($sourceInternalPath, $targetInternalPath);
568
+        }
569
+
570
+        if (!$sourceStorage->isDeletable($sourceInternalPath)) {
571
+            return false;
572
+        }
573
+
574
+        $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true);
575
+        if ($result) {
576
+            if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
577
+                /** @var ObjectStoreStorage $sourceStorage */
578
+                $sourceStorage->setPreserveCacheOnDelete(true);
579
+            }
580
+            try {
581
+                if ($sourceStorage->is_dir($sourceInternalPath)) {
582
+                    $result = $sourceStorage->rmdir($sourceInternalPath);
583
+                } else {
584
+                    $result = $sourceStorage->unlink($sourceInternalPath);
585
+                }
586
+            } finally {
587
+                if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
588
+                    /** @var ObjectStoreStorage $sourceStorage */
589
+                    $sourceStorage->setPreserveCacheOnDelete(false);
590
+                }
591
+            }
592
+        }
593
+        return $result;
594
+    }
595
+
596
+    public function getMetaData(string $path): ?array {
597
+        if (Filesystem::isFileBlacklisted($path)) {
598
+            throw new ForbiddenException('Invalid path: ' . $path, false);
599
+        }
600
+
601
+        $permissions = $this->getPermissions($path);
602
+        if (!$permissions & \OCP\Constants::PERMISSION_READ) {
603
+            //can't read, nothing we can do
604
+            return null;
605
+        }
606
+
607
+        $data = [];
608
+        $data['mimetype'] = $this->getMimeType($path);
609
+        $data['mtime'] = $this->filemtime($path);
610
+        if ($data['mtime'] === false) {
611
+            $data['mtime'] = time();
612
+        }
613
+        if ($data['mimetype'] == 'httpd/unix-directory') {
614
+            $data['size'] = -1; //unknown
615
+        } else {
616
+            $data['size'] = $this->filesize($path);
617
+        }
618
+        $data['etag'] = $this->getETag($path);
619
+        $data['storage_mtime'] = $data['mtime'];
620
+        $data['permissions'] = $permissions;
621
+        $data['name'] = basename($path);
622
+
623
+        return $data;
624
+    }
625
+
626
+    public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
627
+        $logger = $this->getLockLogger();
628
+        if ($logger) {
629
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
630
+            $logger->info(
631
+                sprintf(
632
+                    'acquire %s lock on "%s" on storage "%s"',
633
+                    $typeString,
634
+                    $path,
635
+                    $this->getId()
636
+                ),
637
+                [
638
+                    'app' => 'locking',
639
+                ]
640
+            );
641
+        }
642
+        try {
643
+            $provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path);
644
+        } catch (LockedException $e) {
645
+            $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
646
+            if ($logger) {
647
+                $logger->info($e->getMessage(), ['exception' => $e]);
648
+            }
649
+            throw $e;
650
+        }
651
+    }
652
+
653
+    public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
654
+        $logger = $this->getLockLogger();
655
+        if ($logger) {
656
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
657
+            $logger->info(
658
+                sprintf(
659
+                    'release %s lock on "%s" on storage "%s"',
660
+                    $typeString,
661
+                    $path,
662
+                    $this->getId()
663
+                ),
664
+                [
665
+                    'app' => 'locking',
666
+                ]
667
+            );
668
+        }
669
+        try {
670
+            $provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
671
+        } catch (LockedException $e) {
672
+            $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
673
+            if ($logger) {
674
+                $logger->info($e->getMessage(), ['exception' => $e]);
675
+            }
676
+            throw $e;
677
+        }
678
+    }
679
+
680
+    public function changeLock(string $path, int $type, ILockingProvider $provider): void {
681
+        $logger = $this->getLockLogger();
682
+        if ($logger) {
683
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
684
+            $logger->info(
685
+                sprintf(
686
+                    'change lock on "%s" to %s on storage "%s"',
687
+                    $path,
688
+                    $typeString,
689
+                    $this->getId()
690
+                ),
691
+                [
692
+                    'app' => 'locking',
693
+                ]
694
+            );
695
+        }
696
+        try {
697
+            $provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
698
+        } catch (LockedException $e) {
699
+            $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
700
+            if ($logger) {
701
+                $logger->info($e->getMessage(), ['exception' => $e]);
702
+            }
703
+            throw $e;
704
+        }
705
+    }
706
+
707
+    private function getLockLogger(): ?LoggerInterface {
708
+        if (is_null($this->shouldLogLocks)) {
709
+            $this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false);
710
+            $this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null;
711
+        }
712
+        return $this->logger;
713
+    }
714
+
715
+    /**
716
+     * @return array{available: bool, last_checked: int}
717
+     */
718
+    public function getAvailability(): array {
719
+        return $this->getStorageCache()->getAvailability();
720
+    }
721
+
722
+    public function setAvailability(bool $isAvailable): void {
723
+        $this->getStorageCache()->setAvailability($isAvailable);
724
+    }
725
+
726
+    public function setOwner(?string $user): void {
727
+        $this->owner = $user;
728
+    }
729
+
730
+    public function needsPartFile(): bool {
731
+        return true;
732
+    }
733
+
734
+    public function writeStream(string $path, $stream, ?int $size = null): int {
735
+        $target = $this->fopen($path, 'w');
736
+        if (!$target) {
737
+            throw new GenericFileException("Failed to open $path for writing");
738
+        }
739
+        try {
740
+            [$count, $result] = Files::streamCopy($stream, $target, true);
741
+            if (!$result) {
742
+                throw new GenericFileException('Failed to copy stream');
743
+            }
744
+        } finally {
745
+            fclose($target);
746
+            fclose($stream);
747
+        }
748
+        return $count;
749
+    }
750
+
751
+    public function getDirectoryContent(string $directory): \Traversable {
752
+        $dh = $this->opendir($directory);
753
+
754
+        if ($dh === false) {
755
+            throw new StorageNotAvailableException('Directory listing failed');
756
+        }
757
+
758
+        if (is_resource($dh)) {
759
+            $basePath = rtrim($directory, '/');
760
+            while (($file = readdir($dh)) !== false) {
761
+                if (!Filesystem::isIgnoredDir($file)) {
762
+                    $childPath = $basePath . '/' . trim($file, '/');
763
+                    $metadata = $this->getMetaData($childPath);
764
+                    if ($metadata !== null) {
765
+                        yield $metadata;
766
+                    }
767
+                }
768
+            }
769
+        }
770
+    }
771 771
 }
Please login to merge, or discard this patch.
apps/files_sharing/lib/SharedStorage.php 1 patch
Indentation   +523 added lines, -523 removed lines patch added patch discarded remove patch
@@ -47,527 +47,527 @@
 block discarded – undo
47 47
  * Convert target path to source path and pass the function call to the correct storage provider
48 48
  */
49 49
 class SharedStorage extends Jail implements LegacyISharedStorage, ISharedStorage, IDisableEncryptionStorage {
50
-	/** @var IShare */
51
-	private $superShare;
52
-
53
-	/** @var IShare[] */
54
-	private $groupedShares;
55
-
56
-	/**
57
-	 * @var View
58
-	 */
59
-	private $ownerView;
60
-
61
-	private $initialized = false;
62
-
63
-	/**
64
-	 * @var ICacheEntry
65
-	 */
66
-	private $sourceRootInfo;
67
-
68
-	/** @var string */
69
-	private $user;
70
-
71
-	private LoggerInterface $logger;
72
-
73
-	/** @var IStorage */
74
-	private $nonMaskedStorage;
75
-
76
-	private array $mountOptions = [];
77
-
78
-	/** @var boolean */
79
-	private $sharingDisabledForUser;
80
-
81
-	/** @var ?Folder $ownerUserFolder */
82
-	private $ownerUserFolder = null;
83
-
84
-	private string $sourcePath = '';
85
-
86
-	private static int $initDepth = 0;
87
-
88
-	/**
89
-	 * @psalm-suppress NonInvariantDocblockPropertyType
90
-	 * @var ?Storage $storage
91
-	 */
92
-	protected $storage;
93
-
94
-	public function __construct(array $parameters) {
95
-		$this->ownerView = $parameters['ownerView'];
96
-		$this->logger = Server::get(LoggerInterface::class);
97
-
98
-		$this->superShare = $parameters['superShare'];
99
-		$this->groupedShares = $parameters['groupedShares'];
100
-
101
-		$this->user = $parameters['user'];
102
-		if (isset($parameters['sharingDisabledForUser'])) {
103
-			$this->sharingDisabledForUser = $parameters['sharingDisabledForUser'];
104
-		} else {
105
-			$this->sharingDisabledForUser = false;
106
-		}
107
-
108
-		parent::__construct([
109
-			'storage' => null,
110
-			'root' => null,
111
-		]);
112
-	}
113
-
114
-	/**
115
-	 * @return ICacheEntry
116
-	 */
117
-	private function getSourceRootInfo() {
118
-		if (is_null($this->sourceRootInfo)) {
119
-			if (is_null($this->superShare->getNodeCacheEntry())) {
120
-				$this->init();
121
-				$this->sourceRootInfo = $this->nonMaskedStorage->getCache()->get($this->rootPath);
122
-			} else {
123
-				$this->sourceRootInfo = $this->superShare->getNodeCacheEntry();
124
-			}
125
-		}
126
-		return $this->sourceRootInfo;
127
-	}
128
-
129
-	/**
130
-	 * @psalm-assert Storage $this->storage
131
-	 */
132
-	private function init() {
133
-		if ($this->initialized) {
134
-			if (!$this->storage) {
135
-				// marked as initialized but no storage set
136
-				// this is probably because some code path has caused recursion during the share setup
137
-				// we setup a "failed storage" so `getWrapperStorage` doesn't return null.
138
-				// If the share setup completes after this the "failed storage" will be overwritten by the correct one
139
-				$ex = new \Exception('Possible share setup recursion detected for share ' . $this->superShare->getId());
140
-				$this->logger->warning($ex->getMessage(), ['exception' => $ex, 'app' => 'files_sharing']);
141
-				$this->storage = new FailedStorage(['exception' => $ex]);
142
-				$this->cache = new FailedCache();
143
-				$this->rootPath = '';
144
-			}
145
-			return;
146
-		}
147
-
148
-		$this->initialized = true;
149
-		self::$initDepth++;
150
-
151
-		try {
152
-			if (self::$initDepth > 10) {
153
-				throw new \Exception('Maximum share depth reached');
154
-			}
155
-
156
-			/** @var IRootFolder $rootFolder */
157
-			$rootFolder = Server::get(IRootFolder::class);
158
-			$this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner());
159
-			$sourceId = $this->superShare->getNodeId();
160
-			$ownerNodes = $this->ownerUserFolder->getById($sourceId);
161
-
162
-			if (count($ownerNodes) === 0) {
163
-				$this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]);
164
-				$this->cache = new FailedCache();
165
-				$this->rootPath = '';
166
-			} else {
167
-				foreach ($ownerNodes as $ownerNode) {
168
-					$nonMaskedStorage = $ownerNode->getStorage();
169
-
170
-					// check if potential source node would lead to a recursive share setup
171
-					if ($nonMaskedStorage instanceof Wrapper && $nonMaskedStorage->isWrapperOf($this)) {
172
-						continue;
173
-					}
174
-					$this->nonMaskedStorage = $nonMaskedStorage;
175
-					$this->sourcePath = $ownerNode->getPath();
176
-					$this->rootPath = $ownerNode->getInternalPath();
177
-					$this->cache = null;
178
-					break;
179
-				}
180
-				if (!$this->nonMaskedStorage) {
181
-					// all potential source nodes would have been recursive
182
-					throw new \Exception('recursive share detected');
183
-				}
184
-				$this->storage = new PermissionsMask([
185
-					'storage' => $this->nonMaskedStorage,
186
-					'mask' => $this->superShare->getPermissions(),
187
-				]);
188
-			}
189
-		} catch (NotFoundException $e) {
190
-			// original file not accessible or deleted, set FailedStorage
191
-			$this->storage = new FailedStorage(['exception' => $e]);
192
-			$this->cache = new FailedCache();
193
-			$this->rootPath = '';
194
-		} catch (NoUserException $e) {
195
-			// sharer user deleted, set FailedStorage
196
-			$this->storage = new FailedStorage(['exception' => $e]);
197
-			$this->cache = new FailedCache();
198
-			$this->rootPath = '';
199
-		} catch (\Exception $e) {
200
-			$this->storage = new FailedStorage(['exception' => $e]);
201
-			$this->cache = new FailedCache();
202
-			$this->rootPath = '';
203
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
204
-		}
205
-
206
-		if (!$this->nonMaskedStorage) {
207
-			$this->nonMaskedStorage = $this->storage;
208
-		}
209
-		self::$initDepth--;
210
-	}
211
-
212
-	public function instanceOfStorage(string $class): bool {
213
-		if ($class === '\OC\Files\Storage\Common' || $class == Common::class) {
214
-			return true;
215
-		}
216
-		if (in_array($class, [
217
-			'\OC\Files\Storage\Home',
218
-			'\OC\Files\ObjectStore\HomeObjectStoreStorage',
219
-			'\OCP\Files\IHomeStorage',
220
-			Home::class,
221
-			HomeObjectStoreStorage::class,
222
-			IHomeStorage::class
223
-		])) {
224
-			return false;
225
-		}
226
-		return parent::instanceOfStorage($class);
227
-	}
228
-
229
-	/**
230
-	 * @return string
231
-	 */
232
-	public function getShareId() {
233
-		return $this->superShare->getId();
234
-	}
235
-
236
-	private function isValid(): bool {
237
-		return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE;
238
-	}
239
-
240
-	public function getId(): string {
241
-		return 'shared::' . $this->getMountPoint();
242
-	}
243
-
244
-	public function getPermissions(string $path = ''): int {
245
-		if (!$this->isValid()) {
246
-			return 0;
247
-		}
248
-		$permissions = parent::getPermissions($path) & $this->superShare->getPermissions();
249
-
250
-		// part files and the mount point always have delete permissions
251
-		if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') {
252
-			$permissions |= Constants::PERMISSION_DELETE;
253
-		}
254
-
255
-		if ($this->sharingDisabledForUser) {
256
-			$permissions &= ~Constants::PERMISSION_SHARE;
257
-		}
258
-
259
-		return $permissions;
260
-	}
261
-
262
-	public function isCreatable(string $path): bool {
263
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
264
-	}
265
-
266
-	public function isReadable(string $path): bool {
267
-		if (!$this->isValid()) {
268
-			return false;
269
-		}
270
-		if (!$this->file_exists($path)) {
271
-			return false;
272
-		}
273
-		/** @var IStorage $storage */
274
-		/** @var string $internalPath */
275
-		[$storage, $internalPath] = $this->resolvePath($path);
276
-		return $storage->isReadable($internalPath);
277
-	}
278
-
279
-	public function isUpdatable(string $path): bool {
280
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
281
-	}
282
-
283
-	public function isDeletable(string $path): bool {
284
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
285
-	}
286
-
287
-	public function isSharable(string $path): bool {
288
-		if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) {
289
-			return false;
290
-		}
291
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
292
-	}
293
-
294
-	public function fopen(string $path, string $mode) {
295
-		$source = $this->getUnjailedPath($path);
296
-		switch ($mode) {
297
-			case 'r+':
298
-			case 'rb+':
299
-			case 'w+':
300
-			case 'wb+':
301
-			case 'x+':
302
-			case 'xb+':
303
-			case 'a+':
304
-			case 'ab+':
305
-			case 'w':
306
-			case 'wb':
307
-			case 'x':
308
-			case 'xb':
309
-			case 'a':
310
-			case 'ab':
311
-				$creatable = $this->isCreatable(dirname($path));
312
-				$updatable = $this->isUpdatable($path);
313
-				// if neither permissions given, no need to continue
314
-				if (!$creatable && !$updatable) {
315
-					if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
316
-						$updatable = $this->isUpdatable(dirname($path));
317
-					}
318
-
319
-					if (!$updatable) {
320
-						return false;
321
-					}
322
-				}
323
-
324
-				$exists = $this->file_exists($path);
325
-				// if a file exists, updatable permissions are required
326
-				if ($exists && !$updatable) {
327
-					return false;
328
-				}
329
-
330
-				// part file is allowed if !$creatable but the final file is $updatable
331
-				if (pathinfo($path, PATHINFO_EXTENSION) !== 'part') {
332
-					if (!$exists && !$creatable) {
333
-						return false;
334
-					}
335
-				}
336
-		}
337
-		$info = [
338
-			'target' => $this->getMountPoint() . '/' . $path,
339
-			'source' => $source,
340
-			'mode' => $mode,
341
-		];
342
-		Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info);
343
-		return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode);
344
-	}
345
-
346
-	public function rename(string $source, string $target): bool {
347
-		$this->init();
348
-		$isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part';
349
-		$targetExists = $this->file_exists($target);
350
-		$sameFolder = dirname($source) === dirname($target);
351
-
352
-		if ($targetExists || ($sameFolder && !$isPartFile)) {
353
-			if (!$this->isUpdatable('')) {
354
-				return false;
355
-			}
356
-		} else {
357
-			if (!$this->isCreatable('')) {
358
-				return false;
359
-			}
360
-		}
361
-
362
-		return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target));
363
-	}
364
-
365
-	/**
366
-	 * return mount point of share, relative to data/user/files
367
-	 *
368
-	 * @return string
369
-	 */
370
-	public function getMountPoint(): string {
371
-		return $this->superShare->getTarget();
372
-	}
373
-
374
-	public function setMountPoint(string $path): void {
375
-		$this->superShare->setTarget($path);
376
-
377
-		foreach ($this->groupedShares as $share) {
378
-			$share->setTarget($path);
379
-		}
380
-	}
381
-
382
-	/**
383
-	 * get the user who shared the file
384
-	 *
385
-	 * @return string
386
-	 */
387
-	public function getSharedFrom(): string {
388
-		return $this->superShare->getShareOwner();
389
-	}
390
-
391
-	public function getShare(): IShare {
392
-		return $this->superShare;
393
-	}
394
-
395
-	/**
396
-	 * return share type, can be "file" or "folder"
397
-	 *
398
-	 * @return string
399
-	 */
400
-	public function getItemType(): string {
401
-		return $this->superShare->getNodeType();
402
-	}
403
-
404
-	public function getCache(string $path = '', ?IStorage $storage = null): ICache {
405
-		if ($this->cache) {
406
-			return $this->cache;
407
-		}
408
-		if (!$storage) {
409
-			$storage = $this;
410
-		}
411
-		$sourceRoot = $this->getSourceRootInfo();
412
-		if ($this->storage instanceof FailedStorage) {
413
-			return new FailedCache();
414
-		}
415
-
416
-		$this->cache = new Cache(
417
-			$storage,
418
-			$sourceRoot,
419
-			Server::get(CacheDependencies::class),
420
-			$this->getShare()
421
-		);
422
-		return $this->cache;
423
-	}
424
-
425
-	public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
426
-		if (!$storage) {
427
-			$storage = $this;
428
-		}
429
-		return new Scanner($storage);
430
-	}
431
-
432
-	public function getOwner(string $path): string|false {
433
-		return $this->superShare->getShareOwner();
434
-	}
435
-
436
-	public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
437
-		if ($this->watcher) {
438
-			return $this->watcher;
439
-		}
440
-
441
-		// Get node information
442
-		$node = $this->getShare()->getNodeCacheEntry();
443
-		if ($node instanceof CacheEntry) {
444
-			$storageId = $node->getData()['storage_string_id'] ?? null;
445
-			// for shares from the home storage we can rely on the home storage to keep itself up to date
446
-			// for other storages we need use the proper watcher
447
-			if ($storageId !== null && !(str_starts_with($storageId, 'home::') || str_starts_with($storageId, 'object::user'))) {
448
-				$cache = $this->getCache();
449
-				$this->watcher = parent::getWatcher($path, $storage);
450
-				if ($cache instanceof Cache) {
451
-					$this->watcher->onUpdate($cache->markRootChanged(...));
452
-				}
453
-				return $this->watcher;
454
-			}
455
-		}
456
-
457
-		// cache updating is handled by the share source
458
-		$this->watcher = new NullWatcher();
459
-		return $this->watcher;
460
-	}
461
-
462
-	/**
463
-	 * unshare complete storage, also the grouped shares
464
-	 *
465
-	 * @return bool
466
-	 */
467
-	public function unshareStorage(): bool {
468
-		foreach ($this->groupedShares as $share) {
469
-			Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user);
470
-		}
471
-		return true;
472
-	}
473
-
474
-	public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
475
-		/** @var ILockingStorage $targetStorage */
476
-		[$targetStorage, $targetInternalPath] = $this->resolvePath($path);
477
-		$targetStorage->acquireLock($targetInternalPath, $type, $provider);
478
-		// lock the parent folders of the owner when locking the share as recipient
479
-		if ($path === '') {
480
-			$sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
481
-			$this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
482
-		}
483
-	}
484
-
485
-	public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
486
-		/** @var ILockingStorage $targetStorage */
487
-		[$targetStorage, $targetInternalPath] = $this->resolvePath($path);
488
-		$targetStorage->releaseLock($targetInternalPath, $type, $provider);
489
-		// unlock the parent folders of the owner when unlocking the share as recipient
490
-		if ($path === '') {
491
-			$sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
492
-			$this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
493
-		}
494
-	}
495
-
496
-	public function changeLock(string $path, int $type, ILockingProvider $provider): void {
497
-		/** @var ILockingStorage $targetStorage */
498
-		[$targetStorage, $targetInternalPath] = $this->resolvePath($path);
499
-		$targetStorage->changeLock($targetInternalPath, $type, $provider);
500
-	}
501
-
502
-	public function getAvailability(): array {
503
-		// shares do not participate in availability logic
504
-		return [
505
-			'available' => true,
506
-			'last_checked' => 0,
507
-		];
508
-	}
509
-
510
-	public function setAvailability(bool $isAvailable): void {
511
-		// shares do not participate in availability logic
512
-	}
513
-
514
-	public function getSourceStorage() {
515
-		$this->init();
516
-		return $this->nonMaskedStorage;
517
-	}
518
-
519
-	public function getWrapperStorage(): Storage {
520
-		$this->init();
521
-
522
-		/**
523
-		 * @psalm-suppress DocblockTypeContradiction
524
-		 */
525
-		if (!$this->storage) {
526
-			$message = 'no storage set after init for share ' . $this->getShareId();
527
-			$this->logger->error($message);
528
-			$this->storage = new FailedStorage(['exception' => new \Exception($message)]);
529
-		}
530
-
531
-		return $this->storage;
532
-	}
533
-
534
-	public function file_get_contents(string $path): string|false {
535
-		$info = [
536
-			'target' => $this->getMountPoint() . '/' . $path,
537
-			'source' => $this->getUnjailedPath($path),
538
-		];
539
-		Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info);
540
-		return parent::file_get_contents($path);
541
-	}
542
-
543
-	public function file_put_contents(string $path, mixed $data): int|float|false {
544
-		$info = [
545
-			'target' => $this->getMountPoint() . '/' . $path,
546
-			'source' => $this->getUnjailedPath($path),
547
-		];
548
-		Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info);
549
-		return parent::file_put_contents($path, $data);
550
-	}
551
-
552
-	public function setMountOptions(array $options): void {
553
-		/* Note: This value is never read */
554
-		$this->mountOptions = $options;
555
-	}
556
-
557
-	public function getUnjailedPath(string $path): string {
558
-		$this->init();
559
-		return parent::getUnjailedPath($path);
560
-	}
561
-
562
-	#[Override]
563
-	public function getDirectDownload(string $path): array|false {
564
-		// disable direct download for shares
565
-		return false;
566
-	}
567
-
568
-	#[Override]
569
-	public function getDirectDownloadById(string $fileId): array|false {
570
-		// disable direct download for shares
571
-		return false;
572
-	}
50
+    /** @var IShare */
51
+    private $superShare;
52
+
53
+    /** @var IShare[] */
54
+    private $groupedShares;
55
+
56
+    /**
57
+     * @var View
58
+     */
59
+    private $ownerView;
60
+
61
+    private $initialized = false;
62
+
63
+    /**
64
+     * @var ICacheEntry
65
+     */
66
+    private $sourceRootInfo;
67
+
68
+    /** @var string */
69
+    private $user;
70
+
71
+    private LoggerInterface $logger;
72
+
73
+    /** @var IStorage */
74
+    private $nonMaskedStorage;
75
+
76
+    private array $mountOptions = [];
77
+
78
+    /** @var boolean */
79
+    private $sharingDisabledForUser;
80
+
81
+    /** @var ?Folder $ownerUserFolder */
82
+    private $ownerUserFolder = null;
83
+
84
+    private string $sourcePath = '';
85
+
86
+    private static int $initDepth = 0;
87
+
88
+    /**
89
+     * @psalm-suppress NonInvariantDocblockPropertyType
90
+     * @var ?Storage $storage
91
+     */
92
+    protected $storage;
93
+
94
+    public function __construct(array $parameters) {
95
+        $this->ownerView = $parameters['ownerView'];
96
+        $this->logger = Server::get(LoggerInterface::class);
97
+
98
+        $this->superShare = $parameters['superShare'];
99
+        $this->groupedShares = $parameters['groupedShares'];
100
+
101
+        $this->user = $parameters['user'];
102
+        if (isset($parameters['sharingDisabledForUser'])) {
103
+            $this->sharingDisabledForUser = $parameters['sharingDisabledForUser'];
104
+        } else {
105
+            $this->sharingDisabledForUser = false;
106
+        }
107
+
108
+        parent::__construct([
109
+            'storage' => null,
110
+            'root' => null,
111
+        ]);
112
+    }
113
+
114
+    /**
115
+     * @return ICacheEntry
116
+     */
117
+    private function getSourceRootInfo() {
118
+        if (is_null($this->sourceRootInfo)) {
119
+            if (is_null($this->superShare->getNodeCacheEntry())) {
120
+                $this->init();
121
+                $this->sourceRootInfo = $this->nonMaskedStorage->getCache()->get($this->rootPath);
122
+            } else {
123
+                $this->sourceRootInfo = $this->superShare->getNodeCacheEntry();
124
+            }
125
+        }
126
+        return $this->sourceRootInfo;
127
+    }
128
+
129
+    /**
130
+     * @psalm-assert Storage $this->storage
131
+     */
132
+    private function init() {
133
+        if ($this->initialized) {
134
+            if (!$this->storage) {
135
+                // marked as initialized but no storage set
136
+                // this is probably because some code path has caused recursion during the share setup
137
+                // we setup a "failed storage" so `getWrapperStorage` doesn't return null.
138
+                // If the share setup completes after this the "failed storage" will be overwritten by the correct one
139
+                $ex = new \Exception('Possible share setup recursion detected for share ' . $this->superShare->getId());
140
+                $this->logger->warning($ex->getMessage(), ['exception' => $ex, 'app' => 'files_sharing']);
141
+                $this->storage = new FailedStorage(['exception' => $ex]);
142
+                $this->cache = new FailedCache();
143
+                $this->rootPath = '';
144
+            }
145
+            return;
146
+        }
147
+
148
+        $this->initialized = true;
149
+        self::$initDepth++;
150
+
151
+        try {
152
+            if (self::$initDepth > 10) {
153
+                throw new \Exception('Maximum share depth reached');
154
+            }
155
+
156
+            /** @var IRootFolder $rootFolder */
157
+            $rootFolder = Server::get(IRootFolder::class);
158
+            $this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner());
159
+            $sourceId = $this->superShare->getNodeId();
160
+            $ownerNodes = $this->ownerUserFolder->getById($sourceId);
161
+
162
+            if (count($ownerNodes) === 0) {
163
+                $this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]);
164
+                $this->cache = new FailedCache();
165
+                $this->rootPath = '';
166
+            } else {
167
+                foreach ($ownerNodes as $ownerNode) {
168
+                    $nonMaskedStorage = $ownerNode->getStorage();
169
+
170
+                    // check if potential source node would lead to a recursive share setup
171
+                    if ($nonMaskedStorage instanceof Wrapper && $nonMaskedStorage->isWrapperOf($this)) {
172
+                        continue;
173
+                    }
174
+                    $this->nonMaskedStorage = $nonMaskedStorage;
175
+                    $this->sourcePath = $ownerNode->getPath();
176
+                    $this->rootPath = $ownerNode->getInternalPath();
177
+                    $this->cache = null;
178
+                    break;
179
+                }
180
+                if (!$this->nonMaskedStorage) {
181
+                    // all potential source nodes would have been recursive
182
+                    throw new \Exception('recursive share detected');
183
+                }
184
+                $this->storage = new PermissionsMask([
185
+                    'storage' => $this->nonMaskedStorage,
186
+                    'mask' => $this->superShare->getPermissions(),
187
+                ]);
188
+            }
189
+        } catch (NotFoundException $e) {
190
+            // original file not accessible or deleted, set FailedStorage
191
+            $this->storage = new FailedStorage(['exception' => $e]);
192
+            $this->cache = new FailedCache();
193
+            $this->rootPath = '';
194
+        } catch (NoUserException $e) {
195
+            // sharer user deleted, set FailedStorage
196
+            $this->storage = new FailedStorage(['exception' => $e]);
197
+            $this->cache = new FailedCache();
198
+            $this->rootPath = '';
199
+        } catch (\Exception $e) {
200
+            $this->storage = new FailedStorage(['exception' => $e]);
201
+            $this->cache = new FailedCache();
202
+            $this->rootPath = '';
203
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
204
+        }
205
+
206
+        if (!$this->nonMaskedStorage) {
207
+            $this->nonMaskedStorage = $this->storage;
208
+        }
209
+        self::$initDepth--;
210
+    }
211
+
212
+    public function instanceOfStorage(string $class): bool {
213
+        if ($class === '\OC\Files\Storage\Common' || $class == Common::class) {
214
+            return true;
215
+        }
216
+        if (in_array($class, [
217
+            '\OC\Files\Storage\Home',
218
+            '\OC\Files\ObjectStore\HomeObjectStoreStorage',
219
+            '\OCP\Files\IHomeStorage',
220
+            Home::class,
221
+            HomeObjectStoreStorage::class,
222
+            IHomeStorage::class
223
+        ])) {
224
+            return false;
225
+        }
226
+        return parent::instanceOfStorage($class);
227
+    }
228
+
229
+    /**
230
+     * @return string
231
+     */
232
+    public function getShareId() {
233
+        return $this->superShare->getId();
234
+    }
235
+
236
+    private function isValid(): bool {
237
+        return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE;
238
+    }
239
+
240
+    public function getId(): string {
241
+        return 'shared::' . $this->getMountPoint();
242
+    }
243
+
244
+    public function getPermissions(string $path = ''): int {
245
+        if (!$this->isValid()) {
246
+            return 0;
247
+        }
248
+        $permissions = parent::getPermissions($path) & $this->superShare->getPermissions();
249
+
250
+        // part files and the mount point always have delete permissions
251
+        if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') {
252
+            $permissions |= Constants::PERMISSION_DELETE;
253
+        }
254
+
255
+        if ($this->sharingDisabledForUser) {
256
+            $permissions &= ~Constants::PERMISSION_SHARE;
257
+        }
258
+
259
+        return $permissions;
260
+    }
261
+
262
+    public function isCreatable(string $path): bool {
263
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
264
+    }
265
+
266
+    public function isReadable(string $path): bool {
267
+        if (!$this->isValid()) {
268
+            return false;
269
+        }
270
+        if (!$this->file_exists($path)) {
271
+            return false;
272
+        }
273
+        /** @var IStorage $storage */
274
+        /** @var string $internalPath */
275
+        [$storage, $internalPath] = $this->resolvePath($path);
276
+        return $storage->isReadable($internalPath);
277
+    }
278
+
279
+    public function isUpdatable(string $path): bool {
280
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
281
+    }
282
+
283
+    public function isDeletable(string $path): bool {
284
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
285
+    }
286
+
287
+    public function isSharable(string $path): bool {
288
+        if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) {
289
+            return false;
290
+        }
291
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
292
+    }
293
+
294
+    public function fopen(string $path, string $mode) {
295
+        $source = $this->getUnjailedPath($path);
296
+        switch ($mode) {
297
+            case 'r+':
298
+            case 'rb+':
299
+            case 'w+':
300
+            case 'wb+':
301
+            case 'x+':
302
+            case 'xb+':
303
+            case 'a+':
304
+            case 'ab+':
305
+            case 'w':
306
+            case 'wb':
307
+            case 'x':
308
+            case 'xb':
309
+            case 'a':
310
+            case 'ab':
311
+                $creatable = $this->isCreatable(dirname($path));
312
+                $updatable = $this->isUpdatable($path);
313
+                // if neither permissions given, no need to continue
314
+                if (!$creatable && !$updatable) {
315
+                    if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
316
+                        $updatable = $this->isUpdatable(dirname($path));
317
+                    }
318
+
319
+                    if (!$updatable) {
320
+                        return false;
321
+                    }
322
+                }
323
+
324
+                $exists = $this->file_exists($path);
325
+                // if a file exists, updatable permissions are required
326
+                if ($exists && !$updatable) {
327
+                    return false;
328
+                }
329
+
330
+                // part file is allowed if !$creatable but the final file is $updatable
331
+                if (pathinfo($path, PATHINFO_EXTENSION) !== 'part') {
332
+                    if (!$exists && !$creatable) {
333
+                        return false;
334
+                    }
335
+                }
336
+        }
337
+        $info = [
338
+            'target' => $this->getMountPoint() . '/' . $path,
339
+            'source' => $source,
340
+            'mode' => $mode,
341
+        ];
342
+        Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info);
343
+        return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode);
344
+    }
345
+
346
+    public function rename(string $source, string $target): bool {
347
+        $this->init();
348
+        $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part';
349
+        $targetExists = $this->file_exists($target);
350
+        $sameFolder = dirname($source) === dirname($target);
351
+
352
+        if ($targetExists || ($sameFolder && !$isPartFile)) {
353
+            if (!$this->isUpdatable('')) {
354
+                return false;
355
+            }
356
+        } else {
357
+            if (!$this->isCreatable('')) {
358
+                return false;
359
+            }
360
+        }
361
+
362
+        return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target));
363
+    }
364
+
365
+    /**
366
+     * return mount point of share, relative to data/user/files
367
+     *
368
+     * @return string
369
+     */
370
+    public function getMountPoint(): string {
371
+        return $this->superShare->getTarget();
372
+    }
373
+
374
+    public function setMountPoint(string $path): void {
375
+        $this->superShare->setTarget($path);
376
+
377
+        foreach ($this->groupedShares as $share) {
378
+            $share->setTarget($path);
379
+        }
380
+    }
381
+
382
+    /**
383
+     * get the user who shared the file
384
+     *
385
+     * @return string
386
+     */
387
+    public function getSharedFrom(): string {
388
+        return $this->superShare->getShareOwner();
389
+    }
390
+
391
+    public function getShare(): IShare {
392
+        return $this->superShare;
393
+    }
394
+
395
+    /**
396
+     * return share type, can be "file" or "folder"
397
+     *
398
+     * @return string
399
+     */
400
+    public function getItemType(): string {
401
+        return $this->superShare->getNodeType();
402
+    }
403
+
404
+    public function getCache(string $path = '', ?IStorage $storage = null): ICache {
405
+        if ($this->cache) {
406
+            return $this->cache;
407
+        }
408
+        if (!$storage) {
409
+            $storage = $this;
410
+        }
411
+        $sourceRoot = $this->getSourceRootInfo();
412
+        if ($this->storage instanceof FailedStorage) {
413
+            return new FailedCache();
414
+        }
415
+
416
+        $this->cache = new Cache(
417
+            $storage,
418
+            $sourceRoot,
419
+            Server::get(CacheDependencies::class),
420
+            $this->getShare()
421
+        );
422
+        return $this->cache;
423
+    }
424
+
425
+    public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
426
+        if (!$storage) {
427
+            $storage = $this;
428
+        }
429
+        return new Scanner($storage);
430
+    }
431
+
432
+    public function getOwner(string $path): string|false {
433
+        return $this->superShare->getShareOwner();
434
+    }
435
+
436
+    public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
437
+        if ($this->watcher) {
438
+            return $this->watcher;
439
+        }
440
+
441
+        // Get node information
442
+        $node = $this->getShare()->getNodeCacheEntry();
443
+        if ($node instanceof CacheEntry) {
444
+            $storageId = $node->getData()['storage_string_id'] ?? null;
445
+            // for shares from the home storage we can rely on the home storage to keep itself up to date
446
+            // for other storages we need use the proper watcher
447
+            if ($storageId !== null && !(str_starts_with($storageId, 'home::') || str_starts_with($storageId, 'object::user'))) {
448
+                $cache = $this->getCache();
449
+                $this->watcher = parent::getWatcher($path, $storage);
450
+                if ($cache instanceof Cache) {
451
+                    $this->watcher->onUpdate($cache->markRootChanged(...));
452
+                }
453
+                return $this->watcher;
454
+            }
455
+        }
456
+
457
+        // cache updating is handled by the share source
458
+        $this->watcher = new NullWatcher();
459
+        return $this->watcher;
460
+    }
461
+
462
+    /**
463
+     * unshare complete storage, also the grouped shares
464
+     *
465
+     * @return bool
466
+     */
467
+    public function unshareStorage(): bool {
468
+        foreach ($this->groupedShares as $share) {
469
+            Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user);
470
+        }
471
+        return true;
472
+    }
473
+
474
+    public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
475
+        /** @var ILockingStorage $targetStorage */
476
+        [$targetStorage, $targetInternalPath] = $this->resolvePath($path);
477
+        $targetStorage->acquireLock($targetInternalPath, $type, $provider);
478
+        // lock the parent folders of the owner when locking the share as recipient
479
+        if ($path === '') {
480
+            $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
481
+            $this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
482
+        }
483
+    }
484
+
485
+    public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
486
+        /** @var ILockingStorage $targetStorage */
487
+        [$targetStorage, $targetInternalPath] = $this->resolvePath($path);
488
+        $targetStorage->releaseLock($targetInternalPath, $type, $provider);
489
+        // unlock the parent folders of the owner when unlocking the share as recipient
490
+        if ($path === '') {
491
+            $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
492
+            $this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
493
+        }
494
+    }
495
+
496
+    public function changeLock(string $path, int $type, ILockingProvider $provider): void {
497
+        /** @var ILockingStorage $targetStorage */
498
+        [$targetStorage, $targetInternalPath] = $this->resolvePath($path);
499
+        $targetStorage->changeLock($targetInternalPath, $type, $provider);
500
+    }
501
+
502
+    public function getAvailability(): array {
503
+        // shares do not participate in availability logic
504
+        return [
505
+            'available' => true,
506
+            'last_checked' => 0,
507
+        ];
508
+    }
509
+
510
+    public function setAvailability(bool $isAvailable): void {
511
+        // shares do not participate in availability logic
512
+    }
513
+
514
+    public function getSourceStorage() {
515
+        $this->init();
516
+        return $this->nonMaskedStorage;
517
+    }
518
+
519
+    public function getWrapperStorage(): Storage {
520
+        $this->init();
521
+
522
+        /**
523
+         * @psalm-suppress DocblockTypeContradiction
524
+         */
525
+        if (!$this->storage) {
526
+            $message = 'no storage set after init for share ' . $this->getShareId();
527
+            $this->logger->error($message);
528
+            $this->storage = new FailedStorage(['exception' => new \Exception($message)]);
529
+        }
530
+
531
+        return $this->storage;
532
+    }
533
+
534
+    public function file_get_contents(string $path): string|false {
535
+        $info = [
536
+            'target' => $this->getMountPoint() . '/' . $path,
537
+            'source' => $this->getUnjailedPath($path),
538
+        ];
539
+        Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info);
540
+        return parent::file_get_contents($path);
541
+    }
542
+
543
+    public function file_put_contents(string $path, mixed $data): int|float|false {
544
+        $info = [
545
+            'target' => $this->getMountPoint() . '/' . $path,
546
+            'source' => $this->getUnjailedPath($path),
547
+        ];
548
+        Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info);
549
+        return parent::file_put_contents($path, $data);
550
+    }
551
+
552
+    public function setMountOptions(array $options): void {
553
+        /* Note: This value is never read */
554
+        $this->mountOptions = $options;
555
+    }
556
+
557
+    public function getUnjailedPath(string $path): string {
558
+        $this->init();
559
+        return parent::getUnjailedPath($path);
560
+    }
561
+
562
+    #[Override]
563
+    public function getDirectDownload(string $path): array|false {
564
+        // disable direct download for shares
565
+        return false;
566
+    }
567
+
568
+    #[Override]
569
+    public function getDirectDownloadById(string $fileId): array|false {
570
+        // disable direct download for shares
571
+        return false;
572
+    }
573 573
 }
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/File.php 1 patch
Indentation   +587 added lines, -587 removed lines patch added patch discarded remove patch
@@ -50,591 +50,591 @@
 block discarded – undo
50 50
 use Sabre\DAV\IFile;
51 51
 
52 52
 class File extends Node implements IFile {
53
-	protected IRequest $request;
54
-	protected IL10N $l10n;
55
-
56
-	/**
57
-	 * Sets up the node, expects a full path name
58
-	 *
59
-	 * @param View $view
60
-	 * @param FileInfo $info
61
-	 * @param ?\OCP\Share\IManager $shareManager
62
-	 * @param ?IRequest $request
63
-	 * @param ?IL10N $l10n
64
-	 */
65
-	public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) {
66
-		parent::__construct($view, $info, $shareManager);
67
-
68
-		if ($l10n) {
69
-			$this->l10n = $l10n;
70
-		} else {
71
-			// Querying IL10N directly results in a dependency loop
72
-			/** @var IL10NFactory $l10nFactory */
73
-			$l10nFactory = Server::get(IL10NFactory::class);
74
-			$this->l10n = $l10nFactory->get(Application::APP_ID);
75
-		}
76
-
77
-		if (isset($request)) {
78
-			$this->request = $request;
79
-		} else {
80
-			$this->request = Server::get(IRequest::class);
81
-		}
82
-	}
83
-
84
-	/**
85
-	 * Updates the data
86
-	 *
87
-	 * The data argument is a readable stream resource.
88
-	 *
89
-	 * After a successful put operation, you may choose to return an ETag. The
90
-	 * etag must always be surrounded by double-quotes. These quotes must
91
-	 * appear in the actual string you're returning.
92
-	 *
93
-	 * Clients may use the ETag from a PUT request to later on make sure that
94
-	 * when they update the file, the contents haven't changed in the mean
95
-	 * time.
96
-	 *
97
-	 * If you don't plan to store the file byte-by-byte, and you return a
98
-	 * different object on a subsequent GET you are strongly recommended to not
99
-	 * return an ETag, and just return null.
100
-	 *
101
-	 * @param resource|string $data
102
-	 *
103
-	 * @throws Forbidden
104
-	 * @throws UnsupportedMediaType
105
-	 * @throws BadRequest
106
-	 * @throws Exception
107
-	 * @throws EntityTooLarge
108
-	 * @throws ServiceUnavailable
109
-	 * @throws FileLocked
110
-	 * @return string|null
111
-	 */
112
-	public function put($data) {
113
-		try {
114
-			$exists = $this->fileView->file_exists($this->path);
115
-			if ($exists && !$this->info->isUpdateable()) {
116
-				throw new Forbidden();
117
-			}
118
-		} catch (StorageNotAvailableException $e) {
119
-			throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()]));
120
-		}
121
-
122
-		// verify path of the target
123
-		$this->verifyPath();
124
-
125
-		[$partStorage] = $this->fileView->resolvePath($this->path);
126
-		if ($partStorage === null) {
127
-			throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
128
-		}
129
-		$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
130
-
131
-		$view = Filesystem::getView();
132
-
133
-		if ($needsPartFile) {
134
-			$transferId = \rand();
135
-			// mark file as partial while uploading (ignored by the scanner)
136
-			$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part';
137
-
138
-			if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
139
-				$needsPartFile = false;
140
-			}
141
-		}
142
-		if (!$needsPartFile) {
143
-			// upload file directly as the final path
144
-			$partFilePath = $this->path;
145
-
146
-			if ($view && !$this->emitPreHooks($exists)) {
147
-				throw new Exception($this->l10n->t('Could not write to final file, canceled by hook'));
148
-			}
149
-		}
150
-
151
-		// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
152
-		[$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
153
-		[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
154
-		if ($partStorage === null || $storage === null) {
155
-			throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
156
-		}
157
-		try {
158
-			if (!$needsPartFile) {
159
-				try {
160
-					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
161
-				} catch (LockedException $e) {
162
-					// during very large uploads, the shared lock we got at the start might have been expired
163
-					// meaning that the above lock can fail not just only because somebody else got a shared lock
164
-					// or because there is no existing shared lock to make exclusive
165
-					//
166
-					// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
167
-					// lock this will still fail, if our original shared lock expired the new lock will be successful and
168
-					// the entire operation will be safe
169
-
170
-					try {
171
-						$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
172
-					} catch (LockedException $ex) {
173
-						throw new FileLocked($e->getMessage(), $e->getCode(), $e);
174
-					}
175
-				}
176
-			}
177
-
178
-			if (!is_resource($data)) {
179
-				$tmpData = fopen('php://temp', 'r+');
180
-				if ($data !== null) {
181
-					fwrite($tmpData, $data);
182
-					rewind($tmpData);
183
-				}
184
-				$data = $tmpData;
185
-			}
186
-
187
-			if ($this->request->getHeader('X-HASH') !== '') {
188
-				$hash = $this->request->getHeader('X-HASH');
189
-				if ($hash === 'all' || $hash === 'md5') {
190
-					$data = HashWrapper::wrap($data, 'md5', function ($hash): void {
191
-						$this->header('X-Hash-MD5: ' . $hash);
192
-					});
193
-				}
194
-
195
-				if ($hash === 'all' || $hash === 'sha1') {
196
-					$data = HashWrapper::wrap($data, 'sha1', function ($hash): void {
197
-						$this->header('X-Hash-SHA1: ' . $hash);
198
-					});
199
-				}
200
-
201
-				if ($hash === 'all' || $hash === 'sha256') {
202
-					$data = HashWrapper::wrap($data, 'sha256', function ($hash): void {
203
-						$this->header('X-Hash-SHA256: ' . $hash);
204
-					});
205
-				}
206
-			}
207
-
208
-			$lengthHeader = $this->request->getHeader('content-length');
209
-			$expected = $lengthHeader !== '' ? (int)$lengthHeader : null;
210
-
211
-			if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
212
-				$isEOF = false;
213
-				$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
214
-					$isEOF = feof($stream);
215
-				});
216
-
217
-				$result = is_resource($wrappedData);
218
-				if ($result) {
219
-					$count = -1;
220
-					try {
221
-						/** @var IWriteStreamStorage $partStorage */
222
-						$count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected);
223
-					} catch (GenericFileException $e) {
224
-						$logger = Server::get(LoggerInterface::class);
225
-						$logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
226
-						$result = $isEOF;
227
-						if (is_resource($wrappedData)) {
228
-							$result = feof($wrappedData);
229
-						}
230
-					}
231
-				}
232
-			} else {
233
-				$target = $partStorage->fopen($internalPartPath, 'wb');
234
-				if ($target === false) {
235
-					Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
236
-					// because we have no clue about the cause we can only throw back a 500/Internal Server Error
237
-					throw new Exception($this->l10n->t('Could not write file contents'));
238
-				}
239
-				[$count, $result] = Files::streamCopy($data, $target, true);
240
-				fclose($target);
241
-			}
242
-			if ($result === false && $expected !== null) {
243
-				throw new Exception(
244
-					$this->l10n->t(
245
-						'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
246
-						[
247
-							$this->l10n->n('%n byte', '%n bytes', $count),
248
-							$this->l10n->n('%n byte', '%n bytes', $expected),
249
-						],
250
-					)
251
-				);
252
-			}
253
-
254
-			// if content length is sent by client:
255
-			// double check if the file was fully received
256
-			// compare expected and actual size
257
-			if ($expected !== null
258
-				&& $expected !== $count
259
-				&& $this->request->getMethod() === 'PUT'
260
-			) {
261
-				throw new BadRequest(
262
-					$this->l10n->t(
263
-						'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
264
-						[
265
-							$this->l10n->n('%n byte', '%n bytes', $expected),
266
-							$this->l10n->n('%n byte', '%n bytes', $count),
267
-						],
268
-					)
269
-				);
270
-			}
271
-		} catch (\Exception $e) {
272
-			if ($e instanceof LockedException) {
273
-				Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
274
-			} else {
275
-				Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
276
-			}
277
-
278
-			if ($needsPartFile) {
279
-				$partStorage->unlink($internalPartPath);
280
-			}
281
-			$this->convertToSabreException($e);
282
-		}
283
-
284
-		try {
285
-			if ($needsPartFile) {
286
-				if ($view && !$this->emitPreHooks($exists)) {
287
-					$partStorage->unlink($internalPartPath);
288
-					throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook'));
289
-				}
290
-				try {
291
-					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
292
-				} catch (LockedException $e) {
293
-					// during very large uploads, the shared lock we got at the start might have been expired
294
-					// meaning that the above lock can fail not just only because somebody else got a shared lock
295
-					// or because there is no existing shared lock to make exclusive
296
-					//
297
-					// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
298
-					// lock this will still fail, if our original shared lock expired the new lock will be successful and
299
-					// the entire operation will be safe
300
-
301
-					try {
302
-						$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
303
-					} catch (LockedException $ex) {
304
-						if ($needsPartFile) {
305
-							$partStorage->unlink($internalPartPath);
306
-						}
307
-						throw new FileLocked($e->getMessage(), $e->getCode(), $e);
308
-					}
309
-				}
310
-
311
-				// rename to correct path
312
-				try {
313
-					$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
314
-					$fileExists = $storage->file_exists($internalPath);
315
-					if ($renameOkay === false || $fileExists === false) {
316
-						Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
317
-						throw new Exception($this->l10n->t('Could not rename part file to final file'));
318
-					}
319
-				} catch (ForbiddenException $ex) {
320
-					if (!$ex->getRetry()) {
321
-						$partStorage->unlink($internalPartPath);
322
-					}
323
-					throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
324
-				} catch (\Exception $e) {
325
-					$partStorage->unlink($internalPartPath);
326
-					$this->convertToSabreException($e);
327
-				}
328
-			}
329
-
330
-			// since we skipped the view we need to scan and emit the hooks ourselves
331
-			$storage->getUpdater()->update($internalPath);
332
-
333
-			try {
334
-				$this->changeLock(ILockingProvider::LOCK_SHARED);
335
-			} catch (LockedException $e) {
336
-				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
337
-			}
338
-
339
-			// allow sync clients to send the mtime along in a header
340
-			$mtimeHeader = $this->request->getHeader('x-oc-mtime');
341
-			if ($mtimeHeader !== '') {
342
-				$mtime = $this->sanitizeMtime($mtimeHeader);
343
-				if ($this->fileView->touch($this->path, $mtime)) {
344
-					$this->header('X-OC-MTime: accepted');
345
-				}
346
-			}
347
-
348
-			$fileInfoUpdate = [
349
-				'upload_time' => time()
350
-			];
351
-
352
-			// allow sync clients to send the creation time along in a header
353
-			$ctimeHeader = $this->request->getHeader('x-oc-ctime');
354
-			if ($ctimeHeader) {
355
-				$ctime = $this->sanitizeMtime($ctimeHeader);
356
-				$fileInfoUpdate['creation_time'] = $ctime;
357
-				$this->header('X-OC-CTime: accepted');
358
-			}
359
-
360
-			$this->fileView->putFileInfo($this->path, $fileInfoUpdate);
361
-
362
-			if ($view) {
363
-				$this->emitPostHooks($exists);
364
-			}
365
-
366
-			$this->refreshInfo();
367
-
368
-			$checksumHeader = $this->request->getHeader('oc-checksum');
369
-			if ($checksumHeader) {
370
-				$checksum = trim($checksumHeader);
371
-				$this->setChecksum($checksum);
372
-			} elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
373
-				$this->setChecksum('');
374
-			}
375
-		} catch (StorageNotAvailableException $e) {
376
-			throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e);
377
-		}
378
-
379
-		return '"' . $this->info->getEtag() . '"';
380
-	}
381
-
382
-	private function getPartFileBasePath($path) {
383
-		$partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true);
384
-		if ($partFileInStorage) {
385
-			$filename = basename($path);
386
-			// hash does not need to be secure but fast and semi unique
387
-			$hashedFilename = hash('xxh128', $filename);
388
-			return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename;
389
-		} else {
390
-			// will place the .part file in the users root directory
391
-			// therefor we need to make the name (semi) unique - hash does not need to be secure but fast.
392
-			return hash('xxh128', $path);
393
-		}
394
-	}
395
-
396
-	private function emitPreHooks(bool $exists, ?string $path = null): bool {
397
-		if (is_null($path)) {
398
-			$path = $this->path;
399
-		}
400
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
401
-		if ($hookPath === null) {
402
-			// We only trigger hooks from inside default view
403
-			return true;
404
-		}
405
-		$run = true;
406
-
407
-		if (!$exists) {
408
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
409
-				Filesystem::signal_param_path => $hookPath,
410
-				Filesystem::signal_param_run => &$run,
411
-			]);
412
-		} else {
413
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
414
-				Filesystem::signal_param_path => $hookPath,
415
-				Filesystem::signal_param_run => &$run,
416
-			]);
417
-		}
418
-		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
419
-			Filesystem::signal_param_path => $hookPath,
420
-			Filesystem::signal_param_run => &$run,
421
-		]);
422
-		return $run;
423
-	}
424
-
425
-	private function emitPostHooks(bool $exists, ?string $path = null): void {
426
-		if (is_null($path)) {
427
-			$path = $this->path;
428
-		}
429
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
430
-		if ($hookPath === null) {
431
-			// We only trigger hooks from inside default view
432
-			return;
433
-		}
434
-		if (!$exists) {
435
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
436
-				Filesystem::signal_param_path => $hookPath
437
-			]);
438
-		} else {
439
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
440
-				Filesystem::signal_param_path => $hookPath
441
-			]);
442
-		}
443
-		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
444
-			Filesystem::signal_param_path => $hookPath
445
-		]);
446
-	}
447
-
448
-	/**
449
-	 * Returns the data
450
-	 *
451
-	 * @return resource
452
-	 * @throws Forbidden
453
-	 * @throws ServiceUnavailable
454
-	 */
455
-	public function get() {
456
-		//throw exception if encryption is disabled but files are still encrypted
457
-		try {
458
-			if (!$this->info->isReadable()) {
459
-				// do a if the file did not exist
460
-				throw new NotFound();
461
-			}
462
-			$path = ltrim($this->path, '/');
463
-			try {
464
-				$res = $this->fileView->fopen($path, 'rb');
465
-			} catch (\Exception $e) {
466
-				$this->convertToSabreException($e);
467
-			}
468
-
469
-			if ($res === false) {
470
-				if ($this->fileView->file_exists($path)) {
471
-					throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path]));
472
-				} else {
473
-					throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path]));
474
-				}
475
-			}
476
-
477
-			// comparing current file size with the one in DB
478
-			// if different, fix DB and refresh cache.
479
-			if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
480
-				$logger = Server::get(LoggerInterface::class);
481
-				$logger->warning('fixing cached size of file id=' . $this->getId());
482
-
483
-				$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
484
-				$this->refreshInfo();
485
-			}
486
-
487
-			return $res;
488
-		} catch (GenericEncryptionException $e) {
489
-			// returning 503 will allow retry of the operation at a later point in time
490
-			throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]));
491
-		} catch (StorageNotAvailableException $e) {
492
-			throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()]));
493
-		} catch (ForbiddenException $ex) {
494
-			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
495
-		} catch (LockedException $e) {
496
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
497
-		}
498
-	}
499
-
500
-	/**
501
-	 * Delete the current file
502
-	 *
503
-	 * @throws Forbidden
504
-	 * @throws ServiceUnavailable
505
-	 */
506
-	public function delete() {
507
-		if (!$this->info->isDeletable()) {
508
-			throw new Forbidden();
509
-		}
510
-
511
-		try {
512
-			if (!$this->fileView->unlink($this->path)) {
513
-				// assume it wasn't possible to delete due to permissions
514
-				throw new Forbidden();
515
-			}
516
-		} catch (StorageNotAvailableException $e) {
517
-			throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %1$s', [$e->getMessage()]));
518
-		} catch (ForbiddenException $ex) {
519
-			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
520
-		} catch (LockedException $e) {
521
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
522
-		}
523
-	}
524
-
525
-	/**
526
-	 * Returns the mime-type for a file
527
-	 *
528
-	 * If null is returned, we'll assume application/octet-stream
529
-	 *
530
-	 * @return string
531
-	 */
532
-	public function getContentType() {
533
-		$mimeType = $this->info->getMimetype();
534
-
535
-		// PROPFIND needs to return the correct mime type, for consistency with the web UI
536
-		if ($this->request->getMethod() === 'PROPFIND') {
537
-			return $mimeType;
538
-		}
539
-		return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType);
540
-	}
541
-
542
-	/**
543
-	 * @throws NotFoundException
544
-	 * @throws NotPermittedException
545
-	 */
546
-	public function getDirectDownload(): array|false {
547
-		if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) {
548
-			return false;
549
-		}
550
-		$node = $this->getNode();
551
-		$storage = $node->getStorage();
552
-		if (!$storage) {
553
-			return false;
554
-		}
555
-
556
-		if (!($node->getPermissions() & Constants::PERMISSION_READ)) {
557
-			return false;
558
-		}
559
-
560
-		return $storage->getDirectDownloadById((string)$node->getId());
561
-	}
562
-
563
-	/**
564
-	 * Convert the given exception to a SabreException instance
565
-	 *
566
-	 * @param \Exception $e
567
-	 *
568
-	 * @throws \Sabre\DAV\Exception
569
-	 */
570
-	private function convertToSabreException(\Exception $e) {
571
-		if ($e instanceof \Sabre\DAV\Exception) {
572
-			throw $e;
573
-		}
574
-		if ($e instanceof NotPermittedException) {
575
-			// a more general case - due to whatever reason the content could not be written
576
-			throw new Forbidden($e->getMessage(), 0, $e);
577
-		}
578
-		if ($e instanceof ForbiddenException) {
579
-			// the path for the file was forbidden
580
-			throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
581
-		}
582
-		if ($e instanceof EntityTooLargeException) {
583
-			// the file is too big to be stored
584
-			throw new EntityTooLarge($e->getMessage(), 0, $e);
585
-		}
586
-		if ($e instanceof InvalidContentException) {
587
-			// the file content is not permitted
588
-			throw new UnsupportedMediaType($e->getMessage(), 0, $e);
589
-		}
590
-		if ($e instanceof InvalidPathException) {
591
-			// the path for the file was not valid
592
-			// TODO: find proper http status code for this case
593
-			throw new Forbidden($e->getMessage(), 0, $e);
594
-		}
595
-		if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
596
-			// the file is currently being written to by another process
597
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
598
-		}
599
-		if ($e instanceof GenericEncryptionException) {
600
-			// returning 503 will allow retry of the operation at a later point in time
601
-			throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e);
602
-		}
603
-		if ($e instanceof StorageNotAvailableException) {
604
-			throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e);
605
-		}
606
-		if ($e instanceof NotFoundException) {
607
-			throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e);
608
-		}
609
-
610
-		throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
611
-	}
612
-
613
-	/**
614
-	 * Get the checksum for this file
615
-	 *
616
-	 * @return string|null
617
-	 */
618
-	public function getChecksum() {
619
-		return $this->info->getChecksum();
620
-	}
621
-
622
-	public function setChecksum(string $checksum) {
623
-		$this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
624
-		$this->refreshInfo();
625
-	}
626
-
627
-	protected function header($string) {
628
-		if (!\OC::$CLI) {
629
-			\header($string);
630
-		}
631
-	}
632
-
633
-	public function hash(string $type) {
634
-		return $this->fileView->hash($type, $this->path);
635
-	}
636
-
637
-	public function getNode(): \OCP\Files\File {
638
-		return $this->node;
639
-	}
53
+    protected IRequest $request;
54
+    protected IL10N $l10n;
55
+
56
+    /**
57
+     * Sets up the node, expects a full path name
58
+     *
59
+     * @param View $view
60
+     * @param FileInfo $info
61
+     * @param ?\OCP\Share\IManager $shareManager
62
+     * @param ?IRequest $request
63
+     * @param ?IL10N $l10n
64
+     */
65
+    public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) {
66
+        parent::__construct($view, $info, $shareManager);
67
+
68
+        if ($l10n) {
69
+            $this->l10n = $l10n;
70
+        } else {
71
+            // Querying IL10N directly results in a dependency loop
72
+            /** @var IL10NFactory $l10nFactory */
73
+            $l10nFactory = Server::get(IL10NFactory::class);
74
+            $this->l10n = $l10nFactory->get(Application::APP_ID);
75
+        }
76
+
77
+        if (isset($request)) {
78
+            $this->request = $request;
79
+        } else {
80
+            $this->request = Server::get(IRequest::class);
81
+        }
82
+    }
83
+
84
+    /**
85
+     * Updates the data
86
+     *
87
+     * The data argument is a readable stream resource.
88
+     *
89
+     * After a successful put operation, you may choose to return an ETag. The
90
+     * etag must always be surrounded by double-quotes. These quotes must
91
+     * appear in the actual string you're returning.
92
+     *
93
+     * Clients may use the ETag from a PUT request to later on make sure that
94
+     * when they update the file, the contents haven't changed in the mean
95
+     * time.
96
+     *
97
+     * If you don't plan to store the file byte-by-byte, and you return a
98
+     * different object on a subsequent GET you are strongly recommended to not
99
+     * return an ETag, and just return null.
100
+     *
101
+     * @param resource|string $data
102
+     *
103
+     * @throws Forbidden
104
+     * @throws UnsupportedMediaType
105
+     * @throws BadRequest
106
+     * @throws Exception
107
+     * @throws EntityTooLarge
108
+     * @throws ServiceUnavailable
109
+     * @throws FileLocked
110
+     * @return string|null
111
+     */
112
+    public function put($data) {
113
+        try {
114
+            $exists = $this->fileView->file_exists($this->path);
115
+            if ($exists && !$this->info->isUpdateable()) {
116
+                throw new Forbidden();
117
+            }
118
+        } catch (StorageNotAvailableException $e) {
119
+            throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()]));
120
+        }
121
+
122
+        // verify path of the target
123
+        $this->verifyPath();
124
+
125
+        [$partStorage] = $this->fileView->resolvePath($this->path);
126
+        if ($partStorage === null) {
127
+            throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
128
+        }
129
+        $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
130
+
131
+        $view = Filesystem::getView();
132
+
133
+        if ($needsPartFile) {
134
+            $transferId = \rand();
135
+            // mark file as partial while uploading (ignored by the scanner)
136
+            $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part';
137
+
138
+            if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
139
+                $needsPartFile = false;
140
+            }
141
+        }
142
+        if (!$needsPartFile) {
143
+            // upload file directly as the final path
144
+            $partFilePath = $this->path;
145
+
146
+            if ($view && !$this->emitPreHooks($exists)) {
147
+                throw new Exception($this->l10n->t('Could not write to final file, canceled by hook'));
148
+            }
149
+        }
150
+
151
+        // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
152
+        [$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
153
+        [$storage, $internalPath] = $this->fileView->resolvePath($this->path);
154
+        if ($partStorage === null || $storage === null) {
155
+            throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
156
+        }
157
+        try {
158
+            if (!$needsPartFile) {
159
+                try {
160
+                    $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
161
+                } catch (LockedException $e) {
162
+                    // during very large uploads, the shared lock we got at the start might have been expired
163
+                    // meaning that the above lock can fail not just only because somebody else got a shared lock
164
+                    // or because there is no existing shared lock to make exclusive
165
+                    //
166
+                    // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
167
+                    // lock this will still fail, if our original shared lock expired the new lock will be successful and
168
+                    // the entire operation will be safe
169
+
170
+                    try {
171
+                        $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
172
+                    } catch (LockedException $ex) {
173
+                        throw new FileLocked($e->getMessage(), $e->getCode(), $e);
174
+                    }
175
+                }
176
+            }
177
+
178
+            if (!is_resource($data)) {
179
+                $tmpData = fopen('php://temp', 'r+');
180
+                if ($data !== null) {
181
+                    fwrite($tmpData, $data);
182
+                    rewind($tmpData);
183
+                }
184
+                $data = $tmpData;
185
+            }
186
+
187
+            if ($this->request->getHeader('X-HASH') !== '') {
188
+                $hash = $this->request->getHeader('X-HASH');
189
+                if ($hash === 'all' || $hash === 'md5') {
190
+                    $data = HashWrapper::wrap($data, 'md5', function ($hash): void {
191
+                        $this->header('X-Hash-MD5: ' . $hash);
192
+                    });
193
+                }
194
+
195
+                if ($hash === 'all' || $hash === 'sha1') {
196
+                    $data = HashWrapper::wrap($data, 'sha1', function ($hash): void {
197
+                        $this->header('X-Hash-SHA1: ' . $hash);
198
+                    });
199
+                }
200
+
201
+                if ($hash === 'all' || $hash === 'sha256') {
202
+                    $data = HashWrapper::wrap($data, 'sha256', function ($hash): void {
203
+                        $this->header('X-Hash-SHA256: ' . $hash);
204
+                    });
205
+                }
206
+            }
207
+
208
+            $lengthHeader = $this->request->getHeader('content-length');
209
+            $expected = $lengthHeader !== '' ? (int)$lengthHeader : null;
210
+
211
+            if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
212
+                $isEOF = false;
213
+                $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
214
+                    $isEOF = feof($stream);
215
+                });
216
+
217
+                $result = is_resource($wrappedData);
218
+                if ($result) {
219
+                    $count = -1;
220
+                    try {
221
+                        /** @var IWriteStreamStorage $partStorage */
222
+                        $count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected);
223
+                    } catch (GenericFileException $e) {
224
+                        $logger = Server::get(LoggerInterface::class);
225
+                        $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
226
+                        $result = $isEOF;
227
+                        if (is_resource($wrappedData)) {
228
+                            $result = feof($wrappedData);
229
+                        }
230
+                    }
231
+                }
232
+            } else {
233
+                $target = $partStorage->fopen($internalPartPath, 'wb');
234
+                if ($target === false) {
235
+                    Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
236
+                    // because we have no clue about the cause we can only throw back a 500/Internal Server Error
237
+                    throw new Exception($this->l10n->t('Could not write file contents'));
238
+                }
239
+                [$count, $result] = Files::streamCopy($data, $target, true);
240
+                fclose($target);
241
+            }
242
+            if ($result === false && $expected !== null) {
243
+                throw new Exception(
244
+                    $this->l10n->t(
245
+                        'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
246
+                        [
247
+                            $this->l10n->n('%n byte', '%n bytes', $count),
248
+                            $this->l10n->n('%n byte', '%n bytes', $expected),
249
+                        ],
250
+                    )
251
+                );
252
+            }
253
+
254
+            // if content length is sent by client:
255
+            // double check if the file was fully received
256
+            // compare expected and actual size
257
+            if ($expected !== null
258
+                && $expected !== $count
259
+                && $this->request->getMethod() === 'PUT'
260
+            ) {
261
+                throw new BadRequest(
262
+                    $this->l10n->t(
263
+                        'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
264
+                        [
265
+                            $this->l10n->n('%n byte', '%n bytes', $expected),
266
+                            $this->l10n->n('%n byte', '%n bytes', $count),
267
+                        ],
268
+                    )
269
+                );
270
+            }
271
+        } catch (\Exception $e) {
272
+            if ($e instanceof LockedException) {
273
+                Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
274
+            } else {
275
+                Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
276
+            }
277
+
278
+            if ($needsPartFile) {
279
+                $partStorage->unlink($internalPartPath);
280
+            }
281
+            $this->convertToSabreException($e);
282
+        }
283
+
284
+        try {
285
+            if ($needsPartFile) {
286
+                if ($view && !$this->emitPreHooks($exists)) {
287
+                    $partStorage->unlink($internalPartPath);
288
+                    throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook'));
289
+                }
290
+                try {
291
+                    $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
292
+                } catch (LockedException $e) {
293
+                    // during very large uploads, the shared lock we got at the start might have been expired
294
+                    // meaning that the above lock can fail not just only because somebody else got a shared lock
295
+                    // or because there is no existing shared lock to make exclusive
296
+                    //
297
+                    // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
298
+                    // lock this will still fail, if our original shared lock expired the new lock will be successful and
299
+                    // the entire operation will be safe
300
+
301
+                    try {
302
+                        $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
303
+                    } catch (LockedException $ex) {
304
+                        if ($needsPartFile) {
305
+                            $partStorage->unlink($internalPartPath);
306
+                        }
307
+                        throw new FileLocked($e->getMessage(), $e->getCode(), $e);
308
+                    }
309
+                }
310
+
311
+                // rename to correct path
312
+                try {
313
+                    $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
314
+                    $fileExists = $storage->file_exists($internalPath);
315
+                    if ($renameOkay === false || $fileExists === false) {
316
+                        Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
317
+                        throw new Exception($this->l10n->t('Could not rename part file to final file'));
318
+                    }
319
+                } catch (ForbiddenException $ex) {
320
+                    if (!$ex->getRetry()) {
321
+                        $partStorage->unlink($internalPartPath);
322
+                    }
323
+                    throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
324
+                } catch (\Exception $e) {
325
+                    $partStorage->unlink($internalPartPath);
326
+                    $this->convertToSabreException($e);
327
+                }
328
+            }
329
+
330
+            // since we skipped the view we need to scan and emit the hooks ourselves
331
+            $storage->getUpdater()->update($internalPath);
332
+
333
+            try {
334
+                $this->changeLock(ILockingProvider::LOCK_SHARED);
335
+            } catch (LockedException $e) {
336
+                throw new FileLocked($e->getMessage(), $e->getCode(), $e);
337
+            }
338
+
339
+            // allow sync clients to send the mtime along in a header
340
+            $mtimeHeader = $this->request->getHeader('x-oc-mtime');
341
+            if ($mtimeHeader !== '') {
342
+                $mtime = $this->sanitizeMtime($mtimeHeader);
343
+                if ($this->fileView->touch($this->path, $mtime)) {
344
+                    $this->header('X-OC-MTime: accepted');
345
+                }
346
+            }
347
+
348
+            $fileInfoUpdate = [
349
+                'upload_time' => time()
350
+            ];
351
+
352
+            // allow sync clients to send the creation time along in a header
353
+            $ctimeHeader = $this->request->getHeader('x-oc-ctime');
354
+            if ($ctimeHeader) {
355
+                $ctime = $this->sanitizeMtime($ctimeHeader);
356
+                $fileInfoUpdate['creation_time'] = $ctime;
357
+                $this->header('X-OC-CTime: accepted');
358
+            }
359
+
360
+            $this->fileView->putFileInfo($this->path, $fileInfoUpdate);
361
+
362
+            if ($view) {
363
+                $this->emitPostHooks($exists);
364
+            }
365
+
366
+            $this->refreshInfo();
367
+
368
+            $checksumHeader = $this->request->getHeader('oc-checksum');
369
+            if ($checksumHeader) {
370
+                $checksum = trim($checksumHeader);
371
+                $this->setChecksum($checksum);
372
+            } elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
373
+                $this->setChecksum('');
374
+            }
375
+        } catch (StorageNotAvailableException $e) {
376
+            throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e);
377
+        }
378
+
379
+        return '"' . $this->info->getEtag() . '"';
380
+    }
381
+
382
+    private function getPartFileBasePath($path) {
383
+        $partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true);
384
+        if ($partFileInStorage) {
385
+            $filename = basename($path);
386
+            // hash does not need to be secure but fast and semi unique
387
+            $hashedFilename = hash('xxh128', $filename);
388
+            return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename;
389
+        } else {
390
+            // will place the .part file in the users root directory
391
+            // therefor we need to make the name (semi) unique - hash does not need to be secure but fast.
392
+            return hash('xxh128', $path);
393
+        }
394
+    }
395
+
396
+    private function emitPreHooks(bool $exists, ?string $path = null): bool {
397
+        if (is_null($path)) {
398
+            $path = $this->path;
399
+        }
400
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
401
+        if ($hookPath === null) {
402
+            // We only trigger hooks from inside default view
403
+            return true;
404
+        }
405
+        $run = true;
406
+
407
+        if (!$exists) {
408
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
409
+                Filesystem::signal_param_path => $hookPath,
410
+                Filesystem::signal_param_run => &$run,
411
+            ]);
412
+        } else {
413
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
414
+                Filesystem::signal_param_path => $hookPath,
415
+                Filesystem::signal_param_run => &$run,
416
+            ]);
417
+        }
418
+        \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
419
+            Filesystem::signal_param_path => $hookPath,
420
+            Filesystem::signal_param_run => &$run,
421
+        ]);
422
+        return $run;
423
+    }
424
+
425
+    private function emitPostHooks(bool $exists, ?string $path = null): void {
426
+        if (is_null($path)) {
427
+            $path = $this->path;
428
+        }
429
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
430
+        if ($hookPath === null) {
431
+            // We only trigger hooks from inside default view
432
+            return;
433
+        }
434
+        if (!$exists) {
435
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
436
+                Filesystem::signal_param_path => $hookPath
437
+            ]);
438
+        } else {
439
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
440
+                Filesystem::signal_param_path => $hookPath
441
+            ]);
442
+        }
443
+        \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
444
+            Filesystem::signal_param_path => $hookPath
445
+        ]);
446
+    }
447
+
448
+    /**
449
+     * Returns the data
450
+     *
451
+     * @return resource
452
+     * @throws Forbidden
453
+     * @throws ServiceUnavailable
454
+     */
455
+    public function get() {
456
+        //throw exception if encryption is disabled but files are still encrypted
457
+        try {
458
+            if (!$this->info->isReadable()) {
459
+                // do a if the file did not exist
460
+                throw new NotFound();
461
+            }
462
+            $path = ltrim($this->path, '/');
463
+            try {
464
+                $res = $this->fileView->fopen($path, 'rb');
465
+            } catch (\Exception $e) {
466
+                $this->convertToSabreException($e);
467
+            }
468
+
469
+            if ($res === false) {
470
+                if ($this->fileView->file_exists($path)) {
471
+                    throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path]));
472
+                } else {
473
+                    throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path]));
474
+                }
475
+            }
476
+
477
+            // comparing current file size with the one in DB
478
+            // if different, fix DB and refresh cache.
479
+            if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
480
+                $logger = Server::get(LoggerInterface::class);
481
+                $logger->warning('fixing cached size of file id=' . $this->getId());
482
+
483
+                $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
484
+                $this->refreshInfo();
485
+            }
486
+
487
+            return $res;
488
+        } catch (GenericEncryptionException $e) {
489
+            // returning 503 will allow retry of the operation at a later point in time
490
+            throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]));
491
+        } catch (StorageNotAvailableException $e) {
492
+            throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()]));
493
+        } catch (ForbiddenException $ex) {
494
+            throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
495
+        } catch (LockedException $e) {
496
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
497
+        }
498
+    }
499
+
500
+    /**
501
+     * Delete the current file
502
+     *
503
+     * @throws Forbidden
504
+     * @throws ServiceUnavailable
505
+     */
506
+    public function delete() {
507
+        if (!$this->info->isDeletable()) {
508
+            throw new Forbidden();
509
+        }
510
+
511
+        try {
512
+            if (!$this->fileView->unlink($this->path)) {
513
+                // assume it wasn't possible to delete due to permissions
514
+                throw new Forbidden();
515
+            }
516
+        } catch (StorageNotAvailableException $e) {
517
+            throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %1$s', [$e->getMessage()]));
518
+        } catch (ForbiddenException $ex) {
519
+            throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
520
+        } catch (LockedException $e) {
521
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
522
+        }
523
+    }
524
+
525
+    /**
526
+     * Returns the mime-type for a file
527
+     *
528
+     * If null is returned, we'll assume application/octet-stream
529
+     *
530
+     * @return string
531
+     */
532
+    public function getContentType() {
533
+        $mimeType = $this->info->getMimetype();
534
+
535
+        // PROPFIND needs to return the correct mime type, for consistency with the web UI
536
+        if ($this->request->getMethod() === 'PROPFIND') {
537
+            return $mimeType;
538
+        }
539
+        return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType);
540
+    }
541
+
542
+    /**
543
+     * @throws NotFoundException
544
+     * @throws NotPermittedException
545
+     */
546
+    public function getDirectDownload(): array|false {
547
+        if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) {
548
+            return false;
549
+        }
550
+        $node = $this->getNode();
551
+        $storage = $node->getStorage();
552
+        if (!$storage) {
553
+            return false;
554
+        }
555
+
556
+        if (!($node->getPermissions() & Constants::PERMISSION_READ)) {
557
+            return false;
558
+        }
559
+
560
+        return $storage->getDirectDownloadById((string)$node->getId());
561
+    }
562
+
563
+    /**
564
+     * Convert the given exception to a SabreException instance
565
+     *
566
+     * @param \Exception $e
567
+     *
568
+     * @throws \Sabre\DAV\Exception
569
+     */
570
+    private function convertToSabreException(\Exception $e) {
571
+        if ($e instanceof \Sabre\DAV\Exception) {
572
+            throw $e;
573
+        }
574
+        if ($e instanceof NotPermittedException) {
575
+            // a more general case - due to whatever reason the content could not be written
576
+            throw new Forbidden($e->getMessage(), 0, $e);
577
+        }
578
+        if ($e instanceof ForbiddenException) {
579
+            // the path for the file was forbidden
580
+            throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
581
+        }
582
+        if ($e instanceof EntityTooLargeException) {
583
+            // the file is too big to be stored
584
+            throw new EntityTooLarge($e->getMessage(), 0, $e);
585
+        }
586
+        if ($e instanceof InvalidContentException) {
587
+            // the file content is not permitted
588
+            throw new UnsupportedMediaType($e->getMessage(), 0, $e);
589
+        }
590
+        if ($e instanceof InvalidPathException) {
591
+            // the path for the file was not valid
592
+            // TODO: find proper http status code for this case
593
+            throw new Forbidden($e->getMessage(), 0, $e);
594
+        }
595
+        if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
596
+            // the file is currently being written to by another process
597
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
598
+        }
599
+        if ($e instanceof GenericEncryptionException) {
600
+            // returning 503 will allow retry of the operation at a later point in time
601
+            throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e);
602
+        }
603
+        if ($e instanceof StorageNotAvailableException) {
604
+            throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e);
605
+        }
606
+        if ($e instanceof NotFoundException) {
607
+            throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e);
608
+        }
609
+
610
+        throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
611
+    }
612
+
613
+    /**
614
+     * Get the checksum for this file
615
+     *
616
+     * @return string|null
617
+     */
618
+    public function getChecksum() {
619
+        return $this->info->getChecksum();
620
+    }
621
+
622
+    public function setChecksum(string $checksum) {
623
+        $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
624
+        $this->refreshInfo();
625
+    }
626
+
627
+    protected function header($string) {
628
+        if (!\OC::$CLI) {
629
+            \header($string);
630
+        }
631
+    }
632
+
633
+    public function hash(string $type) {
634
+        return $this->fileView->hash($type, $this->path);
635
+    }
636
+
637
+    public function getNode(): \OCP\Files\File {
638
+        return $this->node;
639
+    }
640 640
 }
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/FilesPlugin.php 1 patch
Indentation   +711 added lines, -711 removed lines patch added patch discarded remove patch
@@ -40,715 +40,715 @@
 block discarded – undo
40 40
 use Sabre\HTTP\ResponseInterface;
41 41
 
42 42
 class FilesPlugin extends ServerPlugin {
43
-	// namespace
44
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
45
-	public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
46
-	public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
47
-	public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
48
-	public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
49
-	public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
50
-	public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
51
-	public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
52
-	public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
53
-	public const DOWNLOADURL_EXPIRATION_PROPERTYNAME = '{http://nextcloud.org/ns}download-url-expiration';
54
-	public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
55
-	public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
56
-	public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
57
-	public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
58
-	public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
59
-	public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
60
-	public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
61
-	public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
62
-	public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
63
-	public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
64
-	public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
65
-	public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
66
-	public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
67
-	public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
68
-	public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
69
-	public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
70
-	public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
71
-	public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
72
-	public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
73
-	public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
74
-	public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
75
-	public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
76
-
77
-	/** Reference to main server object */
78
-	private ?Server $server = null;
79
-
80
-	/**
81
-	 * @param Tree $tree
82
-	 * @param IConfig $config
83
-	 * @param IRequest $request
84
-	 * @param IPreview $previewManager
85
-	 * @param IUserSession $userSession
86
-	 * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
87
-	 * @param bool $downloadAttachment
88
-	 * @return void
89
-	 */
90
-	public function __construct(
91
-		private Tree $tree,
92
-		private IConfig $config,
93
-		private IRequest $request,
94
-		private IPreview $previewManager,
95
-		private IUserSession $userSession,
96
-		private IFilenameValidator $validator,
97
-		private IAccountManager $accountManager,
98
-		private bool $isPublic = false,
99
-		private bool $downloadAttachment = true,
100
-	) {
101
-	}
102
-
103
-	/**
104
-	 * This initializes the plugin.
105
-	 *
106
-	 * This function is called by \Sabre\DAV\Server, after
107
-	 * addPlugin is called.
108
-	 *
109
-	 * This method should set up the required event subscriptions.
110
-	 *
111
-	 * @return void
112
-	 */
113
-	public function initialize(Server $server) {
114
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
115
-		$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
116
-		$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
117
-		$server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
118
-		$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
119
-		$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
120
-		$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
121
-		$server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
122
-		$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
123
-		$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
124
-		$server->protectedProperties[] = self::DOWNLOADURL_EXPIRATION_PROPERTYNAME;
125
-		$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
126
-		$server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
127
-		$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
128
-		$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
129
-		$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
130
-		$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
131
-		$server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
132
-		$server->protectedProperties[] = self::SHARE_NOTE;
133
-
134
-		// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
135
-		$allowedProperties = ['{DAV:}getetag'];
136
-		$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
137
-
138
-		$this->server = $server;
139
-		$this->server->on('propFind', [$this, 'handleGetProperties']);
140
-		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
141
-		$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
142
-		$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
143
-		$this->server->on('afterMethod:GET', [$this,'httpGet']);
144
-		$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
145
-		$this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
146
-			$body = $response->getBody();
147
-			if (is_resource($body)) {
148
-				fclose($body);
149
-			}
150
-		});
151
-		$this->server->on('beforeMove', [$this, 'checkMove']);
152
-		$this->server->on('beforeCopy', [$this, 'checkCopy']);
153
-	}
154
-
155
-	/**
156
-	 * Plugin that checks if a copy can actually be performed.
157
-	 *
158
-	 * @param string $source source path
159
-	 * @param string $target target path
160
-	 * @throws NotFound If the source does not exist
161
-	 * @throws InvalidPath If the target is invalid
162
-	 */
163
-	public function checkCopy($source, $target): void {
164
-		$sourceNode = $this->tree->getNodeForPath($source);
165
-		if (!$sourceNode instanceof Node) {
166
-			return;
167
-		}
168
-
169
-		// Ensure source exists
170
-		$sourceNodeFileInfo = $sourceNode->getFileInfo();
171
-		if ($sourceNodeFileInfo === null) {
172
-			throw new NotFound($source . ' does not exist');
173
-		}
174
-		// Ensure the target name is valid
175
-		try {
176
-			[$targetPath, $targetName] = \Sabre\Uri\split($target);
177
-			$this->validator->validateFilename($targetName);
178
-		} catch (InvalidPathException $e) {
179
-			throw new InvalidPath($e->getMessage(), false);
180
-		}
181
-		// Ensure the target path is valid
182
-		$segments = array_slice(explode('/', $targetPath), 2);
183
-		foreach ($segments as $segment) {
184
-			if ($this->validator->isFilenameValid($segment) === false) {
185
-				$l = \OCP\Server::get(IFactory::class)->get('dav');
186
-				throw new InvalidPath($l->t('Invalid target path'));
187
-			}
188
-		}
189
-	}
190
-
191
-	/**
192
-	 * Plugin that checks if a move can actually be performed.
193
-	 *
194
-	 * @param string $source source path
195
-	 * @param string $target target path
196
-	 * @throws Forbidden If the source is not deletable
197
-	 * @throws NotFound If the source does not exist
198
-	 * @throws InvalidPath If the target name is invalid
199
-	 */
200
-	public function checkMove(string $source, string $target): void {
201
-		$sourceNode = $this->tree->getNodeForPath($source);
202
-		if (!$sourceNode instanceof Node) {
203
-			return;
204
-		}
205
-
206
-		// First check copyable (move only needs additional delete permission)
207
-		$this->checkCopy($source, $target);
208
-
209
-		// The source needs to be deletable for moving
210
-		$sourceNodeFileInfo = $sourceNode->getFileInfo();
211
-		if (!$sourceNodeFileInfo->isDeletable()) {
212
-			throw new Forbidden($source . ' cannot be deleted');
213
-		}
214
-
215
-		// The source is not allowed to be the parent of the target
216
-		if (str_starts_with($source, $target . '/')) {
217
-			throw new Forbidden($source . ' cannot be moved to it\'s parent');
218
-		}
219
-	}
220
-
221
-	/**
222
-	 * This sets a cookie to be able to recognize the start of the download
223
-	 * the content must not be longer than 32 characters and must only contain
224
-	 * alphanumeric characters
225
-	 *
226
-	 * @param RequestInterface $request
227
-	 * @param ResponseInterface $response
228
-	 */
229
-	public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
230
-		$queryParams = $request->getQueryParameters();
231
-
232
-		/**
233
-		 * this sets a cookie to be able to recognize the start of the download
234
-		 * the content must not be longer than 32 characters and must only contain
235
-		 * alphanumeric characters
236
-		 */
237
-		if (isset($queryParams['downloadStartSecret'])) {
238
-			$token = $queryParams['downloadStartSecret'];
239
-			if (!isset($token[32])
240
-				&& preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
241
-				// FIXME: use $response->setHeader() instead
242
-				setcookie('ocDownloadStarted', $token, time() + 20, '/');
243
-			}
244
-		}
245
-	}
246
-
247
-	/**
248
-	 * Add headers to file download
249
-	 *
250
-	 * @param RequestInterface $request
251
-	 * @param ResponseInterface $response
252
-	 */
253
-	public function httpGet(RequestInterface $request, ResponseInterface $response) {
254
-		// Only handle valid files
255
-		$node = $this->tree->getNodeForPath($request->getPath());
256
-		if (!($node instanceof IFile)) {
257
-			return;
258
-		}
259
-
260
-		// adds a 'Content-Disposition: attachment' header in case no disposition
261
-		// header has been set before
262
-		if ($this->downloadAttachment
263
-			&& $response->getHeader('Content-Disposition') === null) {
264
-			$filename = $node->getName();
265
-			if ($this->request->isUserAgent(
266
-				[
267
-					Request::USER_AGENT_IE,
268
-					Request::USER_AGENT_ANDROID_MOBILE_CHROME,
269
-					Request::USER_AGENT_FREEBOX,
270
-				])) {
271
-				$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
272
-			} else {
273
-				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
274
-													 . '; filename="' . rawurlencode($filename) . '"');
275
-			}
276
-		}
277
-
278
-		if ($node instanceof File) {
279
-			//Add OC-Checksum header
280
-			$checksum = $node->getChecksum();
281
-			if ($checksum !== null && $checksum !== '') {
282
-				$response->addHeader('OC-Checksum', $checksum);
283
-			}
284
-		}
285
-		$response->addHeader('X-Accel-Buffering', 'no');
286
-	}
287
-
288
-	/**
289
-	 * Adds all ownCloud-specific properties
290
-	 *
291
-	 * @param PropFind $propFind
292
-	 * @param \Sabre\DAV\INode $node
293
-	 * @return void
294
-	 */
295
-	public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
296
-		$httpRequest = $this->server->httpRequest;
297
-
298
-		if ($node instanceof Node) {
299
-			/**
300
-			 * This was disabled, because it made dir listing throw an exception,
301
-			 * so users were unable to navigate into folders where one subitem
302
-			 * is blocked by the files_accesscontrol app, see:
303
-			 * https://github.com/nextcloud/files_accesscontrol/issues/65
304
-			 * if (!$node->getFileInfo()->isReadable()) {
305
-			 *     // avoid detecting files through this means
306
-			 *     throw new NotFound();
307
-			 * }
308
-			 */
309
-
310
-			$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
311
-				return $node->getFileId();
312
-			});
313
-
314
-			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
315
-				return $node->getInternalFileId();
316
-			});
317
-
318
-			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
319
-				$perms = $node->getDavPermissions();
320
-				if ($this->isPublic) {
321
-					// remove mount information
322
-					$perms = str_replace(['S', 'M'], '', $perms);
323
-				}
324
-				return $perms;
325
-			});
326
-
327
-			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
328
-				$user = $this->userSession->getUser();
329
-				if ($user === null) {
330
-					return null;
331
-				}
332
-				return $node->getSharePermissions(
333
-					$user->getUID()
334
-				);
335
-			});
336
-
337
-			$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
338
-				$user = $this->userSession->getUser();
339
-				if ($user === null) {
340
-					return null;
341
-				}
342
-				$ncPermissions = $node->getSharePermissions(
343
-					$user->getUID()
344
-				);
345
-				$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
346
-				return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
347
-			});
348
-
349
-			$propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
350
-				return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
351
-			});
352
-
353
-			$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
354
-				return $node->getETag();
355
-			});
356
-
357
-			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
358
-				$owner = $node->getOwner();
359
-				if (!$owner) {
360
-					return null;
361
-				} else {
362
-					return $owner->getUID();
363
-				}
364
-			});
365
-			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
366
-				$owner = $node->getOwner();
367
-				if (!$owner) {
368
-					return null;
369
-				}
370
-
371
-				// Get current user to see if we're in a public share or not
372
-				$user = $this->userSession->getUser();
373
-
374
-				// If the user is logged in, we can return the display name
375
-				if ($user !== null) {
376
-					return $owner->getDisplayName();
377
-				}
378
-
379
-				// Check if the user published their display name
380
-				try {
381
-					$ownerAccount = $this->accountManager->getAccount($owner);
382
-				} catch (NoUserException) {
383
-					// do not lock process if owner is not local
384
-					return null;
385
-				}
386
-
387
-				$ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
388
-
389
-				// Since we are not logged in, we need to have at least the published scope
390
-				if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
391
-					return $owner->getDisplayName();
392
-				}
393
-
394
-				return null;
395
-			});
396
-
397
-			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
398
-				return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
399
-			});
400
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
401
-				return $node->getSize();
402
-			});
403
-			$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
404
-				return $node->getFileInfo()->getMountPoint()->getMountType();
405
-			});
406
-
407
-			/**
408
-			 * This is a special property which is used to determine if a node
409
-			 * is a mount root or not, e.g. a shared folder.
410
-			 * If so, then the node can only be unshared and not deleted.
411
-			 * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
412
-			 */
413
-			$propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
414
-				return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
415
-			});
416
-
417
-			$propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
418
-				$user = $this->userSession->getUser();
419
-				return $node->getNoteFromShare(
420
-					$user?->getUID()
421
-				);
422
-			});
423
-
424
-			$propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
425
-				$storage = $node->getNode()->getStorage();
426
-				if ($storage->instanceOfStorage(ISharedStorage::class)) {
427
-					/** @var ISharedStorage $storage */
428
-					return match($storage->getShare()->getHideDownload()) {
429
-						true => 'true',
430
-						false => 'false',
431
-					};
432
-				} else {
433
-					return null;
434
-				}
435
-			});
436
-
437
-			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
438
-				return $this->config->getSystemValue('data-fingerprint', '');
439
-			});
440
-			$propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
441
-				return (new \DateTimeImmutable())
442
-					->setTimestamp($node->getFileInfo()->getCreationTime())
443
-					->format(\DateTimeInterface::ATOM);
444
-			});
445
-			$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
446
-				return $node->getFileInfo()->getCreationTime();
447
-			});
448
-
449
-			foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
450
-				$propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
451
-			}
452
-
453
-			$propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
454
-				$isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
455
-				$isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
456
-				return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
457
-			});
458
-
459
-			/**
460
-			 * Return file/folder name as displayname. The primary reason to
461
-			 * implement it this way is to avoid costly fallback to
462
-			 * CustomPropertiesBackend (esp. visible when querying all files
463
-			 * in a folder).
464
-			 */
465
-			$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
466
-				return $node->getName();
467
-			});
468
-
469
-			$propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
470
-				return $node->getFileInfo()->getMountPoint()
471
-					instanceof SharingExternalMount;
472
-			});
473
-		}
474
-
475
-		if ($node instanceof File) {
476
-			$requestProperties = $propFind->getRequestedProperties();
477
-
478
-			if (in_array(self::DOWNLOADURL_PROPERTYNAME, $requestProperties, true)
479
-				|| in_array(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, $requestProperties, true)) {
480
-				try {
481
-					$directDownloadUrl = $node->getDirectDownload();
482
-				} catch (StorageNotAvailableException|ForbiddenException) {
483
-					$directDownloadUrl = null;
484
-				}
485
-
486
-				$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node, $directDownloadUrl) {
487
-					if ($directDownloadUrl && isset($directDownloadUrl['url'])) {
488
-						return $directDownloadUrl['url'];
489
-					}
490
-					return false;
491
-				});
492
-
493
-				$propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, function () use ($node, $directDownloadUrl) {
494
-					if ($directDownloadUrl && isset($directDownloadUrl['expiration'])) {
495
-						return $directDownloadUrl['expiration'];
496
-					}
497
-					return false;
498
-				});
499
-			}
500
-
501
-			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
502
-				$checksum = $node->getChecksum();
503
-				if ($checksum === null || $checksum === '') {
504
-					return null;
505
-				}
506
-
507
-				return new ChecksumList($checksum);
508
-			});
509
-
510
-			$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
511
-				return $node->getFileInfo()->getUploadTime();
512
-			});
513
-		}
514
-
515
-		if ($node instanceof Directory) {
516
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
517
-				return $node->getSize();
518
-			});
519
-
520
-			$requestProperties = $propFind->getRequestedProperties();
521
-
522
-			if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
523
-				|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
524
-				$nbFiles = 0;
525
-				$nbFolders = 0;
526
-				foreach ($node->getChildren() as $child) {
527
-					if ($child instanceof File) {
528
-						$nbFiles++;
529
-					} elseif ($child instanceof Directory) {
530
-						$nbFolders++;
531
-					}
532
-				}
533
-
534
-				$propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
535
-				$propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
536
-			}
537
-		}
538
-	}
539
-
540
-	/**
541
-	 * translate Nextcloud permissions to OCM Permissions
542
-	 *
543
-	 * @param $ncPermissions
544
-	 * @return array
545
-	 */
546
-	protected function ncPermissions2ocmPermissions($ncPermissions) {
547
-		$ocmPermissions = [];
548
-
549
-		if ($ncPermissions & Constants::PERMISSION_SHARE) {
550
-			$ocmPermissions[] = 'share';
551
-		}
552
-
553
-		if ($ncPermissions & Constants::PERMISSION_READ) {
554
-			$ocmPermissions[] = 'read';
555
-		}
556
-
557
-		if (($ncPermissions & Constants::PERMISSION_CREATE)
558
-			|| ($ncPermissions & Constants::PERMISSION_UPDATE)) {
559
-			$ocmPermissions[] = 'write';
560
-		}
561
-
562
-		return $ocmPermissions;
563
-	}
564
-
565
-	/**
566
-	 * Update ownCloud-specific properties
567
-	 *
568
-	 * @param string $path
569
-	 * @param PropPatch $propPatch
570
-	 *
571
-	 * @return void
572
-	 */
573
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
574
-		$node = $this->tree->getNodeForPath($path);
575
-		if (!($node instanceof Node)) {
576
-			return;
577
-		}
578
-
579
-		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
580
-			if (empty($time)) {
581
-				return false;
582
-			}
583
-			$node->touch($time);
584
-			return true;
585
-		});
586
-		$propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
587
-			if (empty($etag)) {
588
-				return false;
589
-			}
590
-			return $node->setEtag($etag) !== -1;
591
-		});
592
-		$propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
593
-			if (empty($time)) {
594
-				return false;
595
-			}
596
-			$dateTime = new \DateTimeImmutable($time);
597
-			$node->setCreationTime($dateTime->getTimestamp());
598
-			return true;
599
-		});
600
-		$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
601
-			if (empty($time)) {
602
-				return false;
603
-			}
604
-			$node->setCreationTime((int)$time);
605
-			return true;
606
-		});
607
-
608
-		$this->handleUpdatePropertiesMetadata($propPatch, $node);
609
-
610
-		/**
611
-		 * Disable modification of the displayname property for files and
612
-		 * folders via PROPPATCH. See PROPFIND for more information.
613
-		 */
614
-		$propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
615
-			return 403;
616
-		});
617
-	}
618
-
619
-
620
-	/**
621
-	 * handle the update of metadata from PROPPATCH requests
622
-	 *
623
-	 * @param PropPatch $propPatch
624
-	 * @param Node $node
625
-	 *
626
-	 * @throws FilesMetadataException
627
-	 */
628
-	private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
629
-		$userId = $this->userSession->getUser()?->getUID();
630
-		if ($userId === null) {
631
-			return;
632
-		}
633
-
634
-		$accessRight = $this->getMetadataFileAccessRight($node, $userId);
635
-		$filesMetadataManager = $this->initFilesMetadataManager();
636
-		$knownMetadata = $filesMetadataManager->getKnownMetadata();
637
-
638
-		foreach ($propPatch->getRemainingMutations() as $mutation) {
639
-			if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
640
-				continue;
641
-			}
642
-
643
-			$propPatch->handle(
644
-				$mutation,
645
-				function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
646
-					/** @var FilesMetadata $metadata */
647
-					$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
648
-					$metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
649
-					$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
650
-
651
-					// confirm metadata key is editable via PROPPATCH
652
-					if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
653
-						throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
654
-					}
655
-
656
-					if ($value === null) {
657
-						$metadata->unset($metadataKey);
658
-						$filesMetadataManager->saveMetadata($metadata);
659
-						return true;
660
-					}
661
-
662
-					// If the metadata is unknown, it defaults to string.
663
-					try {
664
-						$type = $knownMetadata->getType($metadataKey);
665
-					} catch (FilesMetadataNotFoundException) {
666
-						$type = IMetadataValueWrapper::TYPE_STRING;
667
-					}
668
-
669
-					switch ($type) {
670
-						case IMetadataValueWrapper::TYPE_STRING:
671
-							$metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
672
-							break;
673
-						case IMetadataValueWrapper::TYPE_INT:
674
-							$metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
675
-							break;
676
-						case IMetadataValueWrapper::TYPE_FLOAT:
677
-							$metadata->setFloat($metadataKey, $value);
678
-							break;
679
-						case IMetadataValueWrapper::TYPE_BOOL:
680
-							$metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
681
-							break;
682
-						case IMetadataValueWrapper::TYPE_ARRAY:
683
-							$metadata->setArray($metadataKey, $value);
684
-							break;
685
-						case IMetadataValueWrapper::TYPE_STRING_LIST:
686
-							$metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
687
-							break;
688
-						case IMetadataValueWrapper::TYPE_INT_LIST:
689
-							$metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
690
-							break;
691
-					}
692
-
693
-					$filesMetadataManager->saveMetadata($metadata);
694
-
695
-					return true;
696
-				}
697
-			);
698
-		}
699
-	}
700
-
701
-	/**
702
-	 * init default internal metadata
703
-	 *
704
-	 * @return IFilesMetadataManager
705
-	 */
706
-	private function initFilesMetadataManager(): IFilesMetadataManager {
707
-		/** @var IFilesMetadataManager $manager */
708
-		$manager = \OCP\Server::get(IFilesMetadataManager::class);
709
-		$manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION);
710
-
711
-		return $manager;
712
-	}
713
-
714
-	/**
715
-	 * based on owner and shares, returns the bottom limit to update related metadata
716
-	 *
717
-	 * @param Node $node
718
-	 * @param string $userId
719
-	 *
720
-	 * @return int
721
-	 */
722
-	private function getMetadataFileAccessRight(Node $node, string $userId): int {
723
-		if ($node->getOwner()?->getUID() === $userId) {
724
-			return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
725
-		} else {
726
-			$filePermissions = $node->getSharePermissions($userId);
727
-			if ($filePermissions & Constants::PERMISSION_UPDATE) {
728
-				return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
729
-			}
730
-		}
731
-
732
-		return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
733
-	}
734
-
735
-	/**
736
-	 * @param string $filePath
737
-	 * @param ?\Sabre\DAV\INode $node
738
-	 * @return void
739
-	 * @throws \Sabre\DAV\Exception\BadRequest
740
-	 */
741
-	public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
742
-		// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
743
-		try {
744
-			$node = $this->server->tree->getNodeForPath($filePath);
745
-			if ($node instanceof Node) {
746
-				$fileId = $node->getFileId();
747
-				if (!is_null($fileId)) {
748
-					$this->server->httpResponse->setHeader('OC-FileId', $fileId);
749
-				}
750
-			}
751
-		} catch (NotFound) {
752
-		}
753
-	}
43
+    // namespace
44
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
45
+    public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
46
+    public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
47
+    public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
48
+    public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
49
+    public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
50
+    public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
51
+    public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
52
+    public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
53
+    public const DOWNLOADURL_EXPIRATION_PROPERTYNAME = '{http://nextcloud.org/ns}download-url-expiration';
54
+    public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
55
+    public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
56
+    public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
57
+    public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
58
+    public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
59
+    public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
60
+    public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
61
+    public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
62
+    public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
63
+    public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
64
+    public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
65
+    public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
66
+    public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
67
+    public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
68
+    public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
69
+    public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
70
+    public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
71
+    public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
72
+    public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
73
+    public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
74
+    public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
75
+    public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
76
+
77
+    /** Reference to main server object */
78
+    private ?Server $server = null;
79
+
80
+    /**
81
+     * @param Tree $tree
82
+     * @param IConfig $config
83
+     * @param IRequest $request
84
+     * @param IPreview $previewManager
85
+     * @param IUserSession $userSession
86
+     * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
87
+     * @param bool $downloadAttachment
88
+     * @return void
89
+     */
90
+    public function __construct(
91
+        private Tree $tree,
92
+        private IConfig $config,
93
+        private IRequest $request,
94
+        private IPreview $previewManager,
95
+        private IUserSession $userSession,
96
+        private IFilenameValidator $validator,
97
+        private IAccountManager $accountManager,
98
+        private bool $isPublic = false,
99
+        private bool $downloadAttachment = true,
100
+    ) {
101
+    }
102
+
103
+    /**
104
+     * This initializes the plugin.
105
+     *
106
+     * This function is called by \Sabre\DAV\Server, after
107
+     * addPlugin is called.
108
+     *
109
+     * This method should set up the required event subscriptions.
110
+     *
111
+     * @return void
112
+     */
113
+    public function initialize(Server $server) {
114
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
115
+        $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
116
+        $server->protectedProperties[] = self::FILEID_PROPERTYNAME;
117
+        $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
118
+        $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
119
+        $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
120
+        $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
121
+        $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
122
+        $server->protectedProperties[] = self::SIZE_PROPERTYNAME;
123
+        $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
124
+        $server->protectedProperties[] = self::DOWNLOADURL_EXPIRATION_PROPERTYNAME;
125
+        $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
126
+        $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
127
+        $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
128
+        $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
129
+        $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
130
+        $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
131
+        $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
132
+        $server->protectedProperties[] = self::SHARE_NOTE;
133
+
134
+        // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
135
+        $allowedProperties = ['{DAV:}getetag'];
136
+        $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
137
+
138
+        $this->server = $server;
139
+        $this->server->on('propFind', [$this, 'handleGetProperties']);
140
+        $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
141
+        $this->server->on('afterBind', [$this, 'sendFileIdHeader']);
142
+        $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
143
+        $this->server->on('afterMethod:GET', [$this,'httpGet']);
144
+        $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
145
+        $this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
146
+            $body = $response->getBody();
147
+            if (is_resource($body)) {
148
+                fclose($body);
149
+            }
150
+        });
151
+        $this->server->on('beforeMove', [$this, 'checkMove']);
152
+        $this->server->on('beforeCopy', [$this, 'checkCopy']);
153
+    }
154
+
155
+    /**
156
+     * Plugin that checks if a copy can actually be performed.
157
+     *
158
+     * @param string $source source path
159
+     * @param string $target target path
160
+     * @throws NotFound If the source does not exist
161
+     * @throws InvalidPath If the target is invalid
162
+     */
163
+    public function checkCopy($source, $target): void {
164
+        $sourceNode = $this->tree->getNodeForPath($source);
165
+        if (!$sourceNode instanceof Node) {
166
+            return;
167
+        }
168
+
169
+        // Ensure source exists
170
+        $sourceNodeFileInfo = $sourceNode->getFileInfo();
171
+        if ($sourceNodeFileInfo === null) {
172
+            throw new NotFound($source . ' does not exist');
173
+        }
174
+        // Ensure the target name is valid
175
+        try {
176
+            [$targetPath, $targetName] = \Sabre\Uri\split($target);
177
+            $this->validator->validateFilename($targetName);
178
+        } catch (InvalidPathException $e) {
179
+            throw new InvalidPath($e->getMessage(), false);
180
+        }
181
+        // Ensure the target path is valid
182
+        $segments = array_slice(explode('/', $targetPath), 2);
183
+        foreach ($segments as $segment) {
184
+            if ($this->validator->isFilenameValid($segment) === false) {
185
+                $l = \OCP\Server::get(IFactory::class)->get('dav');
186
+                throw new InvalidPath($l->t('Invalid target path'));
187
+            }
188
+        }
189
+    }
190
+
191
+    /**
192
+     * Plugin that checks if a move can actually be performed.
193
+     *
194
+     * @param string $source source path
195
+     * @param string $target target path
196
+     * @throws Forbidden If the source is not deletable
197
+     * @throws NotFound If the source does not exist
198
+     * @throws InvalidPath If the target name is invalid
199
+     */
200
+    public function checkMove(string $source, string $target): void {
201
+        $sourceNode = $this->tree->getNodeForPath($source);
202
+        if (!$sourceNode instanceof Node) {
203
+            return;
204
+        }
205
+
206
+        // First check copyable (move only needs additional delete permission)
207
+        $this->checkCopy($source, $target);
208
+
209
+        // The source needs to be deletable for moving
210
+        $sourceNodeFileInfo = $sourceNode->getFileInfo();
211
+        if (!$sourceNodeFileInfo->isDeletable()) {
212
+            throw new Forbidden($source . ' cannot be deleted');
213
+        }
214
+
215
+        // The source is not allowed to be the parent of the target
216
+        if (str_starts_with($source, $target . '/')) {
217
+            throw new Forbidden($source . ' cannot be moved to it\'s parent');
218
+        }
219
+    }
220
+
221
+    /**
222
+     * This sets a cookie to be able to recognize the start of the download
223
+     * the content must not be longer than 32 characters and must only contain
224
+     * alphanumeric characters
225
+     *
226
+     * @param RequestInterface $request
227
+     * @param ResponseInterface $response
228
+     */
229
+    public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
230
+        $queryParams = $request->getQueryParameters();
231
+
232
+        /**
233
+         * this sets a cookie to be able to recognize the start of the download
234
+         * the content must not be longer than 32 characters and must only contain
235
+         * alphanumeric characters
236
+         */
237
+        if (isset($queryParams['downloadStartSecret'])) {
238
+            $token = $queryParams['downloadStartSecret'];
239
+            if (!isset($token[32])
240
+                && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
241
+                // FIXME: use $response->setHeader() instead
242
+                setcookie('ocDownloadStarted', $token, time() + 20, '/');
243
+            }
244
+        }
245
+    }
246
+
247
+    /**
248
+     * Add headers to file download
249
+     *
250
+     * @param RequestInterface $request
251
+     * @param ResponseInterface $response
252
+     */
253
+    public function httpGet(RequestInterface $request, ResponseInterface $response) {
254
+        // Only handle valid files
255
+        $node = $this->tree->getNodeForPath($request->getPath());
256
+        if (!($node instanceof IFile)) {
257
+            return;
258
+        }
259
+
260
+        // adds a 'Content-Disposition: attachment' header in case no disposition
261
+        // header has been set before
262
+        if ($this->downloadAttachment
263
+            && $response->getHeader('Content-Disposition') === null) {
264
+            $filename = $node->getName();
265
+            if ($this->request->isUserAgent(
266
+                [
267
+                    Request::USER_AGENT_IE,
268
+                    Request::USER_AGENT_ANDROID_MOBILE_CHROME,
269
+                    Request::USER_AGENT_FREEBOX,
270
+                ])) {
271
+                $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
272
+            } else {
273
+                $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
274
+                                                        . '; filename="' . rawurlencode($filename) . '"');
275
+            }
276
+        }
277
+
278
+        if ($node instanceof File) {
279
+            //Add OC-Checksum header
280
+            $checksum = $node->getChecksum();
281
+            if ($checksum !== null && $checksum !== '') {
282
+                $response->addHeader('OC-Checksum', $checksum);
283
+            }
284
+        }
285
+        $response->addHeader('X-Accel-Buffering', 'no');
286
+    }
287
+
288
+    /**
289
+     * Adds all ownCloud-specific properties
290
+     *
291
+     * @param PropFind $propFind
292
+     * @param \Sabre\DAV\INode $node
293
+     * @return void
294
+     */
295
+    public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
296
+        $httpRequest = $this->server->httpRequest;
297
+
298
+        if ($node instanceof Node) {
299
+            /**
300
+             * This was disabled, because it made dir listing throw an exception,
301
+             * so users were unable to navigate into folders where one subitem
302
+             * is blocked by the files_accesscontrol app, see:
303
+             * https://github.com/nextcloud/files_accesscontrol/issues/65
304
+             * if (!$node->getFileInfo()->isReadable()) {
305
+             *     // avoid detecting files through this means
306
+             *     throw new NotFound();
307
+             * }
308
+             */
309
+
310
+            $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
311
+                return $node->getFileId();
312
+            });
313
+
314
+            $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
315
+                return $node->getInternalFileId();
316
+            });
317
+
318
+            $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
319
+                $perms = $node->getDavPermissions();
320
+                if ($this->isPublic) {
321
+                    // remove mount information
322
+                    $perms = str_replace(['S', 'M'], '', $perms);
323
+                }
324
+                return $perms;
325
+            });
326
+
327
+            $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
328
+                $user = $this->userSession->getUser();
329
+                if ($user === null) {
330
+                    return null;
331
+                }
332
+                return $node->getSharePermissions(
333
+                    $user->getUID()
334
+                );
335
+            });
336
+
337
+            $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
338
+                $user = $this->userSession->getUser();
339
+                if ($user === null) {
340
+                    return null;
341
+                }
342
+                $ncPermissions = $node->getSharePermissions(
343
+                    $user->getUID()
344
+                );
345
+                $ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
346
+                return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
347
+            });
348
+
349
+            $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
350
+                return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
351
+            });
352
+
353
+            $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
354
+                return $node->getETag();
355
+            });
356
+
357
+            $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
358
+                $owner = $node->getOwner();
359
+                if (!$owner) {
360
+                    return null;
361
+                } else {
362
+                    return $owner->getUID();
363
+                }
364
+            });
365
+            $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
366
+                $owner = $node->getOwner();
367
+                if (!$owner) {
368
+                    return null;
369
+                }
370
+
371
+                // Get current user to see if we're in a public share or not
372
+                $user = $this->userSession->getUser();
373
+
374
+                // If the user is logged in, we can return the display name
375
+                if ($user !== null) {
376
+                    return $owner->getDisplayName();
377
+                }
378
+
379
+                // Check if the user published their display name
380
+                try {
381
+                    $ownerAccount = $this->accountManager->getAccount($owner);
382
+                } catch (NoUserException) {
383
+                    // do not lock process if owner is not local
384
+                    return null;
385
+                }
386
+
387
+                $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
388
+
389
+                // Since we are not logged in, we need to have at least the published scope
390
+                if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
391
+                    return $owner->getDisplayName();
392
+                }
393
+
394
+                return null;
395
+            });
396
+
397
+            $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
398
+                return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
399
+            });
400
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
401
+                return $node->getSize();
402
+            });
403
+            $propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
404
+                return $node->getFileInfo()->getMountPoint()->getMountType();
405
+            });
406
+
407
+            /**
408
+             * This is a special property which is used to determine if a node
409
+             * is a mount root or not, e.g. a shared folder.
410
+             * If so, then the node can only be unshared and not deleted.
411
+             * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
412
+             */
413
+            $propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
414
+                return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
415
+            });
416
+
417
+            $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
418
+                $user = $this->userSession->getUser();
419
+                return $node->getNoteFromShare(
420
+                    $user?->getUID()
421
+                );
422
+            });
423
+
424
+            $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
425
+                $storage = $node->getNode()->getStorage();
426
+                if ($storage->instanceOfStorage(ISharedStorage::class)) {
427
+                    /** @var ISharedStorage $storage */
428
+                    return match($storage->getShare()->getHideDownload()) {
429
+                        true => 'true',
430
+                        false => 'false',
431
+                    };
432
+                } else {
433
+                    return null;
434
+                }
435
+            });
436
+
437
+            $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
438
+                return $this->config->getSystemValue('data-fingerprint', '');
439
+            });
440
+            $propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
441
+                return (new \DateTimeImmutable())
442
+                    ->setTimestamp($node->getFileInfo()->getCreationTime())
443
+                    ->format(\DateTimeInterface::ATOM);
444
+            });
445
+            $propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
446
+                return $node->getFileInfo()->getCreationTime();
447
+            });
448
+
449
+            foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
450
+                $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
451
+            }
452
+
453
+            $propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
454
+                $isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
455
+                $isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
456
+                return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
457
+            });
458
+
459
+            /**
460
+             * Return file/folder name as displayname. The primary reason to
461
+             * implement it this way is to avoid costly fallback to
462
+             * CustomPropertiesBackend (esp. visible when querying all files
463
+             * in a folder).
464
+             */
465
+            $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
466
+                return $node->getName();
467
+            });
468
+
469
+            $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
470
+                return $node->getFileInfo()->getMountPoint()
471
+                    instanceof SharingExternalMount;
472
+            });
473
+        }
474
+
475
+        if ($node instanceof File) {
476
+            $requestProperties = $propFind->getRequestedProperties();
477
+
478
+            if (in_array(self::DOWNLOADURL_PROPERTYNAME, $requestProperties, true)
479
+                || in_array(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, $requestProperties, true)) {
480
+                try {
481
+                    $directDownloadUrl = $node->getDirectDownload();
482
+                } catch (StorageNotAvailableException|ForbiddenException) {
483
+                    $directDownloadUrl = null;
484
+                }
485
+
486
+                $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node, $directDownloadUrl) {
487
+                    if ($directDownloadUrl && isset($directDownloadUrl['url'])) {
488
+                        return $directDownloadUrl['url'];
489
+                    }
490
+                    return false;
491
+                });
492
+
493
+                $propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, function () use ($node, $directDownloadUrl) {
494
+                    if ($directDownloadUrl && isset($directDownloadUrl['expiration'])) {
495
+                        return $directDownloadUrl['expiration'];
496
+                    }
497
+                    return false;
498
+                });
499
+            }
500
+
501
+            $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
502
+                $checksum = $node->getChecksum();
503
+                if ($checksum === null || $checksum === '') {
504
+                    return null;
505
+                }
506
+
507
+                return new ChecksumList($checksum);
508
+            });
509
+
510
+            $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
511
+                return $node->getFileInfo()->getUploadTime();
512
+            });
513
+        }
514
+
515
+        if ($node instanceof Directory) {
516
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
517
+                return $node->getSize();
518
+            });
519
+
520
+            $requestProperties = $propFind->getRequestedProperties();
521
+
522
+            if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
523
+                || in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
524
+                $nbFiles = 0;
525
+                $nbFolders = 0;
526
+                foreach ($node->getChildren() as $child) {
527
+                    if ($child instanceof File) {
528
+                        $nbFiles++;
529
+                    } elseif ($child instanceof Directory) {
530
+                        $nbFolders++;
531
+                    }
532
+                }
533
+
534
+                $propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
535
+                $propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
536
+            }
537
+        }
538
+    }
539
+
540
+    /**
541
+     * translate Nextcloud permissions to OCM Permissions
542
+     *
543
+     * @param $ncPermissions
544
+     * @return array
545
+     */
546
+    protected function ncPermissions2ocmPermissions($ncPermissions) {
547
+        $ocmPermissions = [];
548
+
549
+        if ($ncPermissions & Constants::PERMISSION_SHARE) {
550
+            $ocmPermissions[] = 'share';
551
+        }
552
+
553
+        if ($ncPermissions & Constants::PERMISSION_READ) {
554
+            $ocmPermissions[] = 'read';
555
+        }
556
+
557
+        if (($ncPermissions & Constants::PERMISSION_CREATE)
558
+            || ($ncPermissions & Constants::PERMISSION_UPDATE)) {
559
+            $ocmPermissions[] = 'write';
560
+        }
561
+
562
+        return $ocmPermissions;
563
+    }
564
+
565
+    /**
566
+     * Update ownCloud-specific properties
567
+     *
568
+     * @param string $path
569
+     * @param PropPatch $propPatch
570
+     *
571
+     * @return void
572
+     */
573
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
574
+        $node = $this->tree->getNodeForPath($path);
575
+        if (!($node instanceof Node)) {
576
+            return;
577
+        }
578
+
579
+        $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
580
+            if (empty($time)) {
581
+                return false;
582
+            }
583
+            $node->touch($time);
584
+            return true;
585
+        });
586
+        $propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
587
+            if (empty($etag)) {
588
+                return false;
589
+            }
590
+            return $node->setEtag($etag) !== -1;
591
+        });
592
+        $propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
593
+            if (empty($time)) {
594
+                return false;
595
+            }
596
+            $dateTime = new \DateTimeImmutable($time);
597
+            $node->setCreationTime($dateTime->getTimestamp());
598
+            return true;
599
+        });
600
+        $propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
601
+            if (empty($time)) {
602
+                return false;
603
+            }
604
+            $node->setCreationTime((int)$time);
605
+            return true;
606
+        });
607
+
608
+        $this->handleUpdatePropertiesMetadata($propPatch, $node);
609
+
610
+        /**
611
+         * Disable modification of the displayname property for files and
612
+         * folders via PROPPATCH. See PROPFIND for more information.
613
+         */
614
+        $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
615
+            return 403;
616
+        });
617
+    }
618
+
619
+
620
+    /**
621
+     * handle the update of metadata from PROPPATCH requests
622
+     *
623
+     * @param PropPatch $propPatch
624
+     * @param Node $node
625
+     *
626
+     * @throws FilesMetadataException
627
+     */
628
+    private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
629
+        $userId = $this->userSession->getUser()?->getUID();
630
+        if ($userId === null) {
631
+            return;
632
+        }
633
+
634
+        $accessRight = $this->getMetadataFileAccessRight($node, $userId);
635
+        $filesMetadataManager = $this->initFilesMetadataManager();
636
+        $knownMetadata = $filesMetadataManager->getKnownMetadata();
637
+
638
+        foreach ($propPatch->getRemainingMutations() as $mutation) {
639
+            if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
640
+                continue;
641
+            }
642
+
643
+            $propPatch->handle(
644
+                $mutation,
645
+                function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
646
+                    /** @var FilesMetadata $metadata */
647
+                    $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
648
+                    $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
649
+                    $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
650
+
651
+                    // confirm metadata key is editable via PROPPATCH
652
+                    if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
653
+                        throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
654
+                    }
655
+
656
+                    if ($value === null) {
657
+                        $metadata->unset($metadataKey);
658
+                        $filesMetadataManager->saveMetadata($metadata);
659
+                        return true;
660
+                    }
661
+
662
+                    // If the metadata is unknown, it defaults to string.
663
+                    try {
664
+                        $type = $knownMetadata->getType($metadataKey);
665
+                    } catch (FilesMetadataNotFoundException) {
666
+                        $type = IMetadataValueWrapper::TYPE_STRING;
667
+                    }
668
+
669
+                    switch ($type) {
670
+                        case IMetadataValueWrapper::TYPE_STRING:
671
+                            $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
672
+                            break;
673
+                        case IMetadataValueWrapper::TYPE_INT:
674
+                            $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
675
+                            break;
676
+                        case IMetadataValueWrapper::TYPE_FLOAT:
677
+                            $metadata->setFloat($metadataKey, $value);
678
+                            break;
679
+                        case IMetadataValueWrapper::TYPE_BOOL:
680
+                            $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
681
+                            break;
682
+                        case IMetadataValueWrapper::TYPE_ARRAY:
683
+                            $metadata->setArray($metadataKey, $value);
684
+                            break;
685
+                        case IMetadataValueWrapper::TYPE_STRING_LIST:
686
+                            $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
687
+                            break;
688
+                        case IMetadataValueWrapper::TYPE_INT_LIST:
689
+                            $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
690
+                            break;
691
+                    }
692
+
693
+                    $filesMetadataManager->saveMetadata($metadata);
694
+
695
+                    return true;
696
+                }
697
+            );
698
+        }
699
+    }
700
+
701
+    /**
702
+     * init default internal metadata
703
+     *
704
+     * @return IFilesMetadataManager
705
+     */
706
+    private function initFilesMetadataManager(): IFilesMetadataManager {
707
+        /** @var IFilesMetadataManager $manager */
708
+        $manager = \OCP\Server::get(IFilesMetadataManager::class);
709
+        $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION);
710
+
711
+        return $manager;
712
+    }
713
+
714
+    /**
715
+     * based on owner and shares, returns the bottom limit to update related metadata
716
+     *
717
+     * @param Node $node
718
+     * @param string $userId
719
+     *
720
+     * @return int
721
+     */
722
+    private function getMetadataFileAccessRight(Node $node, string $userId): int {
723
+        if ($node->getOwner()?->getUID() === $userId) {
724
+            return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
725
+        } else {
726
+            $filePermissions = $node->getSharePermissions($userId);
727
+            if ($filePermissions & Constants::PERMISSION_UPDATE) {
728
+                return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
729
+            }
730
+        }
731
+
732
+        return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
733
+    }
734
+
735
+    /**
736
+     * @param string $filePath
737
+     * @param ?\Sabre\DAV\INode $node
738
+     * @return void
739
+     * @throws \Sabre\DAV\Exception\BadRequest
740
+     */
741
+    public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
742
+        // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
743
+        try {
744
+            $node = $this->server->tree->getNodeForPath($filePath);
745
+            if ($node instanceof Node) {
746
+                $fileId = $node->getFileId();
747
+                if (!is_null($fileId)) {
748
+                    $this->server->httpResponse->setHeader('OC-FileId', $fileId);
749
+                }
750
+            }
751
+        } catch (NotFound) {
752
+        }
753
+    }
754 754
 }
Please login to merge, or discard this patch.
tests/lib/Files/ObjectStore/FailWriteObjectStore.php 1 patch
Indentation   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -11,37 +11,37 @@
 block discarded – undo
11 11
 use OCP\Files\ObjectStore\IObjectStore;
12 12
 
13 13
 class FailWriteObjectStore implements IObjectStore {
14
-	public function __construct(
15
-		private IObjectStore $objectStore,
16
-	) {
17
-	}
18
-
19
-	public function getStorageId() {
20
-		return $this->objectStore->getStorageId();
21
-	}
22
-
23
-	public function readObject($urn) {
24
-		return $this->objectStore->readObject($urn);
25
-	}
26
-
27
-	public function writeObject($urn, $stream, ?string $mimetype = null) {
28
-		// emulate a failed write that didn't throw an error
29
-		return true;
30
-	}
31
-
32
-	public function deleteObject($urn) {
33
-		$this->objectStore->deleteObject($urn);
34
-	}
35
-
36
-	public function objectExists($urn) {
37
-		return $this->objectStore->objectExists($urn);
38
-	}
39
-
40
-	public function copyObject($from, $to) {
41
-		$this->objectStore->copyObject($from, $to);
42
-	}
43
-
44
-	public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
45
-		return null;
46
-	}
14
+    public function __construct(
15
+        private IObjectStore $objectStore,
16
+    ) {
17
+    }
18
+
19
+    public function getStorageId() {
20
+        return $this->objectStore->getStorageId();
21
+    }
22
+
23
+    public function readObject($urn) {
24
+        return $this->objectStore->readObject($urn);
25
+    }
26
+
27
+    public function writeObject($urn, $stream, ?string $mimetype = null) {
28
+        // emulate a failed write that didn't throw an error
29
+        return true;
30
+    }
31
+
32
+    public function deleteObject($urn) {
33
+        $this->objectStore->deleteObject($urn);
34
+    }
35
+
36
+    public function objectExists($urn) {
37
+        return $this->objectStore->objectExists($urn);
38
+    }
39
+
40
+    public function copyObject($from, $to) {
41
+        $this->objectStore->copyObject($from, $to);
42
+    }
43
+
44
+    public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
45
+        return null;
46
+    }
47 47
 }
Please login to merge, or discard this patch.
tests/lib/Files/ObjectStore/FailDeleteObjectStore.php 1 patch
Indentation   +32 added lines, -32 removed lines patch added patch discarded remove patch
@@ -11,36 +11,36 @@
 block discarded – undo
11 11
 use OCP\Files\ObjectStore\IObjectStore;
12 12
 
13 13
 class FailDeleteObjectStore implements IObjectStore {
14
-	public function __construct(
15
-		private IObjectStore $objectStore,
16
-	) {
17
-	}
18
-
19
-	public function getStorageId() {
20
-		return $this->objectStore->getStorageId();
21
-	}
22
-
23
-	public function readObject($urn) {
24
-		return $this->objectStore->readObject($urn);
25
-	}
26
-
27
-	public function writeObject($urn, $stream, ?string $mimetype = null) {
28
-		return $this->objectStore->writeObject($urn, $stream, $mimetype);
29
-	}
30
-
31
-	public function deleteObject($urn) {
32
-		throw new \Exception();
33
-	}
34
-
35
-	public function objectExists($urn) {
36
-		return $this->objectStore->objectExists($urn);
37
-	}
38
-
39
-	public function copyObject($from, $to) {
40
-		$this->objectStore->copyObject($from, $to);
41
-	}
42
-
43
-	public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
44
-		return null;
45
-	}
14
+    public function __construct(
15
+        private IObjectStore $objectStore,
16
+    ) {
17
+    }
18
+
19
+    public function getStorageId() {
20
+        return $this->objectStore->getStorageId();
21
+    }
22
+
23
+    public function readObject($urn) {
24
+        return $this->objectStore->readObject($urn);
25
+    }
26
+
27
+    public function writeObject($urn, $stream, ?string $mimetype = null) {
28
+        return $this->objectStore->writeObject($urn, $stream, $mimetype);
29
+    }
30
+
31
+    public function deleteObject($urn) {
32
+        throw new \Exception();
33
+    }
34
+
35
+    public function objectExists($urn) {
36
+        return $this->objectStore->objectExists($urn);
37
+    }
38
+
39
+    public function copyObject($from, $to) {
40
+        $this->objectStore->copyObject($from, $to);
41
+    }
42
+
43
+    public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
44
+        return null;
45
+    }
46 46
 }
Please login to merge, or discard this patch.
tests/lib/Files/Mount/ObjectHomeMountProviderTest.php 1 patch
Indentation   +245 added lines, -245 removed lines patch added patch discarded remove patch
@@ -16,260 +16,260 @@
 block discarded – undo
16 16
 use OCP\IUser;
17 17
 
18 18
 class ObjectHomeMountProviderTest extends \Test\TestCase {
19
-	/** @var ObjectHomeMountProvider */
20
-	protected $provider;
21
-
22
-	/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
23
-	protected $config;
24
-
25
-	/** @var IUser|\PHPUnit\Framework\MockObject\MockObject */
26
-	protected $user;
27
-
28
-	/** @var IStorageFactory|\PHPUnit\Framework\MockObject\MockObject */
29
-	protected $loader;
30
-
31
-	protected function setUp(): void {
32
-		parent::setUp();
33
-
34
-		$this->config = $this->createMock(IConfig::class);
35
-		$this->user = $this->createMock(IUser::class);
36
-		$this->loader = $this->createMock(IStorageFactory::class);
37
-
38
-		$objectStoreConfig = new PrimaryObjectStoreConfig($this->config, $this->createMock(IAppManager::class));
39
-		$this->provider = new ObjectHomeMountProvider($objectStoreConfig);
40
-	}
41
-
42
-	public function testSingleBucket(): void {
43
-		$this->config->method('getSystemValue')
44
-			->willReturnCallback(function ($key, $default) {
45
-				if ($key === 'objectstore') {
46
-					return [
47
-						'class' => 'Test\Files\Mount\FakeObjectStore',
48
-						'arguments' => [
49
-							'foo' => 'bar'
50
-						],
51
-					];
52
-				} else {
53
-					return $default;
54
-				}
55
-			});
56
-
57
-		$mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
58
-		$arguments = $this->invokePrivate($mount, 'arguments');
59
-
60
-		$objectStore = $arguments['objectstore'];
61
-		$this->assertInstanceOf(FakeObjectStore::class, $objectStore);
62
-		$this->assertEquals(['foo' => 'bar', 'multibucket' => false], $objectStore->getArguments());
63
-	}
64
-
65
-	public function testMultiBucket(): void {
66
-		$this->config->method('getSystemValue')
67
-			->willReturnCallback(function ($key, $default) {
68
-				if ($key === 'objectstore_multibucket') {
69
-					return [
70
-						'class' => 'Test\Files\Mount\FakeObjectStore',
71
-						'arguments' => [
72
-							'foo' => 'bar'
73
-						],
74
-					];
75
-				} else {
76
-					return $default;
77
-				}
78
-			});
79
-
80
-		$this->user->method('getUID')
81
-			->willReturn('uid');
82
-		$this->loader->expects($this->never())->method($this->anything());
83
-
84
-		$this->config->method('getUserValue')
85
-			->willReturn(null);
86
-
87
-		$this->config
88
-			->method('setUserValue')
89
-			->with(
90
-				$this->equalTo('uid'),
91
-				$this->equalTo('homeobjectstore'),
92
-				$this->equalTo('bucket'),
93
-				$this->equalTo('49'),
94
-				$this->equalTo(null)
95
-			);
96
-
97
-		$mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
98
-		$arguments = $this->invokePrivate($mount, 'arguments');
99
-
100
-		$objectStore = $arguments['objectstore'];
101
-		$this->assertInstanceOf(FakeObjectStore::class, $objectStore);
102
-		$this->assertEquals(['foo' => 'bar', 'bucket' => 49, 'multibucket' => true], $objectStore->getArguments());
103
-	}
104
-
105
-	public function testMultiBucketWithPrefix(): void {
106
-		$this->config->method('getSystemValue')
107
-			->willReturnCallback(function ($key, $default) {
108
-				if ($key === 'objectstore_multibucket') {
109
-					return [
110
-						'class' => 'Test\Files\Mount\FakeObjectStore',
111
-						'arguments' => [
112
-							'foo' => 'bar',
113
-							'bucket' => 'myBucketPrefix',
114
-						],
115
-					];
116
-				} else {
117
-					return $default;
118
-				}
119
-			});
120
-
121
-		$this->user->method('getUID')
122
-			->willReturn('uid');
123
-		$this->loader->expects($this->never())->method($this->anything());
124
-
125
-		$this->config
126
-			->method('getUserValue')
127
-			->willReturn(null);
128
-
129
-		$this->config->expects($this->once())
130
-			->method('setUserValue')
131
-			->with(
132
-				$this->equalTo('uid'),
133
-				$this->equalTo('homeobjectstore'),
134
-				$this->equalTo('bucket'),
135
-				$this->equalTo('myBucketPrefix49'),
136
-				$this->equalTo(null)
137
-			);
138
-
139
-		$mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
140
-		$arguments = $this->invokePrivate($mount, 'arguments');
141
-
142
-		$objectStore = $arguments['objectstore'];
143
-		$this->assertInstanceOf(FakeObjectStore::class, $objectStore);
144
-		$this->assertEquals(['foo' => 'bar', 'bucket' => 'myBucketPrefix49', 'multibucket' => true], $objectStore->getArguments());
145
-	}
146
-
147
-	public function testMultiBucketBucketAlreadySet(): void {
148
-		$this->config->method('getSystemValue')
149
-			->willReturnCallback(function ($key, $default) {
150
-				if ($key === 'objectstore_multibucket') {
151
-					return [
152
-						'class' => 'Test\Files\Mount\FakeObjectStore',
153
-						'arguments' => [
154
-							'foo' => 'bar',
155
-							'bucket' => 'myBucketPrefix',
156
-						],
157
-					];
158
-				} else {
159
-					return $default;
160
-				}
161
-			});
162
-
163
-		$this->user->method('getUID')
164
-			->willReturn('uid');
165
-		$this->loader->expects($this->never())->method($this->anything());
166
-
167
-		$this->config
168
-			->method('getUserValue')
169
-			->willReturnCallback(function ($uid, $app, $key, $default) {
170
-				if ($uid === 'uid' && $app === 'homeobjectstore' && $key === 'bucket') {
171
-					return 'awesomeBucket1';
172
-				} else {
173
-					return $default;
174
-				}
175
-			});
176
-
177
-		$this->config->expects($this->never())
178
-			->method('setUserValue');
179
-
180
-		$mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
181
-		$arguments = $this->invokePrivate($mount, 'arguments');
182
-
183
-		$objectStore = $arguments['objectstore'];
184
-		$this->assertInstanceOf(FakeObjectStore::class, $objectStore);
185
-		$this->assertEquals(['foo' => 'bar', 'bucket' => 'awesomeBucket1', 'multibucket' => true], $objectStore->getArguments());
186
-	}
187
-
188
-	public function testMultiBucketConfigFirst(): void {
189
-		$this->config->method('getSystemValue')
190
-			->willReturnCallback(function ($key, $default) {
191
-				if ($key === 'objectstore_multibucket') {
192
-					return [
193
-						'class' => 'Test\Files\Mount\FakeObjectStore',
194
-						'arguments' => [
195
-							'foo' => 'bar',
196
-							'bucket' => 'myBucketPrefix',
197
-						],
198
-					];
199
-				} else {
200
-					return $default;
201
-				}
202
-			});
203
-
204
-		$this->user->method('getUID')
205
-			->willReturn('uid');
206
-		$this->loader->expects($this->never())->method($this->anything());
207
-
208
-		$mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
209
-		$this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount);
210
-	}
211
-
212
-	public function testMultiBucketConfigFirstFallBackSingle(): void {
213
-		$this->config
214
-			->method('getSystemValue')->willReturnMap([
215
-				['objectstore_multibucket', null, null],
216
-				['objectstore', null, [
217
-					'class' => 'Test\Files\Mount\FakeObjectStore',
218
-					'arguments' => [
219
-						'foo' => 'bar',
220
-						'bucket' => 'myBucketPrefix',
221
-					],
222
-				]],
223
-			]);
224
-
225
-		$this->user->method('getUID')
226
-			->willReturn('uid');
227
-		$this->loader->expects($this->never())->method($this->anything());
228
-
229
-		$mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
230
-		$this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount);
231
-	}
232
-
233
-	public function testNoObjectStore(): void {
234
-		$this->config->method('getSystemValue')
235
-			->willReturnCallback(function ($key, $default) {
236
-				return $default;
237
-			});
238
-
239
-		$mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
240
-		$this->assertNull($mount);
241
-	}
19
+    /** @var ObjectHomeMountProvider */
20
+    protected $provider;
21
+
22
+    /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
23
+    protected $config;
24
+
25
+    /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */
26
+    protected $user;
27
+
28
+    /** @var IStorageFactory|\PHPUnit\Framework\MockObject\MockObject */
29
+    protected $loader;
30
+
31
+    protected function setUp(): void {
32
+        parent::setUp();
33
+
34
+        $this->config = $this->createMock(IConfig::class);
35
+        $this->user = $this->createMock(IUser::class);
36
+        $this->loader = $this->createMock(IStorageFactory::class);
37
+
38
+        $objectStoreConfig = new PrimaryObjectStoreConfig($this->config, $this->createMock(IAppManager::class));
39
+        $this->provider = new ObjectHomeMountProvider($objectStoreConfig);
40
+    }
41
+
42
+    public function testSingleBucket(): void {
43
+        $this->config->method('getSystemValue')
44
+            ->willReturnCallback(function ($key, $default) {
45
+                if ($key === 'objectstore') {
46
+                    return [
47
+                        'class' => 'Test\Files\Mount\FakeObjectStore',
48
+                        'arguments' => [
49
+                            'foo' => 'bar'
50
+                        ],
51
+                    ];
52
+                } else {
53
+                    return $default;
54
+                }
55
+            });
56
+
57
+        $mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
58
+        $arguments = $this->invokePrivate($mount, 'arguments');
59
+
60
+        $objectStore = $arguments['objectstore'];
61
+        $this->assertInstanceOf(FakeObjectStore::class, $objectStore);
62
+        $this->assertEquals(['foo' => 'bar', 'multibucket' => false], $objectStore->getArguments());
63
+    }
64
+
65
+    public function testMultiBucket(): void {
66
+        $this->config->method('getSystemValue')
67
+            ->willReturnCallback(function ($key, $default) {
68
+                if ($key === 'objectstore_multibucket') {
69
+                    return [
70
+                        'class' => 'Test\Files\Mount\FakeObjectStore',
71
+                        'arguments' => [
72
+                            'foo' => 'bar'
73
+                        ],
74
+                    ];
75
+                } else {
76
+                    return $default;
77
+                }
78
+            });
79
+
80
+        $this->user->method('getUID')
81
+            ->willReturn('uid');
82
+        $this->loader->expects($this->never())->method($this->anything());
83
+
84
+        $this->config->method('getUserValue')
85
+            ->willReturn(null);
86
+
87
+        $this->config
88
+            ->method('setUserValue')
89
+            ->with(
90
+                $this->equalTo('uid'),
91
+                $this->equalTo('homeobjectstore'),
92
+                $this->equalTo('bucket'),
93
+                $this->equalTo('49'),
94
+                $this->equalTo(null)
95
+            );
96
+
97
+        $mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
98
+        $arguments = $this->invokePrivate($mount, 'arguments');
99
+
100
+        $objectStore = $arguments['objectstore'];
101
+        $this->assertInstanceOf(FakeObjectStore::class, $objectStore);
102
+        $this->assertEquals(['foo' => 'bar', 'bucket' => 49, 'multibucket' => true], $objectStore->getArguments());
103
+    }
104
+
105
+    public function testMultiBucketWithPrefix(): void {
106
+        $this->config->method('getSystemValue')
107
+            ->willReturnCallback(function ($key, $default) {
108
+                if ($key === 'objectstore_multibucket') {
109
+                    return [
110
+                        'class' => 'Test\Files\Mount\FakeObjectStore',
111
+                        'arguments' => [
112
+                            'foo' => 'bar',
113
+                            'bucket' => 'myBucketPrefix',
114
+                        ],
115
+                    ];
116
+                } else {
117
+                    return $default;
118
+                }
119
+            });
120
+
121
+        $this->user->method('getUID')
122
+            ->willReturn('uid');
123
+        $this->loader->expects($this->never())->method($this->anything());
124
+
125
+        $this->config
126
+            ->method('getUserValue')
127
+            ->willReturn(null);
128
+
129
+        $this->config->expects($this->once())
130
+            ->method('setUserValue')
131
+            ->with(
132
+                $this->equalTo('uid'),
133
+                $this->equalTo('homeobjectstore'),
134
+                $this->equalTo('bucket'),
135
+                $this->equalTo('myBucketPrefix49'),
136
+                $this->equalTo(null)
137
+            );
138
+
139
+        $mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
140
+        $arguments = $this->invokePrivate($mount, 'arguments');
141
+
142
+        $objectStore = $arguments['objectstore'];
143
+        $this->assertInstanceOf(FakeObjectStore::class, $objectStore);
144
+        $this->assertEquals(['foo' => 'bar', 'bucket' => 'myBucketPrefix49', 'multibucket' => true], $objectStore->getArguments());
145
+    }
146
+
147
+    public function testMultiBucketBucketAlreadySet(): void {
148
+        $this->config->method('getSystemValue')
149
+            ->willReturnCallback(function ($key, $default) {
150
+                if ($key === 'objectstore_multibucket') {
151
+                    return [
152
+                        'class' => 'Test\Files\Mount\FakeObjectStore',
153
+                        'arguments' => [
154
+                            'foo' => 'bar',
155
+                            'bucket' => 'myBucketPrefix',
156
+                        ],
157
+                    ];
158
+                } else {
159
+                    return $default;
160
+                }
161
+            });
162
+
163
+        $this->user->method('getUID')
164
+            ->willReturn('uid');
165
+        $this->loader->expects($this->never())->method($this->anything());
166
+
167
+        $this->config
168
+            ->method('getUserValue')
169
+            ->willReturnCallback(function ($uid, $app, $key, $default) {
170
+                if ($uid === 'uid' && $app === 'homeobjectstore' && $key === 'bucket') {
171
+                    return 'awesomeBucket1';
172
+                } else {
173
+                    return $default;
174
+                }
175
+            });
176
+
177
+        $this->config->expects($this->never())
178
+            ->method('setUserValue');
179
+
180
+        $mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
181
+        $arguments = $this->invokePrivate($mount, 'arguments');
182
+
183
+        $objectStore = $arguments['objectstore'];
184
+        $this->assertInstanceOf(FakeObjectStore::class, $objectStore);
185
+        $this->assertEquals(['foo' => 'bar', 'bucket' => 'awesomeBucket1', 'multibucket' => true], $objectStore->getArguments());
186
+    }
187
+
188
+    public function testMultiBucketConfigFirst(): void {
189
+        $this->config->method('getSystemValue')
190
+            ->willReturnCallback(function ($key, $default) {
191
+                if ($key === 'objectstore_multibucket') {
192
+                    return [
193
+                        'class' => 'Test\Files\Mount\FakeObjectStore',
194
+                        'arguments' => [
195
+                            'foo' => 'bar',
196
+                            'bucket' => 'myBucketPrefix',
197
+                        ],
198
+                    ];
199
+                } else {
200
+                    return $default;
201
+                }
202
+            });
203
+
204
+        $this->user->method('getUID')
205
+            ->willReturn('uid');
206
+        $this->loader->expects($this->never())->method($this->anything());
207
+
208
+        $mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
209
+        $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount);
210
+    }
211
+
212
+    public function testMultiBucketConfigFirstFallBackSingle(): void {
213
+        $this->config
214
+            ->method('getSystemValue')->willReturnMap([
215
+                ['objectstore_multibucket', null, null],
216
+                ['objectstore', null, [
217
+                    'class' => 'Test\Files\Mount\FakeObjectStore',
218
+                    'arguments' => [
219
+                        'foo' => 'bar',
220
+                        'bucket' => 'myBucketPrefix',
221
+                    ],
222
+                ]],
223
+            ]);
224
+
225
+        $this->user->method('getUID')
226
+            ->willReturn('uid');
227
+        $this->loader->expects($this->never())->method($this->anything());
228
+
229
+        $mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
230
+        $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount);
231
+    }
232
+
233
+    public function testNoObjectStore(): void {
234
+        $this->config->method('getSystemValue')
235
+            ->willReturnCallback(function ($key, $default) {
236
+                return $default;
237
+            });
238
+
239
+        $mount = $this->provider->getHomeMountForUser($this->user, $this->loader);
240
+        $this->assertNull($mount);
241
+    }
242 242
 }
243 243
 
244 244
 class FakeObjectStore implements IObjectStore {
245
-	public function __construct(
246
-		private array $arguments,
247
-	) {
248
-	}
245
+    public function __construct(
246
+        private array $arguments,
247
+    ) {
248
+    }
249 249
 
250
-	public function getArguments() {
251
-		return $this->arguments;
252
-	}
250
+    public function getArguments() {
251
+        return $this->arguments;
252
+    }
253 253
 
254
-	public function getStorageId() {
255
-	}
254
+    public function getStorageId() {
255
+    }
256 256
 
257
-	public function readObject($urn) {
258
-	}
257
+    public function readObject($urn) {
258
+    }
259 259
 
260
-	public function writeObject($urn, $stream, ?string $mimetype = null) {
261
-	}
260
+    public function writeObject($urn, $stream, ?string $mimetype = null) {
261
+    }
262 262
 
263
-	public function deleteObject($urn) {
264
-	}
263
+    public function deleteObject($urn) {
264
+    }
265 265
 
266
-	public function objectExists($urn) {
267
-	}
266
+    public function objectExists($urn) {
267
+    }
268 268
 
269
-	public function copyObject($from, $to) {
270
-	}
269
+    public function copyObject($from, $to) {
270
+    }
271 271
 
272
-	public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
273
-		return null;
274
-	}
272
+    public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
273
+        return null;
274
+    }
275 275
 }
Please login to merge, or discard this patch.