Completed
Push — master ( 802d93...16088c )
by Joas
23:20 queued 11s
created
lib/private/Federation/CloudIdManager.php 1 patch
Indentation   +280 added lines, -280 removed lines patch added patch discarded remove patch
@@ -22,284 +22,284 @@
 block discarded – undo
22 22
 use OCP\User\Events\UserChangedEvent;
23 23
 
24 24
 class CloudIdManager implements ICloudIdManager {
25
-	private ICache $memCache;
26
-	private ICache $displayNameCache;
27
-	private array $cache = [];
28
-	/** @var ICloudIdResolver[] */
29
-	private array $cloudIdResolvers = [];
30
-
31
-	public function __construct(
32
-		ICacheFactory $cacheFactory,
33
-		IEventDispatcher $eventDispatcher,
34
-		private IManager $contactsManager,
35
-		private IURLGenerator $urlGenerator,
36
-		private IUserManager $userManager,
37
-	) {
38
-		$this->memCache = $cacheFactory->createDistributed('cloud_id_');
39
-		$this->displayNameCache = $cacheFactory->createDistributed('cloudid_name_');
40
-		$eventDispatcher->addListener(UserChangedEvent::class, [$this, 'handleUserEvent']);
41
-		$eventDispatcher->addListener(CardUpdatedEvent::class, [$this, 'handleCardEvent']);
42
-	}
43
-
44
-	public function handleUserEvent(Event $event): void {
45
-		if ($event instanceof UserChangedEvent && $event->getFeature() === 'displayName') {
46
-			$userId = $event->getUser()->getUID();
47
-			$key = $userId . '@local';
48
-			unset($this->cache[$key]);
49
-			$this->memCache->remove($key);
50
-		}
51
-	}
52
-
53
-	public function handleCardEvent(Event $event): void {
54
-		if ($event instanceof CardUpdatedEvent) {
55
-			$data = $event->getCardData()['carddata'];
56
-			foreach (explode("\r\n", $data) as $line) {
57
-				if (str_starts_with($line, 'CLOUD;')) {
58
-					$parts = explode(':', $line, 2);
59
-					if (isset($parts[1])) {
60
-						$key = $parts[1];
61
-						unset($this->cache[$key]);
62
-						$this->memCache->remove($key);
63
-					}
64
-				}
65
-			}
66
-		}
67
-	}
68
-
69
-	/**
70
-	 * @param string $cloudId
71
-	 * @return ICloudId
72
-	 * @throws \InvalidArgumentException
73
-	 */
74
-	public function resolveCloudId(string $cloudId): ICloudId {
75
-		// TODO magic here to get the url and user instead of just splitting on @
76
-
77
-		foreach ($this->cloudIdResolvers as $resolver) {
78
-			if ($resolver->isValidCloudId($cloudId)) {
79
-				return $resolver->resolveCloudId($cloudId);
80
-			}
81
-		}
82
-
83
-		if (!$this->isValidCloudId($cloudId)) {
84
-			throw new \InvalidArgumentException('Invalid cloud id');
85
-		}
86
-
87
-		// Find the first character that is not allowed in user names
88
-		$id = $this->stripShareLinkFragments($cloudId);
89
-		$posSlash = strpos($id, '/');
90
-		$posColon = strpos($id, ':');
91
-
92
-		if ($posSlash === false && $posColon === false) {
93
-			$invalidPos = \strlen($id);
94
-		} elseif ($posSlash === false) {
95
-			$invalidPos = $posColon;
96
-		} elseif ($posColon === false) {
97
-			$invalidPos = $posSlash;
98
-		} else {
99
-			$invalidPos = min($posSlash, $posColon);
100
-		}
101
-
102
-		$lastValidAtPos = strrpos($id, '@', $invalidPos - strlen($id));
103
-
104
-		if ($lastValidAtPos !== false) {
105
-			$user = substr($id, 0, $lastValidAtPos);
106
-			$remote = substr($id, $lastValidAtPos + 1);
107
-
108
-			// We accept slightly more chars when working with federationId than with a local userId.
109
-			// We remove those eventual chars from the UserId before using
110
-			// the IUserManager API to confirm its format.
111
-			$this->validateUser($user, $remote);
112
-
113
-			if (!empty($user) && !empty($remote)) {
114
-				$remote = $this->ensureDefaultProtocol($remote);
115
-				return new CloudId($id, $user, $remote, null);
116
-			}
117
-		}
118
-		throw new \InvalidArgumentException('Invalid cloud id');
119
-	}
120
-
121
-	protected function validateUser(string $user, string $remote): void {
122
-		// Check the ID for bad characters
123
-		// Allowed are: "a-z", "A-Z", "0-9", spaces and "_.@-'" (Nextcloud)
124
-		// Additional: "=" (oCIS)
125
-		if (preg_match('/[^a-zA-Z0-9 _.@\-\'=]/', $user)) {
126
-			throw new \InvalidArgumentException('Invalid characters');
127
-		}
128
-
129
-		// No empty user ID
130
-		if (trim($user) === '') {
131
-			throw new \InvalidArgumentException('Empty user');
132
-		}
133
-
134
-		// No whitespace at the beginning or at the end
135
-		if (trim($user) !== $user) {
136
-			throw new \InvalidArgumentException('User contains whitespace at the beginning or at the end');
137
-		}
138
-
139
-		// User ID only consists of 1 or 2 dots (directory traversal)
140
-		if ($user === '.' || $user === '..') {
141
-			throw new \InvalidArgumentException('User must not consist of dots only');
142
-		}
143
-
144
-		// User ID is too long
145
-		if (strlen($user . '@' . $remote) > 255) {
146
-			// TRANSLATORS User ID is too long
147
-			throw new \InvalidArgumentException('Cloud id is too long');
148
-		}
149
-	}
150
-
151
-	public function getDisplayNameFromContact(string $cloudId): ?string {
152
-		$cachedName = $this->displayNameCache->get($cloudId);
153
-		if ($cachedName !== null) {
154
-			if ($cachedName === $cloudId) {
155
-				return null;
156
-			}
157
-			return $cachedName;
158
-		}
159
-
160
-		$addressBookEntries = $this->contactsManager->search($cloudId, ['CLOUD'], [
161
-			'limit' => 1,
162
-			'enumeration' => false,
163
-			'fullmatch' => false,
164
-			'strict_search' => true,
165
-		]);
166
-		foreach ($addressBookEntries as $entry) {
167
-			if (isset($entry['CLOUD'])) {
168
-				foreach ($entry['CLOUD'] as $cloudID) {
169
-					if ($cloudID === $cloudId) {
170
-						// Warning, if user decides to make their full name local only,
171
-						// no FN is found on federated servers
172
-						if (isset($entry['FN'])) {
173
-							$this->displayNameCache->set($cloudId, $entry['FN'], 15 * 60);
174
-							return $entry['FN'];
175
-						} else {
176
-							$this->displayNameCache->set($cloudId, $cloudId, 15 * 60);
177
-							return null;
178
-						}
179
-					}
180
-				}
181
-			}
182
-		}
183
-		$this->displayNameCache->set($cloudId, $cloudId, 15 * 60);
184
-		return null;
185
-	}
186
-
187
-	/**
188
-	 * @param string $user
189
-	 * @param string|null $remote
190
-	 * @return CloudId
191
-	 */
192
-	public function getCloudId(string $user, ?string $remote): ICloudId {
193
-		$isLocal = $remote === null;
194
-		if ($isLocal) {
195
-			$remote = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
196
-		}
197
-
198
-		// note that for remote id's we don't strip the protocol for the remote we use to construct the CloudId
199
-		// this way if a user has an explicit non-https cloud id this will be preserved
200
-		// we do still use the version without protocol for looking up the display name
201
-		$remote = $this->stripShareLinkFragments($remote);
202
-		$host = $this->removeProtocolFromUrl($remote);
203
-		$remote = $this->ensureDefaultProtocol($remote);
204
-
205
-		$key = $user . '@' . ($isLocal ? 'local' : $host);
206
-		$cached = $this->cache[$key] ?? $this->memCache->get($key);
207
-		if ($cached) {
208
-			$this->cache[$key] = $cached; // put items from memcache into local cache
209
-			return new CloudId($cached['id'], $cached['user'], $cached['remote'], $cached['displayName']);
210
-		}
211
-
212
-		if ($isLocal) {
213
-			$localUser = $this->userManager->get($user);
214
-			$displayName = $localUser ? $localUser->getDisplayName() : '';
215
-		} else {
216
-			$displayName = null;
217
-		}
218
-
219
-		// For the visible cloudID we only strip away https
220
-		$id = $user . '@' . $this->removeProtocolFromUrl($remote, true);
221
-
222
-		$data = [
223
-			'id' => $id,
224
-			'user' => $user,
225
-			'remote' => $remote,
226
-			'displayName' => $displayName,
227
-		];
228
-		$this->cache[$key] = $data;
229
-		$this->memCache->set($key, $data, 15 * 60);
230
-		return new CloudId($id, $user, $remote, $displayName);
231
-	}
232
-
233
-	/**
234
-	 * @param string $url
235
-	 * @return string
236
-	 */
237
-	public function removeProtocolFromUrl(string $url, bool $httpsOnly = false): string {
238
-		if (str_starts_with($url, 'https://')) {
239
-			return substr($url, 8);
240
-		}
241
-		if (!$httpsOnly && str_starts_with($url, 'http://')) {
242
-			return substr($url, 7);
243
-		}
244
-
245
-		return $url;
246
-	}
247
-
248
-	protected function ensureDefaultProtocol(string $remote): string {
249
-		if (!str_contains($remote, '://')) {
250
-			$remote = 'https://' . $remote;
251
-		}
252
-
253
-		return $remote;
254
-	}
255
-
256
-	/**
257
-	 * Strips away a potential file names and trailing slashes:
258
-	 * - http://localhost
259
-	 * - http://localhost/
260
-	 * - http://localhost/index.php
261
-	 * - http://localhost/index.php/s/{shareToken}
262
-	 *
263
-	 * all return: http://localhost
264
-	 *
265
-	 * @param string $remote
266
-	 * @return string
267
-	 */
268
-	protected function stripShareLinkFragments(string $remote): string {
269
-		$remote = str_replace('\\', '/', $remote);
270
-		if ($fileNamePosition = strpos($remote, '/index.php')) {
271
-			$remote = substr($remote, 0, $fileNamePosition);
272
-		}
273
-		$remote = rtrim($remote, '/');
274
-
275
-		return $remote;
276
-	}
277
-
278
-	/**
279
-	 * @param string $cloudId
280
-	 * @return bool
281
-	 */
282
-	public function isValidCloudId(string $cloudId): bool {
283
-		foreach ($this->cloudIdResolvers as $resolver) {
284
-			if ($resolver->isValidCloudId($cloudId)) {
285
-				return true;
286
-			}
287
-		}
288
-
289
-		return strpos($cloudId, '@') !== false;
290
-	}
291
-
292
-	public function createCloudId(string $id, string $user, string $remote, ?string $displayName = null): ICloudId {
293
-		return new CloudId($id, $user, $remote, $displayName);
294
-	}
295
-
296
-	public function registerCloudIdResolver(ICloudIdResolver $resolver): void {
297
-		array_unshift($this->cloudIdResolvers, $resolver);
298
-	}
299
-
300
-	public function unregisterCloudIdResolver(ICloudIdResolver $resolver): void {
301
-		if (($key = array_search($resolver, $this->cloudIdResolvers)) !== false) {
302
-			array_splice($this->cloudIdResolvers, $key, 1);
303
-		}
304
-	}
25
+    private ICache $memCache;
26
+    private ICache $displayNameCache;
27
+    private array $cache = [];
28
+    /** @var ICloudIdResolver[] */
29
+    private array $cloudIdResolvers = [];
30
+
31
+    public function __construct(
32
+        ICacheFactory $cacheFactory,
33
+        IEventDispatcher $eventDispatcher,
34
+        private IManager $contactsManager,
35
+        private IURLGenerator $urlGenerator,
36
+        private IUserManager $userManager,
37
+    ) {
38
+        $this->memCache = $cacheFactory->createDistributed('cloud_id_');
39
+        $this->displayNameCache = $cacheFactory->createDistributed('cloudid_name_');
40
+        $eventDispatcher->addListener(UserChangedEvent::class, [$this, 'handleUserEvent']);
41
+        $eventDispatcher->addListener(CardUpdatedEvent::class, [$this, 'handleCardEvent']);
42
+    }
43
+
44
+    public function handleUserEvent(Event $event): void {
45
+        if ($event instanceof UserChangedEvent && $event->getFeature() === 'displayName') {
46
+            $userId = $event->getUser()->getUID();
47
+            $key = $userId . '@local';
48
+            unset($this->cache[$key]);
49
+            $this->memCache->remove($key);
50
+        }
51
+    }
52
+
53
+    public function handleCardEvent(Event $event): void {
54
+        if ($event instanceof CardUpdatedEvent) {
55
+            $data = $event->getCardData()['carddata'];
56
+            foreach (explode("\r\n", $data) as $line) {
57
+                if (str_starts_with($line, 'CLOUD;')) {
58
+                    $parts = explode(':', $line, 2);
59
+                    if (isset($parts[1])) {
60
+                        $key = $parts[1];
61
+                        unset($this->cache[$key]);
62
+                        $this->memCache->remove($key);
63
+                    }
64
+                }
65
+            }
66
+        }
67
+    }
68
+
69
+    /**
70
+     * @param string $cloudId
71
+     * @return ICloudId
72
+     * @throws \InvalidArgumentException
73
+     */
74
+    public function resolveCloudId(string $cloudId): ICloudId {
75
+        // TODO magic here to get the url and user instead of just splitting on @
76
+
77
+        foreach ($this->cloudIdResolvers as $resolver) {
78
+            if ($resolver->isValidCloudId($cloudId)) {
79
+                return $resolver->resolveCloudId($cloudId);
80
+            }
81
+        }
82
+
83
+        if (!$this->isValidCloudId($cloudId)) {
84
+            throw new \InvalidArgumentException('Invalid cloud id');
85
+        }
86
+
87
+        // Find the first character that is not allowed in user names
88
+        $id = $this->stripShareLinkFragments($cloudId);
89
+        $posSlash = strpos($id, '/');
90
+        $posColon = strpos($id, ':');
91
+
92
+        if ($posSlash === false && $posColon === false) {
93
+            $invalidPos = \strlen($id);
94
+        } elseif ($posSlash === false) {
95
+            $invalidPos = $posColon;
96
+        } elseif ($posColon === false) {
97
+            $invalidPos = $posSlash;
98
+        } else {
99
+            $invalidPos = min($posSlash, $posColon);
100
+        }
101
+
102
+        $lastValidAtPos = strrpos($id, '@', $invalidPos - strlen($id));
103
+
104
+        if ($lastValidAtPos !== false) {
105
+            $user = substr($id, 0, $lastValidAtPos);
106
+            $remote = substr($id, $lastValidAtPos + 1);
107
+
108
+            // We accept slightly more chars when working with federationId than with a local userId.
109
+            // We remove those eventual chars from the UserId before using
110
+            // the IUserManager API to confirm its format.
111
+            $this->validateUser($user, $remote);
112
+
113
+            if (!empty($user) && !empty($remote)) {
114
+                $remote = $this->ensureDefaultProtocol($remote);
115
+                return new CloudId($id, $user, $remote, null);
116
+            }
117
+        }
118
+        throw new \InvalidArgumentException('Invalid cloud id');
119
+    }
120
+
121
+    protected function validateUser(string $user, string $remote): void {
122
+        // Check the ID for bad characters
123
+        // Allowed are: "a-z", "A-Z", "0-9", spaces and "_.@-'" (Nextcloud)
124
+        // Additional: "=" (oCIS)
125
+        if (preg_match('/[^a-zA-Z0-9 _.@\-\'=]/', $user)) {
126
+            throw new \InvalidArgumentException('Invalid characters');
127
+        }
128
+
129
+        // No empty user ID
130
+        if (trim($user) === '') {
131
+            throw new \InvalidArgumentException('Empty user');
132
+        }
133
+
134
+        // No whitespace at the beginning or at the end
135
+        if (trim($user) !== $user) {
136
+            throw new \InvalidArgumentException('User contains whitespace at the beginning or at the end');
137
+        }
138
+
139
+        // User ID only consists of 1 or 2 dots (directory traversal)
140
+        if ($user === '.' || $user === '..') {
141
+            throw new \InvalidArgumentException('User must not consist of dots only');
142
+        }
143
+
144
+        // User ID is too long
145
+        if (strlen($user . '@' . $remote) > 255) {
146
+            // TRANSLATORS User ID is too long
147
+            throw new \InvalidArgumentException('Cloud id is too long');
148
+        }
149
+    }
150
+
151
+    public function getDisplayNameFromContact(string $cloudId): ?string {
152
+        $cachedName = $this->displayNameCache->get($cloudId);
153
+        if ($cachedName !== null) {
154
+            if ($cachedName === $cloudId) {
155
+                return null;
156
+            }
157
+            return $cachedName;
158
+        }
159
+
160
+        $addressBookEntries = $this->contactsManager->search($cloudId, ['CLOUD'], [
161
+            'limit' => 1,
162
+            'enumeration' => false,
163
+            'fullmatch' => false,
164
+            'strict_search' => true,
165
+        ]);
166
+        foreach ($addressBookEntries as $entry) {
167
+            if (isset($entry['CLOUD'])) {
168
+                foreach ($entry['CLOUD'] as $cloudID) {
169
+                    if ($cloudID === $cloudId) {
170
+                        // Warning, if user decides to make their full name local only,
171
+                        // no FN is found on federated servers
172
+                        if (isset($entry['FN'])) {
173
+                            $this->displayNameCache->set($cloudId, $entry['FN'], 15 * 60);
174
+                            return $entry['FN'];
175
+                        } else {
176
+                            $this->displayNameCache->set($cloudId, $cloudId, 15 * 60);
177
+                            return null;
178
+                        }
179
+                    }
180
+                }
181
+            }
182
+        }
183
+        $this->displayNameCache->set($cloudId, $cloudId, 15 * 60);
184
+        return null;
185
+    }
186
+
187
+    /**
188
+     * @param string $user
189
+     * @param string|null $remote
190
+     * @return CloudId
191
+     */
192
+    public function getCloudId(string $user, ?string $remote): ICloudId {
193
+        $isLocal = $remote === null;
194
+        if ($isLocal) {
195
+            $remote = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
196
+        }
197
+
198
+        // note that for remote id's we don't strip the protocol for the remote we use to construct the CloudId
199
+        // this way if a user has an explicit non-https cloud id this will be preserved
200
+        // we do still use the version without protocol for looking up the display name
201
+        $remote = $this->stripShareLinkFragments($remote);
202
+        $host = $this->removeProtocolFromUrl($remote);
203
+        $remote = $this->ensureDefaultProtocol($remote);
204
+
205
+        $key = $user . '@' . ($isLocal ? 'local' : $host);
206
+        $cached = $this->cache[$key] ?? $this->memCache->get($key);
207
+        if ($cached) {
208
+            $this->cache[$key] = $cached; // put items from memcache into local cache
209
+            return new CloudId($cached['id'], $cached['user'], $cached['remote'], $cached['displayName']);
210
+        }
211
+
212
+        if ($isLocal) {
213
+            $localUser = $this->userManager->get($user);
214
+            $displayName = $localUser ? $localUser->getDisplayName() : '';
215
+        } else {
216
+            $displayName = null;
217
+        }
218
+
219
+        // For the visible cloudID we only strip away https
220
+        $id = $user . '@' . $this->removeProtocolFromUrl($remote, true);
221
+
222
+        $data = [
223
+            'id' => $id,
224
+            'user' => $user,
225
+            'remote' => $remote,
226
+            'displayName' => $displayName,
227
+        ];
228
+        $this->cache[$key] = $data;
229
+        $this->memCache->set($key, $data, 15 * 60);
230
+        return new CloudId($id, $user, $remote, $displayName);
231
+    }
232
+
233
+    /**
234
+     * @param string $url
235
+     * @return string
236
+     */
237
+    public function removeProtocolFromUrl(string $url, bool $httpsOnly = false): string {
238
+        if (str_starts_with($url, 'https://')) {
239
+            return substr($url, 8);
240
+        }
241
+        if (!$httpsOnly && str_starts_with($url, 'http://')) {
242
+            return substr($url, 7);
243
+        }
244
+
245
+        return $url;
246
+    }
247
+
248
+    protected function ensureDefaultProtocol(string $remote): string {
249
+        if (!str_contains($remote, '://')) {
250
+            $remote = 'https://' . $remote;
251
+        }
252
+
253
+        return $remote;
254
+    }
255
+
256
+    /**
257
+     * Strips away a potential file names and trailing slashes:
258
+     * - http://localhost
259
+     * - http://localhost/
260
+     * - http://localhost/index.php
261
+     * - http://localhost/index.php/s/{shareToken}
262
+     *
263
+     * all return: http://localhost
264
+     *
265
+     * @param string $remote
266
+     * @return string
267
+     */
268
+    protected function stripShareLinkFragments(string $remote): string {
269
+        $remote = str_replace('\\', '/', $remote);
270
+        if ($fileNamePosition = strpos($remote, '/index.php')) {
271
+            $remote = substr($remote, 0, $fileNamePosition);
272
+        }
273
+        $remote = rtrim($remote, '/');
274
+
275
+        return $remote;
276
+    }
277
+
278
+    /**
279
+     * @param string $cloudId
280
+     * @return bool
281
+     */
282
+    public function isValidCloudId(string $cloudId): bool {
283
+        foreach ($this->cloudIdResolvers as $resolver) {
284
+            if ($resolver->isValidCloudId($cloudId)) {
285
+                return true;
286
+            }
287
+        }
288
+
289
+        return strpos($cloudId, '@') !== false;
290
+    }
291
+
292
+    public function createCloudId(string $id, string $user, string $remote, ?string $displayName = null): ICloudId {
293
+        return new CloudId($id, $user, $remote, $displayName);
294
+    }
295
+
296
+    public function registerCloudIdResolver(ICloudIdResolver $resolver): void {
297
+        array_unshift($this->cloudIdResolvers, $resolver);
298
+    }
299
+
300
+    public function unregisterCloudIdResolver(ICloudIdResolver $resolver): void {
301
+        if (($key = array_search($resolver, $this->cloudIdResolvers)) !== false) {
302
+            array_splice($this->cloudIdResolvers, $key, 1);
303
+        }
304
+    }
305 305
 }
