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
47 changes: 44 additions & 3 deletions apps/files_sharing/lib/Listener/LoadAdditionalListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,28 @@

use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\External\Manager as ExternalManager;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Server;
use OCP\IConfig;
use OCP\IUserSession;
use OCP\Share\IManager;
use OCP\Share\IShare;
use OCP\Util;

/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */
class LoadAdditionalListener implements IEventListener {

public function __construct(
private IInitialState $initialState,
private IConfig $config,
private IManager $shareManager,
private IUserSession $userSession,
private ExternalManager $externalManager,
) {
}

#[\Override]
public function handle(Event $event): void {
if (!($event instanceof LoadAdditionalScriptsEvent)) {
Expand All @@ -29,9 +43,36 @@ public function handle(Event $event): void {
Util::addScript(Application::APP_ID, 'additionalScripts', 'files');
Util::addStyle(Application::APP_ID, 'icons');

$shareManager = Server::get(IManager::class);
if ($shareManager->shareApiEnabled()) {
if ($this->shareManager->shareApiEnabled()) {
Util::addInitScript(Application::APP_ID, 'init');
}

$this->provideInitialStates();
}

private function provideInitialStates(): void {
$user = $this->userSession->getUser();
if ($user === null) {
return;
}

$acceptDefault = $this->config->getSystemValueBool('sharing.enable_share_accept');
$this->initialState->provideInitialState('sharing.enable_share_accept', $acceptDefault);

$hasPendingShares = $this->hasPendingShares($user->getUID());
$this->initialState->provideInitialState('has_pending_shares', $hasPendingShares);
}

private function hasPendingShares(string $userId): bool {
foreach ([IShare::TYPE_USER, IShare::TYPE_GROUP] as $shareType) {
$shares = $this->shareManager->getSharedWith($userId, $shareType, null, -1, 0);
foreach ($shares as $share) {
if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) {
return true;
}
}
}

return !empty($this->externalManager->getOpenShares());
}
}
46 changes: 44 additions & 2 deletions apps/files_sharing/src/files_views/shares.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,26 @@ beforeEach(() => {
expect(navigation.views).toHaveLength(0)
})

/** Helper to mock loadState with sharing.enable_share_accept and has_pending_shares defaults */
function mockLoadState({ acceptDefault = true, hasPendingShares = true, storageQuota = -1 } = {}) {
return vi.spyOn(ncInitialState, 'loadState').mockImplementation((app, key, defaultValue) => {
if (app === 'files_sharing' && key === 'sharing.enable_share_accept') {
return acceptDefault
}
if (app === 'files_sharing' && key === 'has_pending_shares') {
return hasPendingShares
}
if (app === 'files' && key === 'storageStats') {
return { quota: storageQuota }
}
return defaultValue
})
}

describe('Sharing views definition', () => {
test('Default values', () => {
test('Default values with pending shares and accept enabled', () => {
vi.spyOn(navigation, 'register')
mockLoadState({ acceptDefault: true, hasPendingShares: true })

registerSharingViews()
const shareOverviewView = navigation.views.find((view) => view.id === 'shareoverview') as View
Expand Down Expand Up @@ -71,9 +88,33 @@ describe('Sharing views definition', () => {
})
})

test('Pending shares view is not registered when accept is disabled', () => {
vi.spyOn(navigation, 'register')
mockLoadState({ acceptDefault: false, hasPendingShares: true })

registerSharingViews()
expect(navigation.register).toHaveBeenCalledTimes(6)
expect(navigation.views.length).toBe(6)

const pendingSharesView = navigation.views.find((view) => view.id === 'pendingshares')
expect(pendingSharesView).toBeUndefined()
})

test('Pending shares view is not registered when there are no pending shares', () => {
vi.spyOn(navigation, 'register')
mockLoadState({ acceptDefault: true, hasPendingShares: false })

registerSharingViews()
expect(navigation.register).toHaveBeenCalledTimes(6)
expect(navigation.views.length).toBe(6)

const pendingSharesView = navigation.views.find((view) => view.id === 'pendingshares')
expect(pendingSharesView).toBeUndefined()
})

test('Shared with others view is not registered if user has no storage quota', () => {
vi.spyOn(navigation, 'register')
const spy = vi.spyOn(ncInitialState, 'loadState').mockImplementationOnce(() => ({ quota: 0 }))
const spy = mockLoadState({ acceptDefault: true, hasPendingShares: true, storageQuota: 0 })

expect(navigation.views.length).toBe(0)
registerSharingViews()
Expand All @@ -95,6 +136,7 @@ describe('Sharing views definition', () => {

describe('Sharing views contents', () => {
test('Sharing overview get contents', async () => {
mockLoadState({ acceptDefault: true, hasPendingShares: true })
vi.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => {
return {
data: {
Expand Down
22 changes: 22 additions & 0 deletions apps/files_sharing/src/files_views/shares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ export const deletedSharesViewId = 'deletedshares'
export const pendingSharesViewId = 'pendingshares'
export const fileRequestViewId = 'filerequest'

/**
* Checks if share accept approval is required by Nextcloud configuration.
*
* @return True if share accept approval is required, otherwise false.
*/
function isShareAcceptApprovalRequired(): boolean {
return loadState('files_sharing', 'sharing.enable_share_accept', false)
}

/**
* Checks if the current user has any pending shares.
*
* @return True if the user has pending shares, otherwise false.
*/
function hasPendingShares(): boolean {
return loadState('files_sharing', 'has_pending_shares', false)
}

export default () => {
const Navigation = getNavigation()
Navigation.register(new View({
Expand Down Expand Up @@ -137,6 +155,10 @@ export default () => {
getContents: () => getContents(false, false, false, true),
}))

if (!isShareAcceptApprovalRequired() || !hasPendingShares()) {
return
}

Navigation.register(new View({
id: pendingSharesViewId,
name: t('files_sharing', 'Pending shares'),
Expand Down
Loading
Loading