diff --git a/CHANGELOG.md b/CHANGELOG.md index 6258805f48..57072029b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように - Enhance: ウィジェットの設定項目のラベルの多言語対応 - Enhance: 画面幅が広いときにメディアを横並びで表示できるようにするオプションを追加 +- Enhance: 外部サイトへのリンクは移動の前に警告を表示するように + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/558 and https://github.com/MisskeyIO/misskey/commit/f7ec503b9ceb34d61a0dbd658858915eb7399c5d) - Enhance: パフォーマンスの向上 - Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061 - Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 45d2efdf35..2c137a74f2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1260,6 +1260,8 @@ useGroupedNotifications: "通知をグルーピング" emailVerificationFailedError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" doReaction: "リアクションする" +trustedLinkUrlPatterns: "外部サイトへのリンク警告 除外リスト" +trustedLinkUrlPatternsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。ドメイン名だけ書くと後方一致になります。" code: "コード" reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。" remainingN: "残り: {n}" @@ -1408,6 +1410,7 @@ frame: "フレーム" presets: "プリセット" zeroPadding: "ゼロ埋め" nothingToConfigure: "設定項目はありません" +open: "開く" _imageEditing: _vars: @@ -3201,6 +3204,11 @@ _urlPreviewSetting: summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。" summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。" +_externalNavigationWarning: + title: "外部サイトに移動します" + description: "{host}を離れて外部サイトに移動します" + trustThisDomain: "このデバイスで今後このドメインを信頼する" + _mediaControls: pip: "ピクチャインピクチャ" playbackRate: "再生速度" diff --git a/packages/backend/migration/1762172837314-external-website-warn.js b/packages/backend/migration/1762172837314-external-website-warn.js new file mode 100644 index 0000000000..3bae2285a1 --- /dev/null +++ b/packages/backend/migration/1762172837314-external-website-warn.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ExternalWebsiteWarn1762172837314 { + name = 'ExternalWebsiteWarn1762172837314' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "trustedLinkUrlPatterns" character varying(3072) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "trustedLinkUrlPatterns"`); + } +} diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 8e56ddbc02..a881ed175f 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -119,6 +119,7 @@ export class MetaEntityService { dayOfWeek: ad.dayOfWeek, isSensitive: ad.isSensitive ? true : undefined, })), + trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns, notesPerOneAd: instance.notesPerOneAd, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 620853450c..794f5b0eb8 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -639,6 +639,18 @@ export class MiMeta { }) public urlPreviewRequireContentLength: boolean; + /** + * An array of URL strings or regex that can be used to omit warnings about redirects to external sites. + * Separate them with spaces to specify AND, and enclose them with slashes to specify regular expressions. + * Each item is regarded as an OR. + */ + @Column('varchar', { + length: 3072, + array: true, + default: '{}', + }) + public trustedLinkUrlPatterns: string[]; + @Column('varchar', { length: 1024, nullable: true, diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 0c3ec141bc..1267753988 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -201,6 +201,14 @@ export const packedMetaLiteSchema = { }, }, }, + trustedLinkUrlPatterns: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, notesPerOneAd: { type: 'number', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 5beed3a7e8..597d1f7975 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -407,6 +407,14 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + trustedLinkUrlPatterns: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, backgroundImageUrl: { type: 'string', optional: false, nullable: true, @@ -730,6 +738,7 @@ export default class extends Endpoint { // eslint- perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, + trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns, enableReactionsBuffering: instance.enableReactionsBuffering, notesPerOneAd: instance.notesPerOneAd, summalyProxy: instance.urlPreviewSummaryProxyUrl, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 7a8dfc4555..1907a39e25 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -178,6 +178,11 @@ export const paramDef = { type: 'string', nullable: true, description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', }, + trustedLinkUrlPatterns: { + type: 'array', nullable: true, items: { + type: 'string', + }, + }, urlPreviewEnabled: { type: 'boolean' }, urlPreviewAllowRedirect: { type: 'boolean' }, urlPreviewTimeout: { type: 'integer' }, @@ -267,6 +272,11 @@ export default class extends Endpoint { // eslint- return h !== '' && h !== lv && !set.blockedHosts?.includes(h); }); } + + if (Array.isArray(ps.trustedLinkUrlPatterns)) { + set.trustedLinkUrlPatterns = ps.trustedLinkUrlPatterns.filter(Boolean); + } + if (Array.isArray(ps.mediaSilencedHosts)) { let lastValue = ''; set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => { @@ -275,6 +285,7 @@ export default class extends Endpoint { // eslint- return h !== '' && h !== lv && !set.blockedHosts?.includes(h); }); } + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 163f172f57..dd85c1c605 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="maybeRelativeUrl" :rel="rel ?? 'nofollow noopener'" :target="target" :behavior="props.navigationBehavior" :title="url" + @click="(ev: MouseEvent) => warningExternalWebsite(ev, props.url)" > @@ -22,18 +23,20 @@ import type { MkABehavior } from '@/components/global/MkA.vue'; import { useTooltip } from '@/composables/use-tooltip.js'; import * as os from '@/os.js'; import { isEnabledUrlPreview } from '@/utility/url-preview.js'; +import { warningExternalWebsite } from '@/utility/warning-external-website.js'; const props = withDefaults(defineProps<{ url: string; rel?: null | string; navigationBehavior?: MkABehavior; }>(), { + rel: 'nofollow noopener', }); const maybeRelativeUrl = maybeMakeRelative(props.url, local); const self = maybeRelativeUrl !== props.url; const attr = self ? 'to' : 'href'; -const target = self ? null : '_blank'; +const target = self ? undefined : '_blank'; const el = ref(); diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 7c0c06398b..3c136b1937 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -44,7 +44,15 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -94,6 +102,7 @@ import MkButton from '@/components/MkButton.vue'; import { transformPlayerUrl } from '@/utility/url-preview.js'; import { store } from '@/store.js'; import { prefer } from '@/preferences.js'; +import { warningExternalWebsite } from '@/utility/warning-external-website.js'; import { maybeMakeRelative } from '@@/js/url.js'; type SummalyResult = Awaited>; diff --git a/packages/frontend/src/components/MkUrlWarningDialog.vue b/packages/frontend/src/components/MkUrlWarningDialog.vue new file mode 100644 index 0000000000..b6efc7be47 --- /dev/null +++ b/packages/frontend/src/components/MkUrlWarningDialog.vue @@ -0,0 +1,141 @@ + + + + + + + + + diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 159af6f11e..0a2018cd10 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="maybeRelativeUrl" :rel="rel ?? 'nofollow noopener'" :target="target" :behavior="props.navigationBehavior" @contextmenu.stop="() => {}" + @click="(ev: MouseEvent) => warningExternalWebsite(ev, props.url)" >