Compare commits

...

6 Commits

Author SHA1 Message Date
syuilo f195fa4ab9 2023.9.0-beta.10 2023-09-21 15:24:50 +09:00
syuilo 51c3ef5561 update deps 2023-09-21 15:24:47 +09:00
syuilo b654446f93 enhance(frontend): tweak ui 2023-09-21 15:14:08 +09:00
syuilo e41619775f feat: プロフィールでのリンク検証
Resolve #11099
2023-09-21 11:58:51 +09:00
syuilo 1250309a69 🎨 2023-09-21 10:29:40 +09:00
syuilo 6459eadcf1 update deps 2023-09-21 10:25:44 +09:00
19 changed files with 626 additions and 332 deletions

View File

@ -26,6 +26,7 @@
- Feat: 二要素認証のバックアップコードが生成されるようになりました
- ref. https://github.com/MisskeyIO/misskey/pull/121
- Feat: 二要素認証でパスキーをサポートするようになりました
- Feat: プロフィールでのリンク検証
- Feat: 通知をテストできるようになりました
- Feat: PWAのアイコンが設定できるようになりました
- Enhance: manifest.jsonをオーバーライド可能に
@ -62,6 +63,7 @@
- Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように試験的なためPlayなどには未実装
- Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善
- Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように
- Enhance: 細かなデザインの調整
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正

2
locales/index.d.ts vendored
View File

