Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
'OCA\\Files_Sharing\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
'OCA\\Files_Sharing\\OpenMetrics\\SharesCountMetric' => $baseDir . '/../lib/OpenMetrics/SharesCountMetric.php',
'OCA\\Files_Sharing\\OrphanHelper' => $baseDir . '/../lib/OrphanHelper.php',
'OCA\\Files_Sharing\\PublicShareUrlGenerator' => $baseDir . '/../lib/PublicShareUrlGenerator.php',
'OCA\\Files_Sharing\\Repair\\CleanupShareTarget' => $baseDir . '/../lib/Repair/CleanupShareTarget.php',
'OCA\\Files_Sharing\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
'OCA\\Files_Sharing\\Scanner' => $baseDir . '/../lib/Scanner.php',
Expand Down
1 change: 1 addition & 0 deletions apps/files_sharing/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
'OCA\\Files_Sharing\\OpenMetrics\\SharesCountMetric' => __DIR__ . '/..' . '/../lib/OpenMetrics/SharesCountMetric.php',
'OCA\\Files_Sharing\\OrphanHelper' => __DIR__ . '/..' . '/../lib/OrphanHelper.php',
'OCA\\Files_Sharing\\PublicShareUrlGenerator' => __DIR__ . '/..' . '/../lib/PublicShareUrlGenerator.php',
'OCA\\Files_Sharing\\Repair\\CleanupShareTarget' => __DIR__ . '/..' . '/../lib/Repair/CleanupShareTarget.php',
'OCA\\Files_Sharing\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
'OCA\\Files_Sharing\\Scanner' => __DIR__ . '/..' . '/../lib/Scanner.php',
Expand Down
5 changes: 4 additions & 1 deletion apps/files_sharing/lib/Controller/ShareAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use OCA\Federation\TrustedServers;
use OCA\Files_Sharing\Exceptions\SharingRightsException;
use OCA\Files_Sharing\External\Storage;
use OCA\Files_Sharing\PublicShareUrlGenerator;
use OCA\Files_Sharing\ResponseDefinitions;
use OCA\Files_Sharing\SharedStorage;
use OCA\GlobalSiteSelector\Service\SlaveService;
Expand Down Expand Up @@ -107,6 +108,7 @@ public function __construct(
private IMailer $mailer,
private ITagManager $tagManager,
private IEmailValidator $emailValidator,
private PublicShareUrlGenerator $publicShareUrlGenerator,
private ?TrustedServers $trustedServers,
private ?string $userId = null,
) {
Expand Down Expand Up @@ -271,7 +273,7 @@ protected function formatShare(IShare $share, ?Node $recipientNode = null): arra
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith();
} elseif ($share->getShareType() === IShare::TYPE_LINK) {
$url = $token ? $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]) : null;
$url = $token ? $this->publicShareUrlGenerator->getUrl($token) : null;

// "share_with" and "share_with_displayname" for passwords of link
// shares was deprecated in Nextcloud 15, use "password" instead.
Expand All @@ -293,6 +295,7 @@ protected function formatShare(IShare $share, ?Node $recipientNode = null): arra
$result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD');
$result['token'] = $token;
} elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
$result['url'] = $token ? $this->publicShareUrlGenerator->getUrl($token) : null;
$result['share_with'] = $share->getSharedWith();
$result['password'] = $this->formatPasswordField($share->getPassword());
$result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function __construct(
private IPreview $previewManager,
protected FederatedShareProvider $federatedShareProvider,
private IUrlGenerator $urlGenerator,
private PublicShareUrlGenerator $publicShareUrlGenerator,
private IEventDispatcher $eventDispatcher,
private IL10N $l10n,
private Defaults $defaults,
Expand Down Expand Up @@ -175,7 +176,7 @@ public function renderPage(IShare $share, string $token, string $path): Template
}
}

$shareUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]);
$shareUrl = $this->publicShareUrlGenerator->getUrl($token);
// By default use the share link as the direct link
$directLink = $shareUrl;
// Add the direct link header actions
Expand All @@ -199,7 +200,7 @@ public function renderPage(IShare $share, string $token, string $path): Template
protected function addMetaHeaders(IShare $share): void {
$shareNode = $share->getNode();
$token = $share->getToken();
$shareUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]);
$shareUrl = $this->publicShareUrlGenerator->getUrl($token);

