diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-02-05 23:48:15 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-02-06 11:58:24 +0100 |
commit | 460ceaac57343fcb6889a26c3d8fb94ee41a5d81 (patch) | |
tree | bfd61083b61a0831b42e2cc842947ee11a0d3060 | |
parent | 2a0f81da53149ebd57f14cf1d697aded1481ef24 (diff) | |
download | nextcloud-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.vue | 46 | ||||
-rw-r--r-- | apps/settings/src/components/PersonalInfo/TwitterSection.vue | 35 | ||||
-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.js | 2 | ||||
-rw-r--r-- | apps/settings/src/service/PersonalInfo/PersonalInfoService.js | 2 | ||||
-rw-r--r-- | apps/settings/src/utils/validate.js | 2 | ||||
-rw-r--r-- | cypress/e2e/settings/personal-info.cy.ts | 19 |
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') }) }) |