Skip to content

Commit 8e7e0a1

Browse files
committed
feat: global internal link
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
1 parent ee39c92 commit 8e7e0a1

19 files changed

+1540
-24
lines changed

appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
</background-jobs>
2929

3030
<commands>
31+
<command>OCA\GlobalSiteSelector\Command\GlobalScaleDiscovery</command>
3132
<command>OCA\GlobalSiteSelector\Command\UsersUpdate</command>
3233
</commands>
3334
</info>

appinfo/routes.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
return [
1010
'ocs' => [
1111
['name' => 'Slave#createAppToken', 'url' => '/v1/createapptoken', 'verb' => 'GET'],
12+
['name' => 'Slave#discovery', 'url' => '/discovery', 'verb' => 'GET'],
13+
['name' => 'Slave#sharedFile', 'url' => '/sharedfile', 'verb' => 'GET'],
1214
],
1315
'routes' => [
1416
[

lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCA\GlobalSiteSelector\GlobalSiteSelector;
1616
use OCA\GlobalSiteSelector\Listeners\AddContentSecurityPolicyListener;
1717
use OCA\GlobalSiteSelector\Listeners\DeletingUser;
18+
use OCA\GlobalSiteSelector\Listeners\InternalLinkRequested;
1819
use OCA\GlobalSiteSelector\Listeners\UserCreated;
1920
use OCA\GlobalSiteSelector\Listeners\UserDeleted;
2021
use OCA\GlobalSiteSelector\Listeners\UserLoggedOut;
@@ -27,6 +28,7 @@
2728
use OCP\AppFramework\Bootstrap\IBootstrap;
2829
use OCP\AppFramework\Bootstrap\IRegistrationContext;
2930
use OCP\EventDispatcher\IEventDispatcher;
31+
use OCP\Files\Events\InternalLinkRequestEvent;
3032
use OCP\IRequest;
3133
use OCP\IUser;
3234
use OCP\IUserManager;
@@ -79,6 +81,7 @@ public function register(IRegistrationContext $context): void {
7981
$context->registerEventListener(BeforeUserDeletedEvent::class, DeletingUser::class);
8082
$context->registerEventListener(UserDeletedEvent::class, UserDeleted::class);
8183
$context->registerEventListener(UserLoggedOutEvent::class, UserLoggedOut::class);
84+
$context->registerEventListener(InternalLinkRequestEvent::class, InternalLinkRequested::class);
8285

8386
// It seems that AccountManager use deprecated dispatcher, let's use a deprecated listener
8487
/** @var IEventDispatcher $eventDispatcher */

lib/BackgroundJobs/UpdateLookupServer.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,23 @@
66
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
77
* SPDX-License-Identifier: AGPL-3.0-or-later
88
*/
9-
109
namespace OCA\GlobalSiteSelector\BackgroundJobs;
1110

1211
use OCA\GlobalSiteSelector\GlobalSiteSelector;
12+
use OCA\GlobalSiteSelector\Service\GlobalScaleService;
1313
use OCA\GlobalSiteSelector\Slave;
1414
use OCP\AppFramework\Utility\ITimeFactory;
1515
use OCP\BackgroundJob\IJob;
1616
use OCP\BackgroundJob\TimedJob;
1717
use OCP\IConfig;
1818

1919
class UpdateLookupServer extends TimedJob {
20-
21-
2220
public function __construct(
2321
ITimeFactory $time,
2422
IConfig $config,
25-
private GlobalSiteSelector $globalSiteSelector,
26-
private Slave $slave,
23+
private readonly GlobalScaleService $globalScaleService,
24+
private readonly GlobalSiteSelector $globalSiteSelector,
25+
private readonly Slave $slave,
2726
) {
2827
parent::__construct($time);
2928

@@ -36,6 +35,7 @@ protected function run($argument) {
3635
return;
3736
}
3837

38+
$this->globalScaleService->refreshTokenFromGlobalScale();
3939
$this->slave->batchUpdate();
4040
}
4141
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OCA\GlobalSiteSelector\Command;
10+
11+
use OC\Core\Command\Base;
12+
use OCA\GlobalSiteSelector\AppInfo\Application;
13+
use OCA\GlobalSiteSelector\ConfigLexicon;
14+
use OCA\GlobalSiteSelector\Service\GlobalScaleService;
15+
use OCP\IAppConfig;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
20+
class GlobalScaleDiscovery extends Base {
21+
public function __construct(
22+
private readonly IAppConfig $appConfig,
23+
private readonly GlobalScaleService $globalScaleService,
24+
) {
25+
parent::__construct();
26+
}
27+
28+
29+
/**
30+
*
31+
*/
32+
protected function configure() {
33+
parent::configure();
34+
$this->setName('globalsiteselector:discovery')
35+
->addOption('current', '', InputOption::VALUE_NONE, 'display current data')
36+
->setDescription('run a discovery request over Global Scale to get details about each instances');
37+
}
38+
39+
/**
40+
* @param InputInterface $input
41+
* @param OutputInterface $output
42+
*
43+
* @return int
44+
*/
45+
protected function execute(InputInterface $input, OutputInterface $output): int {
46+
if ($input->getOption('current')) {
47+
$output->writeln(json_encode($this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS), JSON_PRETTY_PRINT));
48+
return 0;
49+
}
50+
51+
// currently, the only available data is a unique token that helps identify each instance
52+
$this->globalScaleService->refreshTokenFromGlobalScale();
53+
return 0;
54+
}
55+
}

lib/ConfigLexicon.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OCA\GlobalSiteSelector;
10+
11+
use OCP\Config\Lexicon\Entry;
12+
use OCP\Config\Lexicon\IConfigLexicon;
13+
use OCP\Config\Lexicon\Strictness;
14+
use OCP\Config\ValueType;
15+
16+
class ConfigLexicon implements IConfigLexicon {
17+
public const GS_TOKENS = 'globalScaleTokens';
18+
public const LOCAL_TOKEN = 'localToken';
19+
20+
public function getStrictness(): Strictness {
21+
return Strictness::IGNORE;
22+
}
23+
24+
/**
25+
* @inheritDoc
26+
*/
27+
public function getAppConfigs(): array {
28+
return [
29+
new Entry(key: self::GS_TOKENS, type: ValueType::ARRAY, defaultRaw: [], definition: 'list of token+host to navigate throw GlobalScale', lazy: true),
30+
new Entry(self::LOCAL_TOKEN, ValueType::STRING, '', 'local token to id instance of GlobalScale ', true),
31+
];
32+
}
33+
34+
/**
35+
* @inheritDoc
36+
*/
37+
public function getUserConfigs(): array {
38+
return [
39+
];
40+
}
41+
}

lib/Controller/SlaveController.php

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010

1111
use OC\Authentication\Token\IToken;
1212
use OCA\GlobalSiteSelector\AppInfo\Application;
13+
use OCA\GlobalSiteSelector\Exceptions\LocalFederatedShareException;
1314
use OCA\GlobalSiteSelector\Exceptions\MasterUrlException;
15+
use OCA\GlobalSiteSelector\Exceptions\SharedFileException;
1416
use OCA\GlobalSiteSelector\GlobalSiteSelector;
17+
use OCA\GlobalSiteSelector\Model\LocalFile;
18+
use OCA\GlobalSiteSelector\Service\GlobalScaleService;
19+
use OCA\GlobalSiteSelector\Service\GlobalShareService;
1520
use OCA\GlobalSiteSelector\Service\SlaveService;
1621
use OCA\GlobalSiteSelector\Slave;
1722
use OCA\GlobalSiteSelector\TokenHandler;
@@ -41,25 +46,74 @@
4146
* @package OCA\GlobalSiteSelector\Controller
4247
*/
4348
class SlaveController extends OCSController {
44-
4549
public function __construct(
4650
$appName,
4751
IRequest $request,
48-
private GlobalSiteSelector $gss,
49-
private IUserSession $userSession,
50-
private IURLGenerator $urlGenerator,
51-
private ICrypto $crypto,
52-
private TokenHandler $tokenHandler,
53-
private IUserManager $userManager,
54-
private UserBackend $userBackend,
55-
private ISession $session,
56-
private SlaveService $slaveService,
57-
private IConfig $config,
58-
private LoggerInterface $logger,
52+
private readonly GlobalSiteSelector $gss,
53+
private readonly IUserSession $userSession,
54+
private readonly IURLGenerator $urlGenerator,
55+
private readonly ICrypto $crypto,
56+
private readonly TokenHandler $tokenHandler,
57+
private readonly IUserManager $userManager,
58+
private readonly UserBackend $userBackend,
59+
private readonly ISession $session,
60+
private readonly SlaveService $slaveService,
61+
private readonly GlobalScaleService $globalScaleService,
62+
private readonly GlobalShareService $globalShareService,
63+
private readonly IConfig $config,
64+
private readonly LoggerInterface $logger,
5965
) {
6066
parent::__construct($appName, $request);
6167
}
6268

69+
70+
/**
71+
* @PublicPage
72+
* @NoCSRFRequired
73+
*/
74+
public function discovery(): DataResponse {
75+
// public data, reachable from outside.
76+
return new DataResponse(['token' => $this->globalScaleService->getLocalToken()]);
77+
}
78+
79+
/**
80+
* return sharing details about a file.
81+
* request must contain encoded jwt.
82+
*
83+
* @PublicPage
84+
* @NoCSRFRequired
85+
*/
86+
public function sharedFile(?string $jwt): DataResponse {
87+
if ($jwt === null) {
88+
return new DataResponse(['message' => 'missing jwt'], Http::STATUS_NOT_FOUND);
89+
}
90+
$key = $this->gss->getJwtKey();
91+
$decoded = (array)JWT::decode($jwt, new Key($key, Application::JWT_ALGORITHM));
92+
// JWT store data as stdClass, not array
93+
$decoded = json_decode(json_encode($decoded), true);
94+
95+
$this->logger->debug('decoded request', ['data' => $decoded]);
96+
97+
$fileId = (int)($decoded['fileId'] ?? 0);
98+
$shareId = (int)($decoded['shareId'] ?? 0);
99+
$instance = $decoded['instance'] ?? '';
100+
101+
$target = new LocalFile();
102+
$target->import($decoded['target'] ?? []);
103+
104+
try {
105+
// the file is local and returns shares related to it
106+
return new DataResponse($this->globalShareService->getSharedFiles($fileId, $shareId, $instance, $target));
107+
} catch (SharedFileException $e) {
108+
// file not found
109+
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
110+
} catch (LocalFederatedShareException $e) {
111+
// the file is not local and returns the shared folder and the path to the file
112+
return new DataResponse($e->getFederatedShare(), Http::STATUS_MOVED_PERMANENTLY);
113+
}
114+
}
115+
116+
63117
/**
64118
* @PublicPage
65119
* @NoCSRFRequired

0 commit comments

Comments
 (0)