Skip to content

Commit a596910

Browse files
committed
feat(files_sharing): fetch remote avatars for external shares
Signed-off-by: Kent Delante <kent@delante.me> Assisted-by: ClaudeCode:claude-sonnet-5
1 parent 0d42292 commit a596910

5 files changed

Lines changed: 161 additions & 1 deletion

File tree

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,7 @@
13101310
'OC\\Avatar\\AvatarManager' => $baseDir . '/lib/private/Avatar/AvatarManager.php',
13111311
'OC\\Avatar\\GuestAvatar' => $baseDir . '/lib/private/Avatar/GuestAvatar.php',
13121312
'OC\\Avatar\\PlaceholderAvatar' => $baseDir . '/lib/private/Avatar/PlaceholderAvatar.php',
1313+
'OC\\Avatar\\RemoteAvatar' => $baseDir . '/lib/private/Avatar/RemoteAvatar.php',
13131314
'OC\\Avatar\\UserAvatar' => $baseDir . '/lib/private/Avatar/UserAvatar.php',
13141315
'OC\\BackgroundJob\\JobClassesRegistry' => $baseDir . '/lib/private/BackgroundJob/JobClassesRegistry.php',
13151316
'OC\\BackgroundJob\\JobList' => $baseDir . '/lib/private/BackgroundJob/JobList.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
13511351
'OC\\Avatar\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/Avatar/AvatarManager.php',
13521352
'OC\\Avatar\\GuestAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/GuestAvatar.php',
13531353
'OC\\Avatar\\PlaceholderAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/PlaceholderAvatar.php',
1354+
'OC\\Avatar\\RemoteAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/RemoteAvatar.php',
13541355
'OC\\Avatar\\UserAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/UserAvatar.php',
13551356
'OC\\BackgroundJob\\JobClassesRegistry' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/JobClassesRegistry.php',
13561357
'OC\\BackgroundJob\\JobList' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/JobList.php',

lib/private/Avatar/AvatarManager.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OC\User\Manager;
1414
use OCP\Accounts\IAccountManager;
1515
use OCP\Accounts\PropertyDoesNotExistException;
16+
use OCP\Federation\ICloudIdManager;
1617
use OCP\Files\IAppData;
1718
use OCP\Files\NotFoundException;
1819
use OCP\Files\NotPermittedException;
@@ -38,6 +39,7 @@ public function __construct(
3839
private IConfig $config,
3940
private IAccountManager $accountManager,
4041
private KnownUserService $knownUserService,
42+
private ICloudIdManager $cloudIdManager,
4143
) {
4244
}
4345

