diff --git a/apps/files_sharing/lib/Config/ConfigLexicon.php b/apps/files_sharing/lib/Config/ConfigLexicon.php index 3211c755fc79a..c2743a2c4ce16 100644 --- a/apps/files_sharing/lib/Config/ConfigLexicon.php +++ b/apps/files_sharing/lib/Config/ConfigLexicon.php @@ -22,6 +22,7 @@ */ class ConfigLexicon implements ILexicon { public const SHOW_FEDERATED_AS_INTERNAL = 'show_federated_shares_as_internal'; + public const SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL = 'show_federated_shares_to_trusted_servers_as_internal'; public function getStrictness(): Strictness { return Strictness::IGNORE; @@ -30,6 +31,7 @@ public function getStrictness(): Strictness { public function getAppConfigs(): array { return [ new Entry(self::SHOW_FEDERATED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares as internal shares', true), + new Entry(self::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares to trusted servers as internal shares', true), ]; } diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 7591493167f49..095a8a75963f1 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -15,6 +15,7 @@ use OC\Files\Storage\Wrapper\Wrapper; use OCA\Circles\Api\v1\Circles; use OCA\Deck\Sharing\ShareAPIHelper; +use OCA\Federation\TrustedServers; use OCA\Files\Helper; use OCA\Files_Sharing\Exceptions\SharingRightsException; use OCA\Files_Sharing\External\Storage; @@ -76,6 +77,7 @@ class ShareAPIController extends OCSController { private ?Node $lockedNode = null; + private array $trustedServerCache = []; /** * Share20OCS constructor. @@ -100,6 +102,7 @@ public function __construct( private IProviderFactory $factory, private IMailer $mailer, private ITagManager $tagManager, + private ?TrustedServers $trustedServers, private ?string $userId = null, ) { parent::__construct($appName, $request); @@ -202,6 +205,32 @@ protected function formatShare(IShare $share, ?Node $recipientNode = null): arra $result['item_size'] = $node->getSize(); $result['item_mtime'] = $node->getMTime(); + if ($this->trustedServers !== null && in_array($share->getShareType(), [IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], true)) { + $result['is_trusted_server'] = false; + $sharedWith = $share->getSharedWith(); + $remoteIdentifier = is_string($sharedWith) ? strrchr($sharedWith, '@') : false; + if ($remoteIdentifier !== false) { + $remote = substr($remoteIdentifier, 1); + + if (isset($this->trustedServerCache[$remote])) { + $result['is_trusted_server'] = $this->trustedServerCache[$remote]; + } else { + try { + $isTrusted = $this->trustedServers->isTrustedServer($remote); + $this->trustedServerCache[$remote] = $isTrusted; + $result['is_trusted_server'] = $isTrusted; + } catch (\Exception $e) { + // Server not found or other issue, we consider it not trusted + $this->trustedServerCache[$remote] = false; + $this->logger->error( + 'Error checking if remote server is trusted (treating as untrusted): ' . $e->getMessage(), + ['exception' => $e] + ); + } + } + } + } + $expiration = $share->getExpirationDate(); if ($expiration !== null) { $expiration->setTimezone($this->dateTimeZone->getTimeZone()); diff --git a/apps/files_sharing/lib/Listener/LoadSidebarListener.php b/apps/files_sharing/lib/Listener/LoadSidebarListener.php index 9f0eee9159a2b..88c39f38545e3 100644 --- a/apps/files_sharing/lib/Listener/LoadSidebarListener.php +++ b/apps/files_sharing/lib/Listener/LoadSidebarListener.php @@ -38,6 +38,7 @@ public function handle(Event $event): void { $appConfig = Server::get(IAppConfig::class); $this->initialState->provideInitialState('showFederatedSharesAsInternal', $appConfig->getValueBool('files_sharing', ConfigLexicon::SHOW_FEDERATED_AS_INTERNAL)); + $this->initialState->provideInitialState('showFederatedSharesToTrustedServersAsInternal', $appConfig->getValueBool('files_sharing', ConfigLexicon::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL)); Util::addScript(Application::APP_ID, 'files_sharing_tab', 'files'); } } diff --git a/apps/files_sharing/lib/ResponseDefinitions.php b/apps/files_sharing/lib/ResponseDefinitions.php index 6b6b0fcc4b680..71a2b25a70cf8 100644 --- a/apps/files_sharing/lib/ResponseDefinitions.php +++ b/apps/files_sharing/lib/ResponseDefinitions.php @@ -22,6 +22,7 @@ * file_target: string, * has_preview: bool, * hide_download: 0|1, + * is_trusted_server?: bool, * is-mount-root: bool, * id: string, * item_mtime: int, diff --git a/apps/files_sharing/openapi.json b/apps/files_sharing/openapi.json index 627c25ce8ea33..aed112b17f12a 100644 --- a/apps/files_sharing/openapi.json +++ b/apps/files_sharing/openapi.json @@ -548,6 +548,9 @@ 1 ] }, + "is_trusted_server": { + "type": "boolean" + }, "is-mount-root": { "type": "boolean" }, diff --git a/apps/files_sharing/src/components/SharingEntry.vue b/apps/files_sharing/src/components/SharingEntry.vue index 4ff5fae364bba..1fbe740cb1172 100644 --- a/apps/files_sharing/src/components/SharingEntry.vue +++ b/apps/files_sharing/src/components/SharingEntry.vue @@ -77,9 +77,9 @@ export default { title += ` (${t('files_sharing', 'group')})` } else if (this.share.type === ShareType.Room) { title += ` (${t('files_sharing', 'conversation')})` - } else if (this.share.type === ShareType.Remote) { + } else if (this.share.type === ShareType.Remote && !this.share.isTrustedServer) { title += ` (${t('files_sharing', 'remote')})` - } else if (this.share.type === ShareType.RemoteGroup) { + } else if (this.share.type === ShareType.RemoteGroup && !this.share.isTrustedServer) { title += ` (${t('files_sharing', 'remote group')})` } else if (this.share.type === ShareType.Guest) { title += ` (${t('files_sharing', 'guest')})` diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue index b886ba95a177e..f50dc96fc08f6 100644 --- a/apps/files_sharing/src/components/SharingInput.vue +++ b/apps/files_sharing/src/components/SharingInput.vue @@ -192,14 +192,27 @@ export default { lookup = true } - let shareType = [] - const remoteTypes = [ShareType.Remote, ShareType.RemoteGroup] - - if (this.isExternal && !this.config.showFederatedSharesAsInternal) { - shareType.push(...remoteTypes) + const shareType = [] + + const showFederatedAsInternal + = this.config.showFederatedSharesAsInternal + || this.config.showFederatedSharesToTrustedServersAsInternal + + const shouldAddRemoteTypes + // For internal users, add remote types if config says to show them as internal + = (!this.isExternal && showFederatedAsInternal) + // For external users, add them if config *doesn't* say to show them as internal + || (this.isExternal && !showFederatedAsInternal) + // Edge case: federated-to-trusted is a separate "add" trigger for external users + || (this.isExternal && this.config.showFederatedSharesToTrustedServersAsInternal) + + if (this.isExternal) { + if (getCapabilities().files_sharing.public.enabled === true) { + shareType.push(ShareType.Email) + } } else { - shareType = shareType.concat([ + shareType.push( ShareType.User, ShareType.Group, ShareType.Team, @@ -207,15 +220,11 @@ export default { ShareType.Guest, ShareType.Deck, ShareType.ScienceMesh, - ]) - - if (this.config.showFederatedSharesAsInternal) { - shareType.push(...remoteTypes) - } + ) } - if (getCapabilities().files_sharing.public.enabled === true && this.isExternal) { - shareType.push(ShareType.Email) + if (shouldAddRemoteTypes) { + shareType.push(...remoteTypes) } let request = null @@ -366,6 +375,11 @@ export default { // filter out existing mail shares if (share.value.shareType === ShareType.Email) { + // When sharing internally, we don't want to suggest email addresses + // that the user previously created shares to + if (!this.isExternal) { + return arr + } const emails = this.linkShares.map(elem => elem.shareWith) if (emails.indexOf(share.value.shareWith.trim()) !== -1) { return arr diff --git a/apps/files_sharing/src/models/Share.ts b/apps/files_sharing/src/models/Share.ts index fb76a655d53f6..b0638b2944801 100644 --- a/apps/files_sharing/src/models/Share.ts +++ b/apps/files_sharing/src/models/Share.ts @@ -486,4 +486,11 @@ export default class Share { return this._share.status } + /** + * Is the share from a trusted server + */ + get isTrustedServer(): boolean { + return !!this._share.is_trusted_server + } + } diff --git a/apps/files_sharing/src/services/ConfigService.ts b/apps/files_sharing/src/services/ConfigService.ts index 2114e2d1baeb8..f75f34c793643 100644 --- a/apps/files_sharing/src/services/ConfigService.ts +++ b/apps/files_sharing/src/services/ConfigService.ts @@ -315,4 +315,12 @@ export default class Config { return loadState('files_sharing', 'showFederatedSharesAsInternal', false) } + /** + * Show federated shares to trusted servers as internal shares + * @return {boolean} + */ + get showFederatedSharesToTrustedServersAsInternal(): boolean { + return loadState('files_sharing', 'showFederatedSharesToTrustedServersAsInternal', false) + } + } diff --git a/apps/files_sharing/src/views/SharingLinkList.vue b/apps/files_sharing/src/views/SharingLinkList.vue index 3dd6fdf317b38..c3d9a7f83dc80 100644 --- a/apps/files_sharing/src/views/SharingLinkList.vue +++ b/apps/files_sharing/src/views/SharingLinkList.vue @@ -7,12 +7,6 @@
diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index e56201f6e06ea..e6acf33e04d5c 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -402,7 +402,13 @@ export default { if ([ShareType.Link, ShareType.Email].includes(share.type)) { this.linkShares.push(share) } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) { - if (this.config.showFederatedSharesAsInternal) { + if (this.config.showFederatedSharesToTrustedServersAsInternal) { + if (share.isTrustedServer) { + this.shares.push(share) + } else { + this.externalShares.push(share) + } + } else if (this.config.showFederatedSharesAsInternal) { this.shares.push(share) } else { this.externalShares.push(share) @@ -478,6 +484,10 @@ export default { } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) { if (this.config.showFederatedSharesAsInternal) { this.shares.unshift(share) + } if (this.config.showFederatedSharesToTrustedServersAsInternal) { + if (share.isTrustedServer) { + this.shares.unshift(share) + } } else { this.externalShares.unshift(share) } diff --git a/apps/files_sharing/tests/ApiTest.php b/apps/files_sharing/tests/ApiTest.php index 676809eebff69..960f29224bb9e 100644 --- a/apps/files_sharing/tests/ApiTest.php +++ b/apps/files_sharing/tests/ApiTest.php @@ -13,6 +13,7 @@ use OC\Files\Filesystem; use OC\Files\Storage\Temporary; use OC\Files\View; +use OCA\Federation\TrustedServers; use OCA\Files_Sharing\Controller\ShareAPIController; use OCP\App\IAppManager; use OCP\AppFramework\OCS\OCSBadRequestException; @@ -117,6 +118,7 @@ private function createOCS($userId) { $providerFactory = $this->createMock(IProviderFactory::class); $mailer = $this->createMock(IMailer::class); $tagManager = $this->createMock(ITagManager::class); + $trustedServers = $this->createMock(TrustedServers::class); $dateTimeZone->method('getTimeZone')->willReturn(new \DateTimeZone(date_default_timezone_get())); return new ShareAPIController( @@ -139,6 +141,7 @@ private function createOCS($userId) { $providerFactory, $mailer, $tagManager, + $trustedServers, $userId, ); } diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index abc405fc21c82..e6be0342c26d2 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -7,6 +7,7 @@ */ namespace OCA\Files_Sharing\Tests\Controller; +use OCA\Federation\TrustedServers; use OCA\Files_Sharing\Controller\ShareAPIController; use OCP\App\IAppManager; use OCP\AppFramework\Http\DataResponse; @@ -82,6 +83,7 @@ class ShareAPIControllerTest extends TestCase { private IProviderFactory&MockObject $factory; private IMailer&MockObject $mailer; private ITagManager&MockObject $tagManager; + private TrustedServers&MockObject $trustedServers; protected function setUp(): void { $this->shareManager = $this->createMock(IManager::class); @@ -119,6 +121,7 @@ protected function setUp(): void { $this->factory = $this->createMock(IProviderFactory::class); $this->mailer = $this->createMock(IMailer::class); $this->tagManager = $this->createMock(ITagManager::class); + $this->trustedServers = $this->createMock(TrustedServers::class); $this->ocs = new ShareAPIController( $this->appName, @@ -140,8 +143,10 @@ protected function setUp(): void { $this->factory, $this->mailer, $this->tagManager, - $this->currentUser, + $this->trustedServers, + $this->currentUser ); + } /** @@ -169,6 +174,7 @@ private function mockFormatShare() { $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ])->onlyMethods(['formatShare']) ->getMock(); @@ -853,6 +859,7 @@ public function testGetShare(IShare $share, array $result): void { $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ]) ->onlyMethods(['canAccessShare']) @@ -1485,6 +1492,7 @@ public function testGetShares(array $getSharesParameters, array $shares, array $ $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ]) ->onlyMethods(['formatShare']) @@ -1873,6 +1881,7 @@ public function testCreateShareUser(): void { $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ])->onlyMethods(['formatShare']) ->getMock(); @@ -1972,6 +1981,7 @@ public function testCreateShareGroup(): void { $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ])->onlyMethods(['formatShare']) ->getMock(); @@ -2399,6 +2409,7 @@ public function testCreateShareRemote(): void { $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ])->onlyMethods(['formatShare']) ->getMock(); @@ -2471,6 +2482,7 @@ public function testCreateShareRemoteGroup(): void { $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ])->onlyMethods(['formatShare']) ->getMock(); @@ -2710,6 +2722,7 @@ public function testCreateReshareOfFederatedMountNoDeletePermissions(): void { $this->factory, $this->mailer, $this->tagManager, + $this->trustedServers, $this->currentUser, ])->onlyMethods(['formatShare']) ->getMock(); @@ -4492,6 +4505,7 @@ public function dataFormatShare() { 'mount-type' => '', 'attributes' => null, 'item_permissions' => 1, + 'is_trusted_server' => false, ], $share, [], false ]; @@ -4545,6 +4559,7 @@ public function dataFormatShare() { 'mount-type' => '', 'attributes' => null, 'item_permissions' => 1, + 'is_trusted_server' => false, ], $share, [], false ]; @@ -5228,4 +5243,138 @@ public function testPopulateTags(): void { ['file_source' => 42, 'x' => 'y', 'tags' => ['tag1', 'tag2']], ], $result); } + + public function trustedServerProvider(): array { + return [ + 'Trusted server' => [true, true], + 'Untrusted server' => [false, false], + ]; + } + + /** + * @dataProvider trustedServerProvider + */ + public function testFormatShareWithFederatedShare(bool $isKnownServer, bool $isTrusted): void { + $nodeId = 12; + $nodePath = '/test.txt'; + $share = $this->createShare( + 1, + IShare::TYPE_REMOTE, + 'recipient@remoteserver.com', // shared with + 'sender@testserver.com', // shared by + 'shareOwner', // share owner + $nodePath, // path + Constants::PERMISSION_READ, + time(), + null, + null, + $nodePath, + $nodeId + ); + + $node = $this->createMock(\OCP\Files\File::class); + $node->method('getId')->willReturn($nodeId); + $node->method('getPath')->willReturn($nodePath); + $node->method('getInternalPath')->willReturn(ltrim($nodePath, '/')); + $mountPoint = $this->createMock(\OCP\Files\Mount\IMountPoint::class); + $mountPoint->method('getMountType')->willReturn('local'); + $node->method('getMountPoint')->willReturn($mountPoint); + $node->method('getMimetype')->willReturn('text/plain'); + $storage = $this->createMock(\OCP\Files\Storage\IStorage::class); + $storageCache = $this->createMock(\OCP\Files\Cache\ICache::class); + $storageCache->method('getNumericStorageId')->willReturn(1); + $storage->method('getCache')->willReturn($storageCache); + $storage->method('getId')->willReturn('home::shareOwner'); + $node->method('getStorage')->willReturn($storage); + $parent = $this->createMock(\OCP\Files\Folder::class); + $parent->method('getId')->willReturn(2); + $node->method('getParent')->willReturn($parent); + $node->method('getSize')->willReturn(1234); + $node->method('getMTime')->willReturn(1234567890); + + $this->previewManager->method('isAvailable')->with($node)->willReturn(false); + + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturnSelf(); + + $this->rootFolder->method('getFirstNodeById') + ->with($share->getNodeId()) + ->willReturn($node); + + $this->rootFolder->method('getRelativePath') + ->with($node->getPath()) + ->willReturnArgument(0); + + $serverName = 'remoteserver.com'; + $this->trustedServers->method('isTrustedServer') + ->with($serverName) + ->willReturn($isKnownServer); + + $result = $this->invokePrivate($this->ocs, 'formatShare', [$share]); + + $this->assertSame($isTrusted, $result['is_trusted_server']); + } + + public function testFormatShareWithFederatedShareWithAtInUsername(): void { + $nodeId = 12; + $nodePath = '/test.txt'; + $share = $this->createShare( + 1, + IShare::TYPE_REMOTE, + 'recipient@domain.com@remoteserver.com', + 'sender@testserver.com', + 'shareOwner', + $nodePath, + Constants::PERMISSION_READ, + time(), + null, + null, + $nodePath, + $nodeId + ); + + $node = $this->createMock(\OCP\Files\File::class); + $node->method('getId')->willReturn($nodeId); + $node->method('getPath')->willReturn($nodePath); + $node->method('getInternalPath')->willReturn(ltrim($nodePath, '/')); + $mountPoint = $this->createMock(\OCP\Files\Mount\IMountPoint::class); + $mountPoint->method('getMountType')->willReturn('local'); + $node->method('getMountPoint')->willReturn($mountPoint); + $node->method('getMimetype')->willReturn('text/plain'); + $storage = $this->createMock(\OCP\Files\Storage\IStorage::class); + $storageCache = $this->createMock(\OCP\Files\Cache\ICache::class); + $storageCache->method('getNumericStorageId')->willReturn(1); + $storage->method('getCache')->willReturn($storageCache); + $storage->method('getId')->willReturn('home::shareOwner'); + $node->method('getStorage')->willReturn($storage); + $parent = $this->createMock(\OCP\Files\Folder::class); + $parent->method('getId')->willReturn(2); + $node->method('getParent')->willReturn($parent); + $node->method('getSize')->willReturn(1234); + $node->method('getMTime')->willReturn(1234567890); + + $this->previewManager->method('isAvailable')->with($node)->willReturn(false); + + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturnSelf(); + + $this->rootFolder->method('getFirstNodeById') + ->with($share->getNodeId()) + ->willReturn($node); + + $this->rootFolder->method('getRelativePath') + ->with($node->getPath()) + ->willReturnArgument(0); + + $serverName = 'remoteserver.com'; + $this->trustedServers->method('isTrustedServer') + ->with($serverName) + ->willReturn(true); + + $result = $this->invokePrivate($this->ocs, 'formatShare', [$share]); + + $this->assertTrue($result['is_trusted_server']); + } } diff --git a/cypress/e2e/files_sharing/public-share/download.cy.ts b/cypress/e2e/files_sharing/public-share/download.cy.ts index 786a81deb4c2c..372f553a8a0bc 100644 --- a/cypress/e2e/files_sharing/public-share/download.cy.ts +++ b/cypress/e2e/files_sharing/public-share/download.cy.ts @@ -212,8 +212,6 @@ describe('files_sharing: Public share - downloading files', { testIsolation: tru cy.reload() - getRowForFile('test').should('be.visible') - triggerActionForFile('test', 'details') openLinkShareDetails(0) cy.findByRole('checkbox', { name: /hide download/i }) .should('be.checked') @@ -257,7 +255,7 @@ describe('files_sharing: Public share - downloading files', { testIsolation: tru cy.wait('@update') - openLinkShareDetails(1) + openLinkShareDetails(0) cy.findByRole('button', { name: /advanced settings/i }) .click() cy.findByRole('checkbox', { name: /hide download/i }) diff --git a/dist/4732-4732.js b/dist/4732-4732.js new file mode 100644 index 0000000000000..ff0c923a4fa1c --- /dev/null +++ b/dist/4732-4732.js @@ -0,0 +1,2 @@ +(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[4732,5810],{12143:(t,e,r)=>{"use strict";r.d(e,{A:()=>o});var i=r(71354),n=r.n(i),a=r(76314),s=r.n(a)()(n());s.push([t.id,".sharing-entry[data-v-42622a71]{display:flex;align-items:center;height:44px}.sharing-entry__summary[data-v-42622a71]{padding:8px;padding-inline-start:10px;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;flex:1 0;min-width:0}.sharing-entry__summary__desc[data-v-42622a71]{display:inline-block;padding-bottom:0;line-height:1.2em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sharing-entry__summary__desc p[data-v-42622a71],.sharing-entry__summary__desc small[data-v-42622a71]{color:var(--color-text-maxcontrast)}.sharing-entry__summary__desc-unique[data-v-42622a71]{color:var(--color-text-maxcontrast)}","",{version:3,sources:["webpack://./apps/files_sharing/src/components/SharingEntry.vue"],names:[],mappings:"AACA,gCACC,YAAA,CACA,kBAAA,CACA,WAAA,CACA,yCACC,WAAA,CACA,yBAAA,CACA,YAAA,CACA,qBAAA,CACA,sBAAA,CACA,sBAAA,CACA,QAAA,CACA,WAAA,CAEA,+CACC,oBAAA,CACA,gBAAA,CACA,iBAAA,CACA,kBAAA,CACA,eAAA,CACA,sBAAA,CAEA,sGAEC,mCAAA,CAGD,sDACC,mCAAA",sourcesContent:["\n.sharing-entry {\n\tdisplay: flex;\n\talign-items: center;\n\theight: 44px;\n\t&__summary {\n\t\tpadding: 8px;\n\t\tpadding-inline-start: 10px;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: flex-start;\n\t\tflex: 1 0;\n\t\tmin-width: 0;\n\n\t\t&__desc {\n\t\t\tdisplay: inline-block;\n\t\t\tpadding-bottom: 0;\n\t\t\tline-height: 1.2em;\n\t\t\twhite-space: nowrap;\n\t\t\toverflow: hidden;\n\t\t\ttext-overflow: ellipsis;\n\n\t\t\tp,\n\t\t\tsmall {\n\t\t\t\tcolor: var(--color-text-maxcontrast);\n\t\t\t}\n\n\t\t\t&-unique {\n\t\t\t\tcolor: var(--color-text-maxcontrast);\n\t\t\t}\n\t\t}\n\t}\n\n}\n"],sourceRoot:""}]);const o=s},17816:function(t){t.exports=function(){"use strict";function t(){throw new Error("Dynamic requires are not currently supported by rollup-plugin-commonjs")}var e=function(t,e){return t(e={exports:{}},e.exports),e.exports}((function(e,r){var i;i=function(){return function e(r,i,n){function a(o,l){if(!i[o]){if(!r[o]){if(!l&&t)return t();if(s)return s(o,!0);var c=new Error("Cannot find module '"+o+"'");throw c.code="MODULE_NOT_FOUND",c}var h=i[o]={exports:{}};r[o][0].call(h.exports,(function(t){return a(r[o][1][t]||t)}),h,h.exports,e,r,i,n)}return i[o].exports}for(var s=t,o=0;o\n\t\t\t\t\t{{ subtitle }}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\t\t{{ internalSharesHelpText }}\n\t\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t{{ externalSharesHelpText }}\n\t\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t{{ additionalSharesHelpText }}\n\t\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t{{ subtitle }}\n\t\t\t
\n\t\t\n\t\t\t\t\t{{ subtitle }}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\t\t{{ internalSharesHelpText }}\n\t\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t{{ externalSharesHelpText }}\n\t\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\t{{ additionalSharesHelpText }}\n\t\t\t\t\t\t
\n\t\t\t\t\t