// Handle preview generation for OpenGraph
$hasImagePreview = false;
Expand Down
75 changes: 75 additions & 0 deletions apps/files_sharing/lib/PublicShareUrlGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_Sharing;

use OC\Core\AppInfo\ConfigLexicon;
use OCP\IAppConfig;
use OCP\IURLGenerator;
use Psr\Log\LoggerInterface;

class PublicShareUrlGenerator {
private bool $invalidBaseUrlLogged = false;

public function __construct(
private IAppConfig $appConfig,
private IURLGenerator $urlGenerator,
private LoggerInterface $logger,
) {
}

public function getUrl(string $token): string {
$baseUrl = $this->getBaseUrl();
if ($baseUrl === null) {
return $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]);
}

// Keep the router as the single source of truth for the share link path,
// including a potential '/index.php' front controller prefix on instances
// without pretty URLs, and only swap the instance's webroot for the
// configured base URL.
$path = $this->urlGenerator->linkToRoute('files_sharing.sharecontroller.showShare', ['token' => $token]);
$webroot = $this->urlGenerator->getWebroot();
if ($webroot !== '' && str_starts_with($path, $webroot)) {
$path = substr($path, strlen($webroot));
}

return $baseUrl . $path;
}

private function getBaseUrl(): ?string {
$baseUrl = trim($this->appConfig->getValueString('core', ConfigLexicon::SHARE_LINK_BASE_URL, ''));
if ($baseUrl === '') {
return null;
}

if (filter_var($baseUrl, FILTER_VALIDATE_URL) !== false) {
$parts = parse_url($baseUrl);
if (is_array($parts)
&& isset($parts['host'])
&& in_array($parts['scheme'] ?? '', ['http', 'https'], true)
&& !isset($parts['user'])
&& !isset($parts['pass'])
&& !isset($parts['query'])
&& !isset($parts['fragment'])) {
return rtrim($baseUrl, '/');
}
}

// Do not log the configured value itself, it may contain credentials
if (!$this->invalidBaseUrlLogged) {
$this->invalidBaseUrlLogged = true;
$this->logger->warning(
'Ignoring invalid {configKey} app config value, falling back to the default base URL for public share links. Expected an absolute http(s) URL without credentials, query or fragment.',
['app' => 'files_sharing', 'configKey' => ConfigLexicon::SHARE_LINK_BASE_URL],
);
}

return null;
}
}
5 changes: 2 additions & 3 deletions apps/files_sharing/lib/SharesReminderJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public function __construct(
private readonly IUserManager $userManager,
private readonly LoggerInterface $logger,
private readonly IURLGenerator $urlGenerator,
private readonly PublicShareUrlGenerator $publicShareUrlGenerator,
private readonly IFactory $l10nFactory,
private readonly IMailer $mailer,
private readonly Defaults $defaults,
Expand Down Expand Up @@ -231,9 +232,7 @@ private function prepareReminder(IShare $share): ?array {
]);
} else {
$reminderInfo['email'] = $sharedWith;
$reminderInfo['folderLink'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', [
'token' => $share->getToken()
]);
$reminderInfo['folderLink'] = $this->publicShareUrlGenerator->getUrl($share->getToken());
}
if (empty($reminderInfo['email'])) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export default defineComponent({

computed: {
shareLink() {
return generateUrl('/s/{token}', { token: this.share.token }, { baseURL: getBaseUrl() })
return this.share.url ?? generateUrl('/s/{token}', { token: this.share.token }, { baseURL: getBaseUrl() })
},
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type * as Router from '@nextcloud/router'

import { beforeEach, describe, expect, it, vi } from 'vitest'
import NewFileRequestDialogFinish from './NewFileRequestDialog/NewFileRequestDialogFinish.vue'
import SharingEntryLink from './SharingEntryLink.vue'

const generateUrl = vi.hoisted(() => vi.fn())
const getBaseUrl = vi.hoisted(() => vi.fn())
vi.mock('@nextcloud/router', async (importOriginal) => ({
...await importOriginal<typeof Router>(),
generateUrl,
getBaseUrl,
}))

const components = [
['SharingEntryLink', SharingEntryLink],
['NewFileRequestDialogFinish', NewFileRequestDialogFinish],
] as const

beforeEach(() => {
vi.clearAllMocks()
})

describe.each(components)('%s public share link', (_name, component) => {
it('uses the complete URL returned by the server', () => {
generateUrl.mockReturnValue('http://nextcloud.local/s/token')

const shareLink = component.computed.shareLink.call({
share: {
token: 'token',
url: 'https://public-gateway.example/s/token',
},
})

expect(shareLink).toBe('https://public-gateway.example/s/token')
expect(generateUrl).not.toHaveBeenCalled()
})

it('falls back to the existing URL generation when the server URL is absent', () => {
getBaseUrl.mockReturnValue('/nextcloud')
generateUrl.mockReturnValue('http://nextcloud.local/nextcloud/s/token')

const shareLink = component.computed.shareLink.call({
share: { token: 'token' },
})

expect(shareLink).toBe('http://nextcloud.local/nextcloud/s/token')
expect(generateUrl).toHaveBeenCalledWith('/s/{token}', { token: 'token' }, { baseURL: '/nextcloud' })
})
})
2 changes: 1 addition & 1 deletion apps/files_sharing/src/components/SharingEntryLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ export default {
* @return {string}
*/
shareLink() {
return generateUrl('/s/{token}', { token: this.share.token }, { baseURL: getBaseUrl() })
return this.share.url ?? generateUrl('/s/{token}', { token: this.share.token }, { baseURL: getBaseUrl() })
},

/**
Expand Down
19 changes: 19 additions & 0 deletions apps/files_sharing/src/models/Share.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { describe, expect, it } from 'vitest'
import Share from './Share.ts'

describe('Share', () => {
it('exposes the complete public link URL returned by the server', () => {
const share = new Share({
id: 1,
share_type: 3,
url: 'https://public-gateway.example/prefix/s/token',
})

expect(share.url).toBe('https://public-gateway.example/prefix/s/token')
})
})
7 changes: 7 additions & 0 deletions apps/files_sharing/src/models/Share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ export default class Share {
return this._share.token
}

/**
* Get the complete public share URL returned by the server
*/
get url(): string | undefined {
return this._share.url
}

/**
* Set the public share token
*/
Expand Down
26 changes: 26 additions & 0 deletions apps/files_sharing/tests/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OC\Files\View;
use OCA\Federation\TrustedServers;
use OCA\Files_Sharing\Controller\ShareAPIController;
use OCA\Files_Sharing\PublicShareUrlGenerator;
use OCP\App\IAppManager;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSException;
Expand Down Expand Up @@ -146,6 +147,7 @@ private function createOCS($userId) {
$mailer,
$tagManager,
$this->getEmailValidatorWithStrictEmailCheck(),
Server::get(PublicShareUrlGenerator::class),
$trustedServers,
$userId,
);
Expand Down Expand Up @@ -240,6 +242,30 @@ public function testCreateShareLink(): void {
$ocs->cleanup();
}

#[\PHPUnit\Framework\Attributes\Group(name: 'RoutingWeirdness')]
public function testCreateShareLinkWithConfiguredPublicLinkBaseUrl(): void {
$appConfig = Server::get(IAppConfig::class);
$appConfig->setValueString('core', ConfigLexicon::SHARE_LINK_BASE_URL, 'https://public-gateway.example/prefix/');

try {
$ocs = $this->createOCS(self::TEST_FILES_SHARING_API_USER1);
$result = $ocs->createShare($this->folder, Constants::PERMISSION_ALL, IShare::TYPE_LINK);
$ocs->cleanup();

$data = $result->getData();
$this->assertSame(
'https://public-gateway.example/prefix/s/' . $data['token'],
$data['url'],
);

$ocs = $this->createOCS(self::TEST_FILES_SHARING_API_USER1);
$ocs->deleteShare($data['id']);
$ocs->cleanup();
} finally {
$appConfig->deleteKey('core', ConfigLexicon::SHARE_LINK_BASE_URL);
}
}

#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataAllowFederationOnPublicShares')]
#[\PHPUnit\Framework\Attributes\Group(name: 'RoutingWeirdness')]
public function testCreateShareLinkPublicUpload(array $appConfig, int $permissions): void {
Expand Down
Loading