aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/src/views/CalDavSettings.spec.js
blob: 7a4345b3ddf82b4fb19d318f54e5a86961fae9cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
 * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */
import { render } from '@testing-library/vue'
import { beforeEach, describe, expect, test, vi } from 'vitest'

import CalDavSettings from './CalDavSettings.vue'

vi.mock('@nextcloud/axios')
vi.mock('@nextcloud/router', () => {
	return {
		generateUrl(url) {
			return url
		},
	}
})
vi.mock('@nextcloud/initial-state', () => {
	return {
		loadState: vi.fn(() => 'https://docs.nextcloud.com/server/23/go.php?to=user-sync-calendars'),
	}
})

describe('CalDavSettings', () => {
	beforeEach(() => {
		window.OC = { requestToken: 'secret' }
		window.OCP = {
			AppConfig: {
				setValue: vi.fn(),
			},
		}
	})

	test('interactions', async () => {
		const TLUtils = render(
			CalDavSettings,
			{
				data() {
					return {
						sendInvitations: true,
						generateBirthdayCalendar: true,
						sendEventReminders: true,
						sendEventRemindersToSharedUsers: true,
						sendEventRemindersPush: true,
					}
				},
			},
			Vue => {
				Vue.prototype.$t = vi.fn((app, text) => text)
			},
		)
		expect(TLUtils.container).toMatchSnapshot()
		const sendInvitations = TLUtils.getByLabelText(
			'Send invitations to attendees',
		)
		expect(sendInvitations).toBeChecked()
		const generateBirthdayCalendar = TLUtils.getByLabelText(
			'Automatically generate a birthday calendar',
		)
		expect(generateBirthdayCalendar).toBeChecked()
		const sendEventReminders = TLUtils.getByLabelText(
			'Send notifications for events',
		)
		expect(sendEventReminders).toBeChecked()
		const sendEventRemindersToSharedUsers = TLUtils.getByLabelText(
			'Send reminder notifications to calendar sharees as well',
		)
		expect(sendEventRemindersToSharedUsers).toBeChecked()
		const sendEventRemindersPush = TLUtils.getByLabelText(
			'Enable notifications for events via push',
		)
		expect(sendEventRemindersPush).toBeChecked()

		/*
		FIXME userEvent.click is broken with nextcloud-vue/Button

		await userEvent.click(sendInvitations)
		expect(sendInvitations).not.toBeChecked()
		expect(OCP.AppConfig.setValue).toHaveBeenCalledWith(
			'dav',
			'sendInvitations',
			'no'
		)
		OCP.AppConfig.setValue.mockClear()
		await userEvent.click(sendInvitations)
		expect(sendInvitations).toBeChecked()
		expect(OCP.AppConfig.setValue).toHaveBeenCalledWith(
			'dav',
			'sendInvitations',
			'yes'
		)

		axios.post.mockImplementationOnce((uri) => {
			expect(uri).toBe('/apps/dav/disableBirthdayCalendar')
			return Promise.resolve()
		})
		await userEvent.click(generateBirthdayCalendar)
		axios.post.mockImplementationOnce((uri) => {
			expect(uri).toBe('/apps/dav/enableBirthdayCalendar')
			return Promise.resolve()
		})
		await userEvent.click(generateBirthdayCalendar)
		expect(generateBirthdayCalendar).toBeEnabled()

		OCP.AppConfig.setValue.mockClear()
		await userEvent.click(sendEventReminders)
		expect(sendEventReminders).not.toBeChecked()
		expect(OCP.AppConfig.setValue).toHaveBeenCalledWith(
			'dav',
			'sendEventReminders',
			'no'
		)

		expect(sendEventRemindersToSharedUsers).toBeDisabled()
		expect(sendEventRemindersPush).toBeDisabled()

		OCP.AppConfig.setValue.mockClear()
		await userEvent.click(sendEventReminders)
		expect(sendEventReminders).toBeChecked()
		expect(OCP.AppConfig.setValue).toHaveBeenCalledWith(
			'dav',
			'sendEventReminders',
			'yes'
		)

		expect(sendEventRemindersToSharedUsers).toBeEnabled()
		expect(sendEventRemindersPush).toBeEnabled()
		*/
	})
})
ass="p">, private IURLGenerator $urlGenerator, private Defaults $defaults, private IThrottler $throttler, private IInitialState $initialState, private WebAuthnManager $webAuthnManager, private IManager $manager, private IL10N $l10n, private IAppManager $appManager, ) { parent::__construct($appName, $request); } /** * @return RedirectResponse */ #[NoAdminRequired] #[UseSession] #[FrontpageRoute(verb: 'GET', url: '/logout')] public function logout() { $loginToken = $this->request->getCookie('nc_token'); if (!is_null($loginToken)) { $this->config->deleteUserValue($this->userSession->getUser()->getUID(), 'login_token', $loginToken); } $this->userSession->logout(); $response = new RedirectResponse($this->urlGenerator->linkToRouteAbsolute( 'core.login.showLoginForm', ['clear' => true] // this param the code in login.js may be removed when the "Clear-Site-Data" is working in the browsers )); $this->session->set('clearingExecutionContexts', '1'); $this->session->close(); if ( $this->request->getServerProtocol() === 'https' && !$this->request->isUserAgent([Request::USER_AGENT_CHROME, Request::USER_AGENT_ANDROID_MOBILE_CHROME]) ) { $response->addHeader('Clear-Site-Data', '"cache", "storage"'); } return $response; } /** * @param string $user * @param string $redirect_url * * @return TemplateResponse|RedirectResponse */ #[NoCSRFRequired] #[PublicPage] #[UseSession] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'GET', url: '/login')] public function showLoginForm(?string $user = null, ?string $redirect_url = null): Http\Response { if ($this->userSession->isLoggedIn()) { return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl()); } $loginMessages = $this->session->get('loginMessages'); if (!$this->manager->isFairUseOfFreePushService()) { if (!is_array($loginMessages)) { $loginMessages = [[], []]; } $loginMessages[1][] = $this->l10n->t('This community release of Nextcloud is unsupported and push notifications are limited.'); } if (is_array($loginMessages)) { [$errors, $messages] = $loginMessages; $this->initialState->provideInitialState('loginMessages', $messages); $this->initialState->provideInitialState('loginErrors', $errors); } $this->session->remove('loginMessages'); if ($user !== null && $user !== '') { $this->initialState->provideInitialState('loginUsername', $user); } else { $this->initialState->provideInitialState('loginUsername', ''); } $this->initialState->provideInitialState( 'loginAutocomplete', $this->config->getSystemValue('login_form_autocomplete', true) === true ); if (!empty($redirect_url)) { [$url, ] = explode('?', $redirect_url); if ($url !== $this->urlGenerator->linkToRoute('core.login.logout')) { $this->initialState->provideInitialState('loginRedirectUrl', $redirect_url); } } $this->initialState->provideInitialState( 'loginThrottleDelay', $this->throttler->getDelay($this->request->getRemoteAddress()) ); $this->setPasswordResetInitialState($user); $this->setEmailStates(); $this->initialState->provideInitialState('webauthn-available', $this->webAuthnManager->isWebAuthnAvailable()); $this->initialState->provideInitialState('hideLoginForm', $this->config->getSystemValueBool('hide_login_form', false)); // OpenGraph Support: http://ogp.me/ Util::addHeader('meta', ['property' => 'og:title', 'content' => Util::sanitizeHTML($this->defaults->getName())]); Util::addHeader('meta', ['property' => 'og:description', 'content' => Util::sanitizeHTML($this->defaults->getSlogan())]); Util::addHeader('meta', ['property' => 'og:site_name', 'content' => Util::sanitizeHTML($this->defaults->getName())]); Util::addHeader('meta', ['property' => 'og:url', 'content' => $this->urlGenerator->getAbsoluteURL('/')]); Util::addHeader('meta', ['property' => 'og:type', 'content' => 'website']); Util::addHeader('meta', ['property' => 'og:image', 'content' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-touch.png'))]); $parameters = [ 'alt_login' => OC_App::getAlternativeLogIns(), 'pageTitle' => $this->l10n->t('Login'), ]; $this->initialState->provideInitialState('countAlternativeLogins', count($parameters['alt_login'])); $this->initialState->provideInitialState('alternativeLogins', $parameters['alt_login']); $this->initialState->provideInitialState('loginTimeout', $this->config->getSystemValueInt('login_form_timeout', 5 * 60)); return new TemplateResponse( $this->appName, 'login', $parameters, TemplateResponse::RENDER_AS_GUEST, ); } /** * Sets the password reset state * * @param string $username */ private function setPasswordResetInitialState(?string $username): void { if ($username !== null && $username !== '') { $user = $this->userManager->get($username); } else { $user = null; } $passwordLink = $this->config->getSystemValueString('lost_password_link', ''); $this->initialState->provideInitialState( 'loginResetPasswordLink', $passwordLink ); $this->initialState->provideInitialState( 'loginCanResetPassword', $this->canResetPassword($passwordLink, $user) ); } /** * Sets the initial state of whether or not a user is allowed to login with their email * initial state is passed in the array of 1 for email allowed and 0 for not allowed */ private function setEmailStates(): void { $emailStates = []; // true: can login with email, false otherwise - default to true // check if user_ldap is enabled, and the required classes exist if ($this->appManager->isAppLoaded('user_ldap') && class_exists(Helper::class)) { $helper = \OCP\Server::get(Helper::class); $allPrefixes = $helper->getServerConfigurationPrefixes(); // check each LDAP server the user is connected too foreach ($allPrefixes as $prefix) { $emailConfig = new Configuration($prefix); array_push($emailStates, $emailConfig->__get('ldapLoginFilterEmail')); } } $this->initialState->provideInitialState('emailStates', $emailStates); } /** * @param string|null $passwordLink * @param IUser|null $user * * Users may not change their passwords if: * - The account is disabled * - The backend doesn't support password resets * - The password reset function is disabled * * @return bool */ private function canResetPassword(?string $passwordLink, ?IUser $user): bool { if ($passwordLink === 'disabled') { return false; } if (!$passwordLink && $user !== null) { return $user->canChangePassword(); } if ($user !== null && $user->isEnabled() === false) { return false; } return true; } private function generateRedirect(?string $redirectUrl): RedirectResponse { if ($redirectUrl !== null && $this->userSession->isLoggedIn()) { $location = $this->urlGenerator->getAbsoluteURL($redirectUrl); // Deny the redirect if the URL contains a @ // This prevents unvalidated redirects like ?redirect_url=:user@domain.com if (!str_contains($location, '@')) { return new RedirectResponse($location); } } return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl()); } /** * @return RedirectResponse */ #[NoCSRFRequired] #[PublicPage] #[BruteForceProtection(action: 'login')] #[UseSession] #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'POST', url: '/login')] public function tryLogin(Chain $loginChain, string $user = '', string $password = '', ?string $redirect_url = null, string $timezone = '', string $timezone_offset = ''): RedirectResponse { if (!$this->request->passesCSRFCheck()) { if ($this->userSession->isLoggedIn()) { // If the user is already logged in and the CSRF check does not pass then // simply redirect the user to the correct page as required. This is the // case when a user has already logged-in, in another tab. return $this->generateRedirect($redirect_url); } // Clear any auth remnants like cookies to ensure a clean login // For the next attempt $this->userSession->logout(); return $this->createLoginFailedResponse( $user, $user, $redirect_url, self::LOGIN_MSG_CSRFCHECKFAILED, false, ); } $user = trim($user); if (strlen($user) > 255) { return $this->createLoginFailedResponse( $user, $user, $redirect_url, $this->l10n->t('Unsupported email length (>255)') ); } $data = new LoginData( $this->request, $user, $password, $redirect_url, $timezone, $timezone_offset ); $result = $loginChain->process($data); if (!$result->isSuccess()) { return $this->createLoginFailedResponse( $data->getUsername(), $user, $redirect_url, $result->getErrorMessage() ); } if ($result->getRedirectUrl() !== null) { return new RedirectResponse($result->getRedirectUrl()); } return $this->generateRedirect($redirect_url); } /** * Creates a login failed response. * * @param string $user * @param string $originalUser * @param string $redirect_url * @param string $loginMessage * * @return RedirectResponse */ private function createLoginFailedResponse( $user, $originalUser, $redirect_url, string $loginMessage, bool $throttle = true, ) { // Read current user and append if possible we need to // return the unmodified user otherwise we will leak the login name $args = $user !== null ? ['user' => $originalUser, 'direct' => 1] : []; if ($redirect_url !== null) { $args['redirect_url'] = $redirect_url; } $response = new RedirectResponse( $this->urlGenerator->linkToRoute('core.login.showLoginForm', $args) ); if ($throttle) { $response->throttle(['user' => substr($user, 0, 64)]); } $this->session->set('loginMessages', [ [$loginMessage], [] ]); return $response; } /** * Confirm the user password * * @license GNU AGPL version 3 or any later version * * @param string $password The password of the user * * @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, array{}> * * 200: Password confirmation succeeded * 403: Password confirmation failed */ #[NoAdminRequired] #[BruteForceProtection(action: 'sudo')] #[UseSession] #[NoCSRFRequired] #[FrontpageRoute(verb: 'POST', url: '/login/confirm')] public function confirmPassword(string $password): DataResponse { $loginName = $this->userSession->getLoginName(); $loginResult = $this->userManager->checkPassword($loginName, $password); if ($loginResult === false) { $response = new DataResponse([], Http::STATUS_FORBIDDEN); $response->throttle(['loginName' => $loginName]); return $response; } $confirmTimestamp = time(); $this->session->set('last-password-confirm', $confirmTimestamp); $this->throttler->resetDelay($this->request->getRemoteAddress(), 'sudo', ['loginName' => $loginName]); return new DataResponse(['lastLogin' => $confirmTimestamp], Http::STATUS_OK); } }