Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix(settings): Also sanitize fediverse and twitter handle in the fron…
…tend

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Feb 7, 2025
commit e046ce6858065698cd17f8e649ae6ae74a589685
46 changes: 28 additions & 18 deletions apps/settings/src/components/PersonalInfo/FediverseSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
35 changes: 18 additions & 17 deletions apps/settings/src/components/PersonalInfo/TwitterSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,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({
Expand Down Expand Up @@ -188,11 +188,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
Expand All @@ -201,3 +201,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>
2 changes: 1 addition & 1 deletion apps/settings/src/service/PersonalInfo/EmailService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
2 changes: 1 addition & 1 deletion apps/settings/src/utils/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions cypress/e2e/settings/personal-info.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,26 @@ const checkSettingsVisibility = (property: string, defaultVisibility: Visibility
}) */
}

const genericProperties = ['Location', 'X (formerly Twitter)', 'Fediverse']
const genericProperties = [
['Location', 'Berlin'],
['X (formerly Twitter)', 'nextclouders'],
['Fediverse', '[email protected]'],
]
const nonfederatedProperties = ['Organisation', 'Role', 'Headline', 'About']

describe('Settings: Change personal information', { testIsolation: true }, () => {

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')
})

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')
})
Expand Down Expand Up @@ -333,22 +341,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')
})
})

Expand Down