Please login to merge, or discard this patch.
apps/files_sharing/lib/Migration/Version32000Date20251017081948.php 1 patch
Indentation   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -19,20 +19,20 @@
 block discarded – undo
19 19
 
20 20
 #[ModifyColumn(table: 'share_external', name: 'owner', type: ColumnType::STRING, description: 'Change length to 255 characters')]
21 21
 class Version32000Date20251017081948 extends SimpleMigrationStep {
22
-	/**
23
-	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
24
-	 */
25
-	#[Override]
26
-	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
27
-		/** @var ISchemaWrapper $schema */
28
-		$schema = $schemaClosure();
22
+    /**
23
+     * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
24
+     */
25
+    #[Override]
26
+    public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
27
+        /** @var ISchemaWrapper $schema */
28
+        $schema = $schemaClosure();
29 29
 
30
-		$table = $schema->getTable('share_external');
31
-		$column = $table->getColumn('owner');
32
-		if ($column->getLength() < 255) {
33
-			$column->setLength(255);
34
-			return $schema;
35
-		}
36
-		return null;
37
-	}
30
+        $table = $schema->getTable('share_external');
31
+        $column = $table->getColumn('owner');
32
+        if ($column->getLength() < 255) {
33
+            $column->setLength(255);
34
+            return $schema;
35
+        }
36
+        return null;
37
+    }
38 38
 }
Please login to merge, or discard this patch.