@ -1116,6 +1116,7 @@ export interface Locale {
"loadConversation": string;
"pinnedList": string;
"keepScreenOn": string;
"verifiedLink": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
@ -2020,6 +2021,7 @@ export interface Locale {
"metadataContent": string;
"changeAvatar": string;
"changeBanner": string;
"verifiedLinkDescription": string;
};
"_exportOrImport": {
"allNotes": string;

View File

@ -1113,6 +1113,7 @@ loadReplies: "返信を見る"
loadConversation: "会話を見る"
pinnedList: "ピン留めされたリスト"
keepScreenOn: "デバイスの画面を常にオンにする"
verifiedLink: "このリンク先の所有者であることが確認されました"
_announcement:
forExistingUsers: "既存ユーザーのみ"
@ -1935,6 +1936,7 @@ _profile:
metadataContent: "内容"
changeAvatar: "アイコン画像を変更"
changeBanner: "バナー画像を変更"
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
_exportOrImport:
allNotes: "全てのノート"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.9.0-beta.9",
"version": "2023.9.0-beta.10",
"codename": "nasubi",
"repository": {
"type": "git",
@ -47,7 +47,7 @@
"cssnano": "6.0.1",
"js-yaml": "4.1.0",
"postcss": "8.4.30",
"terser": "5.19.4",
"terser": "5.20.0",
"typescript": "5.2.2"
},
"devDependencies": {

View File

@ -0,0 +1,11 @@
export class VerifiedLinks1695260774117 {
name = 'VerifiedLinks1695260774117'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "verifiedLinks" character varying array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "verifiedLinks"`);
}
}

View File

@ -64,7 +64,7 @@
"@bull-board/ui": "5.8.4",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0",
"@fastify/cookie": "9.0.4",
"@fastify/cookie": "9.1.0",
"@fastify/cors": "8.4.0",
"@fastify/express": "2.3.0",
"@fastify/http-proxy": "9.2.1",
@ -86,7 +86,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.11.1",
"bullmq": "4.11.2",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -158,7 +158,7 @@
"systeminformation": "5.21.8",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.7",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",

View File

@ -384,6 +384,7 @@ export class UserEntityService implements OnModuleInit {
birthday: profile!.birthday,
lang: profile!.lang,
fields: profile!.fields,
verifiedLinks: profile!.verifiedLinks,
followersCount: followersCount ?? 0,
followingCount: followingCount ?? 0,
notesCount: user.notesCount,

View File

@ -48,6 +48,12 @@ export class MiUserProfile {
value: string;
}[];
@Column('varchar', {
array: true,
default: '{}',
})
public verifiedLinks: string[];
@Column('varchar', {
length: 32, nullable: true,
})

View File

@ -169,6 +169,15 @@ export const packedUserDetailedNotMeOnlySchema = {
},
},
},
verifiedLinks: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
format: 'url',
},
},
followersCount: {
type: 'number',
nullable: false, optional: false,

View File

@ -6,11 +6,13 @@
import RE2 from 're2';
import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import { notificationTypes } from '@/types.js';
@ -27,6 +29,9 @@ import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { Config } from '@/config.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
@ -37,6 +42,11 @@ export const meta = {
kind: 'write:account',
limit: {
duration: ms('1hour'),
max: 10,
},
errors: {
noSuchAvatar: {
message: 'No such avatar file.',
@ -173,6 +183,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -195,9 +208,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private hashtagService: HashtagService,
private roleService: RoleService,
private cacheService: CacheService,
private httpRequestService: HttpRequestService,
) {
super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
const isSecure = token == null;
const updates = {} as Partial<MiUser>;
@ -296,9 +310,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.fields) {
profileUpdates.fields = ps.fields
.filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
.filter(x => typeof x.name === 'string' && x.name.trim() !== '' && typeof x.value === 'string' && x.value.trim() !== '')
.map(x => {
return { name: x.name, value: x.value };
return { name: x.name.trim(), value: x.value.trim() };
});
}
@ -364,7 +378,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Object.keys(updates).includes('alsoKnownAs')) {
this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
}
if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
await this.userProfilesRepository.update(user.id, {
...profileUpdates,
verifiedLinks: [],
});
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
detail: true,
@ -386,7 +404,34 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// フォロワーにUpdateを配信
this.accountUpdateService.publishToFollowers(user.id);
const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://'));
for (const url of urls) {
this.verifyLink(url.value, user);
}
return iObj;
});
}
private async verifyLink(url: string, user: MiLocalUser) {
if (!safeForSql(url)) return;
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
const doc = window.document;
const myLink = `${this.config.url}/@${user.username}`;
const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink);
if (includesMyLink) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id })
.set({
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
})
.execute();
}
}
}

View File

@ -102,6 +102,7 @@ describe('ユーザー', () => {
birthday: user.birthday,
lang: user.lang,
fields: user.fields,
verifiedLinks: user.verifiedLinks,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
@ -369,6 +370,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.birthday, null);
assert.strictEqual(response.lang, null);
assert.deepStrictEqual(response.fields, []);
assert.deepStrictEqual(response.verifiedLinks, []);
assert.strictEqual(response.followersCount, 0);
assert.strictEqual(response.followingCount, 0);
assert.strictEqual(response.notesCount, 0);

View File

@ -89,6 +89,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
value: 'https://misskey-hub.net',
},
],
verifiedLinks: [],
followersCount: 1024,
followingCount: 16,
hasPendingFollowRequestFromYou: false,

View File

@ -59,13 +59,13 @@
"querystring": "0.2.1",
"rollup": "3.29.2",
"sanitize-html": "2.11.0",
"sass": "1.67.0",
"sass": "1.68.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.156.1",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.7",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typescript": "5.2.2",
@ -77,24 +77,24 @@
"vuedraggable": "next"
},
"devDependencies": {
"@storybook/addon-actions": "7.4.2",
"@storybook/addon-essentials": "7.4.2",
"@storybook/addon-interactions": "7.4.2",
"@storybook/addon-links": "7.4.2",
"@storybook/addon-storysource": "7.4.2",
"@storybook/addons": "7.4.2",
"@storybook/blocks": "7.4.2",
"@storybook/core-events": "7.4.2",
"@storybook/addon-actions": "7.4.3",
"@storybook/addon-essentials": "7.4.3",
"@storybook/addon-interactions": "7.4.3",
"@storybook/addon-links": "7.4.3",
"@storybook/addon-storysource": "7.4.3",
"@storybook/addons": "7.4.3",
"@storybook/blocks": "7.4.3",
"@storybook/core-events": "7.4.3",
"@storybook/jest": "0.2.2",
"@storybook/manager-api": "7.4.2",
"@storybook/preview-api": "7.4.2",
"@storybook/react": "7.4.2",
"@storybook/react-vite": "7.4.2",
"@storybook/manager-api": "7.4.3",
"@storybook/preview-api": "7.4.3",
"@storybook/react": "7.4.3",
"@storybook/react-vite": "7.4.3",
"@storybook/testing-library": "0.2.1",
"@storybook/theming": "7.4.2",
"@storybook/types": "7.4.2",
"@storybook/vue3": "7.4.2",
"@storybook/vue3-vite": "7.4.2",
"@storybook/theming": "7.4.3",
"@storybook/types": "7.4.3",
"@storybook/vue3": "7.4.3",
"@storybook/vue3-vite": "7.4.3",
"@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1",
"@types/estree": "1.0.1",

View File

@ -170,6 +170,10 @@ useTooltip(buttonEl, async (showing) => {
}
}
.icon {
max-width: 150px;
}
.count {
font-size: 0.7em;
line-height: 42px;

View File

@ -76,6 +76,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
</Sortable>
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
</div>
</MkFolder>
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
@ -119,6 +121,7 @@ import { langmap } from '@/scripts/langmap.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));

View File

@ -101,6 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</dt>
<dd class="value">
<Mfm :text="field.value" :author="user" :i="$i" :colored="false"/>
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
</dd>
</dl>
</div>
@ -671,7 +672,12 @@ onUnmounted(() => {
<style lang="scss" module>
.tl {
background: var(--bg);
border-radius: var(--radius);
overflow: clip;
border-radius: var(--radius);
overflow: clip;
}
.verifiedLink {
margin-left: 4px;
color: var(--success);
}
</style>

View File

@ -2778,6 +2778,7 @@ type UserDetailed = UserLite & {
name: string;
value: string;
}[];
verifiedLinks: string[];
followersCount: number;
followingCount: number;
hasPendingFollowRequestFromYou: boolean;

View File

@ -38,6 +38,7 @@ export type UserDetailed = UserLite & {
description: string | null;
ffVisibility: 'public' | 'followers' | 'private';
fields: {name: string; value: string}[];
verifiedLinks: string[];
followersCount: number;
followingCount: number;
hasPendingFollowRequestFromYou: boolean;

File diff suppressed because it is too large Load Diff