aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2025-02-05 23:48:15 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2025-02-06 11:58:24 +0100
commit460ceaac57343fcb6889a26c3d8fb94ee41a5d81 (patch)
treebfd61083b61a0831b42e2cc842947ee11a0d3060
parent2a0f81da53149ebd57f14cf1d697aded1481ef24 (diff)
downloadnextcloud-server-460ceaac57343fcb6889a26c3d8fb94ee41a5d81.tar.gz
nextcloud-server-460ceaac57343fcb6889a26c3d8fb94ee41a5d81.zip
fix(settings): Also sanitize fediverse and twitter handle in the frontend
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--apps/settings/src/components/PersonalInfo/FediverseSection.vue46
-rw-r--r--apps/settings/src/components/PersonalInfo/TwitterSection.vue35
-rw-r--r--apps/settings/src/constants/AccountPropertyConstants.ts (renamed from apps/settings/src/constants/AccountPropertyConstants.js)31
-rw-r--r--apps/settings/src/service/PersonalInfo/EmailService.js2
-rw-r--r--apps/settings/src/service/PersonalInfo/PersonalInfoService.js2
-rw-r--r--apps/settings/src/utils/validate.js2
-rw-r--r--cypress/e2e/settings/personal-info.cy.ts19
7 files changed, 82 insertions, 55 deletions
diff --git a/apps/settings/src/components/PersonalInfo/FediverseSection.vue b/apps/settings/src/components/PersonalInfo/FediverseSection.vue
index 9ba9c37ab80..65400ce0e45 100644
--- a/apps/settings/src/components/PersonalInfo/FediverseSection.vue
+++ b/apps/settings/src/components/PersonalInfo/FediverseSection.vue
@@ -4,30 +4,40 @@
-->
<template>
- <AccountPropertySection v-bind.sync="fediverse"
+ <AccountPropertySection v-bind.sync="value"
+ :readable="readable"
+ :on-validate="onValidate"
:placeholder="t('settings', 'Your handle')" />
</template>
-<script>
+<script setup lang="ts">
+import type { AccountProperties } from '../../constants/AccountPropertyConstants.js'
import { loadState } from '@nextcloud/initial-state'
-
-import AccountPropertySection from './shared/AccountPropertySection.vue'
-
+import { t } from '@nextcloud/l10n'
+import { ref } from 'vue'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
-const { fediverse } = loadState('settings', 'personalInfoParameters', {})
-
-export default {
- name: 'FediverseSection',
-
- components: {
- AccountPropertySection,
- },
+import AccountPropertySection from './shared/AccountPropertySection.vue'
- data() {
- return {
- fediverse: { ...fediverse, readable: NAME_READABLE_ENUM[fediverse.name] },
- }
- },
+const { fediverse } = loadState<AccountProperties>('settings', 'personalInfoParameters', {})
+
+const value = ref({ ...fediverse })
+const readable = NAME_READABLE_ENUM[fediverse.name]
+
+/**
+ * Validate a fediverse handle
+ * @param text The potential fediverse handle
+ */
+function onValidate(text: string): boolean {
+ const result = text.match(/^@?([^@/]+)@([^@/]+)$/)
+ if (result === null) {
+ return false
+ }
+
+ try {
+ return URL.parse(`https://${result[2]}/`) !== null
+ } catch {
+ return false
+ }
}
</script>
diff --git a/apps/settings/src/components/PersonalInfo/TwitterSection.vue b/apps/settings/src/components/PersonalInfo/TwitterSection.vue
index bb809c8d2b7..802f96087c2 100644
--- a/apps/settings/src/components/PersonalInfo/TwitterSection.vue
+++ b/apps/settings/src/components/PersonalInfo/TwitterSection.vue
@@ -4,30 +4,31 @@
-->
<template>
- <AccountPropertySection v-bind.sync="twitter"
+ <AccountPropertySection v-bind.sync="value"
+ :readable="readable"
+ :on-validate="onValidate"
:placeholder="t('settings', 'Your X (formerly Twitter) handle')" />
</template>
-<script>
-import { loadState } from '@nextcloud/initial-state'
+<script setup lang="ts">
+import type { AccountProperties } from '../../constants/AccountPropertyConstants.js'
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { ref } from 'vue'
+import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts'
import AccountPropertySection from './shared/AccountPropertySection.vue'
-import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
-
-const { twitter } = loadState('settings', 'personalInfoParameters', {})
-
-export default {
- name: 'TwitterSection',
+const { twitter } = loadState<AccountProperties>('settings', 'personalInfoParameters', {})
- components: {
- AccountPropertySection,
- },
+const value = ref({ ...twitter })
+const readable = NAME_READABLE_ENUM[twitter.name]
- data() {
- return {
- twitter: { ...twitter, readable: NAME_READABLE_ENUM[twitter.name] },
- }
- },
+/**
+ * Validate that the text might be a twitter handle
+ * @param text The potential twitter handle
+ */
+function onValidate(text: string): boolean {
+ return text.match(/^@?([a-zA-Z0-9_]{2,15})$/) !== null
}
</script>
diff --git a/apps/settings/src/constants/AccountPropertyConstants.js b/apps/settings/src/constants/AccountPropertyConstants.ts
index 4b475b54ab4..5ea15e05c6c 100644
--- a/apps/settings/src/constants/AccountPropertyConstants.js
+++ b/apps/settings/src/constants/AccountPropertyConstants.ts
@@ -114,12 +114,12 @@ export const ACCOUNT_SETTING_PROPERTY_READABLE_ENUM = Object.freeze({
})
/** Enum of scopes */
-export const SCOPE_ENUM = Object.freeze({
- PRIVATE: 'v2-private',
- LOCAL: 'v2-local',
- FEDERATED: 'v2-federated',
- PUBLISHED: 'v2-published',
-})
+export enum SCOPE_ENUM {
+ PRIVATE = 'v2-private',
+ LOCAL = 'v2-local',
+ FEDERATED = 'v2-federated',
+ PUBLISHED = 'v2-published',
+}
/** Enum of readable account properties to supported scopes */
export const PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM = Object.freeze({
@@ -193,11 +193,11 @@ export const SCOPE_PROPERTY_ENUM = Object.freeze({
export const DEFAULT_ADDITIONAL_EMAIL_SCOPE = SCOPE_ENUM.LOCAL
/** Enum of verification constants, according to IAccountManager */
-export const VERIFICATION_ENUM = Object.freeze({
- NOT_VERIFIED: 0,
- VERIFICATION_IN_PROGRESS: 1,
- VERIFIED: 2,
-})
+export enum VERIFICATION_ENUM {
+ NOT_VERIFIED = 0,
+ VERIFICATION_IN_PROGRESS = 1,
+ VERIFIED = 2,
+}
/**
* Email validation regex
@@ -206,3 +206,12 @@ export const VERIFICATION_ENUM = Object.freeze({
*/
// eslint-disable-next-line no-control-regex
export const VALIDATE_EMAIL_REGEX = /^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-+[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-+[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/i
+
+export interface IAccountProperty {
+ name: string
+ value: string
+ scope: SCOPE_ENUM
+ verified: VERIFICATION_ENUM
+}
+
+export type AccountProperties = Record<(typeof ACCOUNT_PROPERTY_ENUM)[keyof (typeof ACCOUNT_PROPERTY_ENUM)], IAccountProperty>
diff --git a/apps/settings/src/service/PersonalInfo/EmailService.js b/apps/settings/src/service/PersonalInfo/EmailService.js
index 52e5106328d..0adbe5225bc 100644
--- a/apps/settings/src/service/PersonalInfo/EmailService.js
+++ b/apps/settings/src/service/PersonalInfo/EmailService.js
@@ -8,7 +8,7 @@ import { generateOcsUrl } from '@nextcloud/router'
import { confirmPassword } from '@nextcloud/password-confirmation'
import axios from '@nextcloud/axios'
-import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.js'
+import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.ts'
import '@nextcloud/password-confirmation/dist/style.css'
diff --git a/apps/settings/src/service/PersonalInfo/PersonalInfoService.js b/apps/settings/src/service/PersonalInfo/PersonalInfoService.js
index 678fab628d3..f2eaac91301 100644
--- a/apps/settings/src/service/PersonalInfo/PersonalInfoService.js
+++ b/apps/settings/src/service/PersonalInfo/PersonalInfoService.js
@@ -8,7 +8,7 @@ import { generateOcsUrl } from '@nextcloud/router'
import { confirmPassword } from '@nextcloud/password-confirmation'
import axios from '@nextcloud/axios'
-import { SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.js'
+import { SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.ts'
import '@nextcloud/password-confirmation/dist/style.css'
diff --git a/apps/settings/src/utils/validate.js b/apps/settings/src/utils/validate.js
index d13ad52b026..0f76f4e6dc5 100644
--- a/apps/settings/src/utils/validate.js
+++ b/apps/settings/src/utils/validate.js
@@ -9,7 +9,7 @@
* TODO add nice validation errors for Profile page settings modal
*/
-import { VALIDATE_EMAIL_REGEX } from '../constants/AccountPropertyConstants.js'
+import { VALIDATE_EMAIL_REGEX } from '../constants/AccountPropertyConstants.ts'
/**
* Validate the email input
diff --git a/cypress/e2e/settings/personal-info.cy.ts b/cypress/e2e/settings/personal-info.cy.ts
index 940e3cd6449..80d2c87fe40 100644
--- a/cypress/e2e/settings/personal-info.cy.ts
+++ b/cypress/e2e/settings/personal-info.cy.ts
@@ -98,13 +98,19 @@ const checkSettingsVisibility = (property: string, defaultVisibility: Visibility
}) */
}
-const genericProperties = ['Location', 'X (formerly Twitter)', 'Fediverse']
+const genericProperties = [
+ ['Location', 'Berlin'],
+ ['X (formerly Twitter)', 'nextclouders'],
+ ['Fediverse', 'nextcloud@mastodon.xyz'],
+]
const nonfederatedProperties = ['Organisation', 'Role', 'Headline', 'About']
describe('Settings: Change personal information', { testIsolation: true }, () => {
let snapshot: string = ''
before(() => {
+ // make sure the fediverse check does not do http requests
+ cy.runOccCommand('config:system:set has_internet_connection --value false')
// ensure we can set locale and language
cy.runOccCommand('config:system:delete force_language')
cy.runOccCommand('config:system:delete force_locale')
@@ -125,6 +131,8 @@ describe('Settings: Change personal information', { testIsolation: true }, () =>
})
after(() => {
+ cy.runOccCommand('config:system:delete has_internet_connection')
+
cy.runOccCommand('config:system:set force_language --value en')
cy.runOccCommand('config:system:set force_locale --value en_US')
})
@@ -349,22 +357,21 @@ describe('Settings: Change personal information', { testIsolation: true }, () =>
})
// Check generic properties that allow any visibility and any value
- genericProperties.forEach((property) => {
+ genericProperties.forEach(([property, value]) => {
it(`Can set ${property} and change its visibility`, () => {
- const uniqueValue = `${property.toUpperCase()} ${property.toLowerCase()}`
cy.contains('label', property).scrollIntoView()
- inputForLabel(property).type(uniqueValue)
+ inputForLabel(property).type(value)
handlePasswordConfirmation(user.password)
cy.wait('@submitSetting')
cy.reload()
- inputForLabel(property).should('have.value', uniqueValue)
+ inputForLabel(property).should('have.value', value)
checkSettingsVisibility(property)
// check it is visible on the profile
cy.visit(`/u/${user.userId}`)
- cy.contains(uniqueValue).should('be.visible')
+ cy.contains(value).should('be.visible')
})
})