@@ -53,6 +55,10 @@ public function __construct(
5355
*/
5456
#[\Override]
5557
public function getAvatar(string $userId): IAvatar {
58+
if ($this->cloudIdManager->isValidCloudId($userId)) {
59+
return $this->getRemoteAvatar($userId);
60+
}
61+
5662
$user = $this->userManager->get($userId);
5763
if ($user === null) {
5864
throw new \Exception('user does not exist');
@@ -134,4 +140,20 @@ public function deleteUserAvatar(string $userId): void {
134140
public function getGuestAvatar(string $name): IAvatar {
135141
return new GuestAvatar($name, $this->config, $this->logger);
136142
}
143+
144+
/**
145+
* Returns a RemoteAvatar
146+
*
147+
* @param string $userId The \OCP\Federation\ICloudId of the remote account, e.g. account@example.com
148+
*/
149+
private function getRemoteAvatar(string $userId): IAvatar {
150+
try {
151+
$remoteAvatarFolder = $this->appData->getFolder('__remote');
152+
} catch (NotFoundException $e) {
153+
$remoteAvatarFolder = $this->appData->newFolder('__remote');
154+
}
155+
156+
$folder = $remoteAvatarFolder->getOrCreateFolder($userId);
157+
return new RemoteAvatar($folder, $userId, $this->config, $this->logger);
158+
}
137159
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OC\Avatar;
11+
12+
use OCP\Federation\ICloudId;
13+
use OCP\Federation\ICloudIdManager;
14+
use OCP\Files\SimpleFS\ISimpleFile;
15+
use OCP\Files\SimpleFS\ISimpleFolder;
16+
use OCP\Http\Client\IClientService;
17+
use OCP\IConfig;
18+
use Psr\Log\LoggerInterface;
19+
20+
class RemoteAvatar extends Avatar {
21+
private const IMAGE_CACHE_AGE = 60 * 60 * 24; // One day
22+
23+
private ICloudId $cloudId;
24+
25+
public function __construct(
26+
protected readonly ISimpleFolder $folder,
27+
protected readonly string $userId,
28+
protected IConfig $config,
29+
protected LoggerInterface $logger,
30+
) {
31+
parent::__construct($config, $logger);
32+
33+
$cloudIdManager = \OCP\Server::get(ICloudIdManager::class);
34+
$this->cloudId = $cloudIdManager->resolveCloudId($userId);
35+
}
36+
37+
#[\Override]
38+
public function exists(): bool {
39+
return true;
40+
}
41+
42+
#[\Override]
43+
public function getDisplayName(): string {
44+
return $this->cloudId->getDisplayId();
45+
}
46+
47+
/**
48+
* Setting avatars isn't implemented for remote accounts
49+
*/
50+
#[\Override]
51+
public function set($data): void {
52+
}
53+
54+
/**
55+
* Removing avatars isn't implemented for remote accounts
56+
*/
57+
#[\Override]
58+
public function remove(bool $silent = false): void {
59+
}
60+
61+
#[\Override]
62+
public function getFile(int $size, bool $darkTheme = false): ISimpleFile {
63+
if ($size === -1) {
64+
$filename = 'avatar' . ($darkTheme ? '-dark' : '');
65+
} else {
66+
$filename = 'avatar' . ($darkTheme ? '-dark' : '') . '.' . $size;
67+
}
68+
69+
$avatar = null;
70+
$files = $this->folder->getDirectoryListing();
71+
foreach ($files as $file) {
72+
if (pathinfo($file->getName(), PATHINFO_FILENAME) === $filename) {
73+
$avatar = $file;
74+
break;
75+
}
76+
}
77+
78+
if ($avatar !== null) {
79+
// check if a remote avatar is at least a day old, in case a new avatar was uploaded
80+
$isAvatarOld = (time() - $avatar->getMTime()) >= self::IMAGE_CACHE_AGE;
81+
if (!$isAvatarOld) {
82+
return $avatar;
83+
}
84+
}
85+
86+
$url = rtrim($this->cloudId->getRemote(), '/') . '/index.php/avatar/' . rawurlencode($this->cloudId->getUser()) . '/' . $size;
87+
if ($darkTheme) {
88+
$url .= '/dark';
89+
}
90+
91+
$clientService = \OCP\Server::get(IClientService::class);
92+
$client = $clientService->newClient();
93+
$response = $client->get($url, [
94+
'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false)
95+
]);
96+
97+
$contentType = $response->getHeader('Content-Type');
98+
if (str_starts_with($contentType, 'image/') === false) {
99+
throw new \Exception('Unknown filetype');
100+
}
101+
102+
$avatar = $response->getBody();
103+
if ($avatar === null) {
104+
throw new \Exception('Failed to fetch remote avatar');
105+
}
106+
107+
if (is_resource($avatar)) {
108+
$avatar = stream_get_contents($avatar);
109+
if ($avatar === false) {
110+
throw new \Exception('Failed to fetch remote avatar');
111+
}
112+
}
113+
114+
$ext = match ($contentType) {
115+
'image/png' => 'png',
116+
'image/jpg', 'image/jpeg' => 'jpg',
117+
'image/gif' => 'gif',
118+
'image/webp' => 'webp',
119+
default => 'png',
120+
};
121+
return $this->folder->newFile($filename . '.' . $ext, $avatar);
122+
}
123+
124+
/**
125+
* Handling user changes isn't implemented for remote accounts
126+
*/
127+
#[\Override]
128+
public function userChanged(string $feature, $oldValue, $newValue): void {
129+
}
130+
131+
#[\Override]
132+
public function isCustomAvatar(): bool {
133+
return true;
134+
}
135+
}

lib/private/Server.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,8 @@ public function __construct(
605605
$c->get(LoggerInterface::class),
606606
$c->get(IConfig::class),
607607
$c->get(IAccountManager::class),
608-
$c->get(KnownUserService::class)
608+
$c->get(KnownUserService::class),
609+
$c->get(ICloudIdManager::class)
609610
);
610611
});
611612

0 commit comments

Comments
 (0)