Skip to content

Commit 87c0e74

Browse files
Merge pull request #50698 from nextcloud/backport/50678/stable29
[stable29] fix(AccountProperty): better validation of twitter and fediverse handles
2 parents 48ea49b + 43b95e1 commit 87c0e74

22 files changed

Lines changed: 655 additions & 196 deletions

apps/settings/lib/Controller/UsersController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ protected function canAdminChangeUserPasswords(): bool {
304304
* @NoAdminRequired
305305
* @NoSubAdminRequired
306306
* @PasswordConfirmationRequired
307+
* @UserRateThrottle(limit=50, period=600)
307308
*
308309
* @param string|null $avatarScope
309310
* @param string|null $displayname

apps/settings/src/components/PersonalInfo/FediverseSection.vue

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,40 @@
2121
-->
2222

2323
<template>
24-
<AccountPropertySection v-bind.sync="fediverse"
24+
<AccountPropertySection v-bind.sync="value"
25+
:readable="readable"
26+
:on-validate="onValidate"
2527
:placeholder="t('settings', 'Your handle')" />
2628
</template>
2729

28-
<script>
30+
<script setup lang="ts">
31+
import type { AccountProperties } from '../../constants/AccountPropertyConstants.js'
2932
import { loadState } from '@nextcloud/initial-state'
30-
31-
import AccountPropertySection from './shared/AccountPropertySection.vue'
32-
33+
import { t } from '@nextcloud/l10n'
34+
import { ref } from 'vue'
3335
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
3436
35-
const { fediverse } = loadState('settings', 'personalInfoParameters', {})
36-
37-
export default {
38-
name: 'FediverseSection',
39-
40-
components: {
41-
AccountPropertySection,
42-
},
37+
import AccountPropertySection from './shared/AccountPropertySection.vue'
4338
44-
data() {
45-
return {
46-
fediverse: { ...fediverse, readable: NAME_READABLE_ENUM[fediverse.name] },
47-
}
48-
},
39+
const { fediverse } = loadState<AccountProperties>('settings', 'personalInfoParameters', {})
40+
41+
const value = ref({ ...fediverse })
42+
const readable = NAME_READABLE_ENUM[fediverse.name]
43+
44+
/**
45+
* Validate a fediverse handle
46+
* @param text The potential fediverse handle
47+
*/
48+
function onValidate(text: string): boolean {
49+
const result = text.match(/^@?([^@/]+)@([^@/]+)$/)
50+
if (result === null) {
51+
return false
52+
}
53+
54+
try {
55+
return URL.parse(`https://${result[2]}/`) !== null
56+
} catch {
57+
return false
58+
}
4959
}
5060
</script>

apps/settings/src/components/PersonalInfo/TwitterSection.vue

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,31 @@
2121
-->
2222

2323
<template>
24-
<AccountPropertySection v-bind.sync="twitter"
24+
<AccountPropertySection v-bind.sync="value"
25+
:readable="readable"
26+
:on-validate="onValidate"
2527
:placeholder="t('settings', 'Your X (formerly Twitter) handle')" />
2628
</template>
2729

28-
<script>
29-
import { loadState } from '@nextcloud/initial-state'
30+
<script setup lang="ts">
31+
import type { AccountProperties } from '../../constants/AccountPropertyConstants.js'
3032
33+
import { loadState } from '@nextcloud/initial-state'
34+
import { t } from '@nextcloud/l10n'
35+
import { ref } from 'vue'
36+
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts'
3137
import AccountPropertySection from './shared/AccountPropertySection.vue'
3238
33-
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
34-
35-
const { twitter } = loadState('settings', 'personalInfoParameters', {})
36-
37-
export default {
38-
name: 'TwitterSection',
39+
const { twitter } = loadState<AccountProperties>('settings', 'personalInfoParameters', {})
3940
40-
components: {
41-
AccountPropertySection,
42-
},
41+
const value = ref({ ...twitter })
42+
const readable = NAME_READABLE_ENUM[twitter.name]
4343
44-
data() {
45-
return {
46-
twitter: { ...twitter, readable: NAME_READABLE_ENUM[twitter.name] },
47-
}
48-
},
44+
/**
45+
* Validate that the text might be a twitter handle
46+
* @param text The potential twitter handle
47+
*/
48+
function onValidate(text: string): boolean {
49+
return text.match(/^@?([a-zA-Z0-9_]{2,15})$/) !== null
4950
}
5051
</script>

apps/settings/src/constants/AccountPropertyConstants.js renamed to apps/settings/src/constants/AccountPropertyConstants.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ export const ACCOUNT_SETTING_PROPERTY_READABLE_ENUM = Object.freeze({
121121
})
122122

123123
/** Enum of scopes */
124-
export const SCOPE_ENUM = Object.freeze({
125-
PRIVATE: 'v2-private',
126-
LOCAL: 'v2-local',
127-
FEDERATED: 'v2-federated',
128-
PUBLISHED: 'v2-published',
129-
})
124+
export enum SCOPE_ENUM {
125+
PRIVATE = 'v2-private',
126+
LOCAL = 'v2-local',
127+
FEDERATED = 'v2-federated',
128+
PUBLISHED = 'v2-published',
129+
}
130130

131131
/** Enum of readable account properties to supported scopes */
132132
export const PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM = Object.freeze({
@@ -197,11 +197,11 @@ export const SCOPE_PROPERTY_ENUM = Object.freeze({
197197
export const DEFAULT_ADDITIONAL_EMAIL_SCOPE = SCOPE_ENUM.LOCAL
198198

199199
/** Enum of verification constants, according to IAccountManager */
200-
export const VERIFICATION_ENUM = Object.freeze({
201-
NOT_VERIFIED: 0,
202-
VERIFICATION_IN_PROGRESS: 1,
203-
VERIFIED: 2,
204-
})
200+
export enum VERIFICATION_ENUM {
201+
NOT_VERIFIED = 0,
202+
VERIFICATION_IN_PROGRESS = 1,
203+
VERIFIED = 2,
204+
}
205205

206206
/**
207207
* Email validation regex
@@ -210,3 +210,12 @@ export const VERIFICATION_ENUM = Object.freeze({
210210
*/
211211
// eslint-disable-next-line no-control-regex
212212
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
213+
214+
export interface IAccountProperty {
215+
name: string
216+
value: string
217+
scope: SCOPE_ENUM
218+
verified: VERIFICATION_ENUM
219+
}
220+
221+
export type AccountProperties = Record<(typeof ACCOUNT_PROPERTY_ENUM)[keyof (typeof ACCOUNT_PROPERTY_ENUM)], IAccountProperty>

apps/settings/src/service/PersonalInfo/EmailService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { generateOcsUrl } from '@nextcloud/router'
2626
import { confirmPassword } from '@nextcloud/password-confirmation'
2727
import '@nextcloud/password-confirmation/dist/style.css'
2828

29-
import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.js'
29+
import { ACCOUNT_PROPERTY_ENUM, SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.ts'
3030

3131
/**
3232
* Save the primary email of the user

apps/settings/src/service/PersonalInfo/PersonalInfoService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { generateOcsUrl } from '@nextcloud/router'
2626
import { confirmPassword } from '@nextcloud/password-confirmation'
2727
import '@nextcloud/password-confirmation/dist/style.css'
2828

29-
import { SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.js'
29+
import { SCOPE_SUFFIX } from '../../constants/AccountPropertyConstants.ts'
3030

3131
/**
3232
* Save the primary account property value for the user

apps/settings/src/utils/validate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* TODO add nice validation errors for Profile page settings modal
2727
*/
2828

29-
import { VALIDATE_EMAIL_REGEX } from '../constants/AccountPropertyConstants.js'
29+
import { VALIDATE_EMAIL_REGEX } from '../constants/AccountPropertyConstants.ts'
3030

3131
/**
3232
* Validate the email input

apps/settings/tests/UserMigration/AccountMigratorTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use OCP\Accounts\IAccountManager;
3232
use OCP\AppFramework\App;
3333
use OCP\IAvatarManager;
34+
use OCP\IConfig;
3435
use OCP\IUserManager;
3536
use OCP\UserMigration\IExportDestination;
3637
use OCP\UserMigration\IImportSource;
@@ -69,8 +70,11 @@ class AccountMigratorTest extends TestCase {
6970
private const REGEX_CONFIG_FILE = '/^' . Application::APP_ID . '\/' . '[a-z]+\.json' . '$/';
7071

7172
protected function setUp(): void {
73+
parent::setUp();
74+
7275
$app = new App(Application::APP_ID);
7376
$container = $app->getContainer();
77+
$container->get(IConfig::class)->setSystemValue('has_internet_connection', false);
7478

7579
$this->userManager = $container->get(IUserManager::class);
7680
$this->avatarManager = $container->get(IAvatarManager::class);
@@ -81,6 +85,11 @@ protected function setUp(): void {
8185
$this->output = $this->createMock(OutputInterface::class);
8286
}
8387

88+
protected function tearDown(): void {
89+
\OCP\Server::get(IConfig::class)->setSystemValue('has_internet_connection', true);
90+
parent::tearDown();
91+
}
92+
8493
public function dataImportExportAccount(): array {
8594
return array_map(
8695
function (string $filename) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"displayname":{"name":"displayname","value":"Steve Smith","scope":"v2-local","verified":"0","verificationData":""},"address":{"name":"address","value":"123 Water St","scope":"v2-local","verified":"0","verificationData":""},"website":{"name":"website","value":"https:\/\/example.org","scope":"v2-local","verified":"0","verificationData":""},"email":{"name":"email","value":"steve@example.org","scope":"v2-federated","verified":"0","verificationData":""},"avatar":{"name":"avatar","value":"","scope":"v2-local","verified":"0","verificationData":""},"phone":{"name":"phone","value":"+12178515387","scope":"v2-private","verified":"0","verificationData":""},"twitter":{"name":"twitter","value":"steve","scope":"v2-federated","verified":"0","verificationData":""},"fediverse":{"name":"fediverse","value":"@steve@floss.social","scope":"v2-federated","verified":"0","verificationData":""},"organisation":{"name":"organisation","value":"Mytery Machine","scope":"v2-private","verified":"0","verificationData":""},"role":{"name":"role","value":"Manager","scope":"v2-private","verified":"0","verificationData":""},"headline":{"name":"headline","value":"I am Steve","scope":"v2-local","verified":"0","verificationData":""},"biography":{"name":"biography","value":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris porttitor ullamcorper dictum. Sed fermentum ut ligula scelerisque semper. Aliquam interdum convallis tellus eu dapibus. Integer in justo sollicitudin, hendrerit ligula sit amet, blandit sem.\n\nSuspendisse consectetur ultrices accumsan. Quisque sagittis bibendum lectus ut placerat. Mauris tincidunt ornare neque, et pulvinar tortor porttitor eu.","scope":"v2-local","verified":"0","verificationData":""},"profile_enabled":{"name":"profile_enabled","value":"1","scope":"v2-local","verified":"0","verificationData":""},"additional_mail":[{"name":"additional_mail","value":"steve@example.com","scope":"v2-published","verified":"0","verificationData":""},{"name":"additional_mail","value":"steve@earth.world","scope":"v2-local","verified":"0","verificationData":""}]}
1+
{"displayname":{"name":"displayname","value":"Steve Smith","scope":"v2-local","verified":"0","verificationData":""},"address":{"name":"address","value":"123 Water St","scope":"v2-local","verified":"0","verificationData":""},"website":{"name":"website","value":"https:\/\/example.org","scope":"v2-local","verified":"0","verificationData":""},"email":{"name":"email","value":"steve@example.org","scope":"v2-federated","verified":"0","verificationData":""},"avatar":{"name":"avatar","value":"","scope":"v2-local","verified":"0","verificationData":""},"phone":{"name":"phone","value":"+12178515387","scope":"v2-private","verified":"0","verificationData":""},"twitter":{"name":"twitter","value":"steve","scope":"v2-federated","verified":"0","verificationData":""},"fediverse":{"name":"fediverse","value":"steve@floss.social","scope":"v2-federated","verified":"0","verificationData":""},"organisation":{"name":"organisation","value":"Mytery Machine","scope":"v2-private","verified":"0","verificationData":""},"role":{"name":"role","value":"Manager","scope":"v2-private","verified":"0","verificationData":""},"headline":{"name":"headline","value":"I am Steve","scope":"v2-local","verified":"0","verificationData":""},"biography":{"name":"biography","value":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris porttitor ullamcorper dictum. Sed fermentum ut ligula scelerisque semper. Aliquam interdum convallis tellus eu dapibus. Integer in justo sollicitudin, hendrerit ligula sit amet, blandit sem.\n\nSuspendisse consectetur ultrices accumsan. Quisque sagittis bibendum lectus ut placerat. Mauris tincidunt ornare neque, et pulvinar tortor porttitor eu.","scope":"v2-local","verified":"0","verificationData":""},"profile_enabled":{"name":"profile_enabled","value":"1","scope":"v2-local","verified":"0","verificationData":""},"additional_mail":[{"name":"additional_mail","value":"steve@example.com","scope":"v2-published","verified":"0","verificationData":""},{"name":"additional_mail","value":"steve@earth.world","scope":"v2-local","verified":"0","verificationData":""}]}

build/integration/features/bootstrap/BasicStructure.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ public function sendingTo($verb, $url) {
147147
* @return string
148148
*/
149149
public function getOCSResponse($response) {
150-
return simplexml_load_string($response->getBody())->meta[0]->statuscode;
150+
$body = simplexml_load_string((string)$response->getBody());
151+
if ($body === false) {
152+
throw new \RuntimeException('Could not parse OCS response, body is not valid XML');
153+
}
154+
return $body->meta[0]->statuscode;
151155
}
152156

153157
/**

0 commit comments

Comments
 (0)