upd&merge: Merge Cherrypick/MisskeyIO's external url popup, delete old popup warning and modify script to handle undefined domains
This commit is contained in:
parent
ac1e5a0fb5
commit
d5b372f7a9
|
@ -61,6 +61,9 @@ copyNoteId: "Copy note ID"
|
||||||
copyFileId: "Copy file ID"
|
copyFileId: "Copy file ID"
|
||||||
copyFolderId: "Copy folder ID"
|
copyFolderId: "Copy folder ID"
|
||||||
copyProfileUrl: "Copy profile URL"
|
copyProfileUrl: "Copy profile URL"
|
||||||
|
trustedLinkUrlPatterns: "Link to external site warning exclusion list"
|
||||||
|
trustedLinkUrlPatternsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition. Using surrounding keywords with slashes will turn them into a regular expression. If you write only the domain name, it will be a backward match."
|
||||||
|
open: "Open"
|
||||||
searchUser: "Search for a user"
|
searchUser: "Search for a user"
|
||||||
searchThisUsersNotes: "Search this user’s notes"
|
searchThisUsersNotes: "Search this user’s notes"
|
||||||
reply: "Reply"
|
reply: "Reply"
|
||||||
|
@ -291,7 +294,6 @@ removeAreYouSure: "Are you sure that you want to remove \"{x}\"?"
|
||||||
deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?"
|
deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?"
|
||||||
resetAreYouSure: "Really reset?"
|
resetAreYouSure: "Really reset?"
|
||||||
areYouSure: "Are you sure?"
|
areYouSure: "Are you sure?"
|
||||||
confirmRemoteUrl: "Are you sure that you want to go to \"{x}\"?"
|
|
||||||
saved: "Saved"
|
saved: "Saved"
|
||||||
messaging: "Chat"
|
messaging: "Chat"
|
||||||
upload: "Upload"
|
upload: "Upload"
|
||||||
|
@ -2827,3 +2829,7 @@ _contextMenu:
|
||||||
app: "Application"
|
app: "Application"
|
||||||
appWithShift: "Application with shift key"
|
appWithShift: "Application with shift key"
|
||||||
native: "Native"
|
native: "Native"
|
||||||
|
_externalNavigationWarning:
|
||||||
|
title: "Navigate to an external site"
|
||||||
|
description: "Leave {host} and go to an external site"
|
||||||
|
trustThisDomain: "Trust this domain on this device in the future"
|
||||||
|
|
|
@ -260,6 +260,18 @@ export interface Locale extends ILocale {
|
||||||
* プロフィールURLをコピー
|
* プロフィールURLをコピー
|
||||||
*/
|
*/
|
||||||
"copyProfileUrl": string;
|
"copyProfileUrl": string;
|
||||||
|
/**
|
||||||
|
* 外部サイトへのリンク警告 除外リスト
|
||||||
|
*/
|
||||||
|
"trustedLinkUrlPatterns": string;
|
||||||
|
/**
|
||||||
|
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。ドメイン名だけ書くと後方一致になります。
|
||||||
|
*/
|
||||||
|
"trustedLinkUrlPatternsDescription": string;
|
||||||
|
/**
|
||||||
|
* 開く
|
||||||
|
*/
|
||||||
|
"open": string;
|
||||||
/**
|
/**
|
||||||
* ユーザーを検索
|
* ユーザーを検索
|
||||||
*/
|
*/
|
||||||
|
@ -3128,6 +3140,10 @@ export interface Locale extends ILocale {
|
||||||
* 返信にサーバー情報を表示する
|
* 返信にサーバー情報を表示する
|
||||||
*/
|
*/
|
||||||
"showTickerOnReplies": string;
|
"showTickerOnReplies": string;
|
||||||
|
/**
|
||||||
|
* 猫の話し方を無効にする
|
||||||
|
*/
|
||||||
|
"disableCatSpeak": string;
|
||||||
/**
|
/**
|
||||||
* 検索MFMの検索エンジン
|
* 検索MFMの検索エンジン
|
||||||
*/
|
*/
|
||||||
|
@ -4429,10 +4445,6 @@ export interface Locale extends ILocale {
|
||||||
* 連合なしにする
|
* 連合なしにする
|
||||||
*/
|
*/
|
||||||
"disableFederationOk": string;
|
"disableFederationOk": string;
|
||||||
/**
|
|
||||||
* 猫の話し方を無効にする
|
|
||||||
*/
|
|
||||||
"disableCatSpeak": string;
|
|
||||||
/**
|
/**
|
||||||
* 現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。
|
* 現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。
|
||||||
*/
|
*/
|
||||||
|
@ -5777,7 +5789,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"social": string;
|
"social": string;
|
||||||
/**
|
/**
|
||||||
* バッッブルタイムラインでは、管理者が選択した接続サーバーからのメモを表示できます。
|
* バブルタイムラインでは、管理者が選択した接続サーバーからの投稿を表示できます。
|
||||||
*/
|
*/
|
||||||
"bubble": string;
|
"bubble": string;
|
||||||
/**
|
/**
|
||||||
|
@ -9139,7 +9151,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"global": string;
|
"global": string;
|
||||||
/**
|
/**
|
||||||
* バッッブル
|
* バブル
|
||||||
*/
|
*/
|
||||||
"bubble": string;
|
"bubble": string;
|
||||||
};
|
};
|
||||||
|
@ -10913,6 +10925,20 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"native": string;
|
"native": string;
|
||||||
};
|
};
|
||||||
|
"_externalNavigationWarning": {
|
||||||
|
/**
|
||||||
|
* 外部サイトに移動します
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* {host}を離れて外部サイトに移動します
|
||||||
|
*/
|
||||||
|
"description": ParameterizedString<"host">;
|
||||||
|
/**
|
||||||
|
* このデバイスで今後このドメインを信頼する
|
||||||
|
*/
|
||||||
|
"trustThisDomain": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -61,6 +61,9 @@ copyNoteId: "ノートIDをコピー"
|
||||||
copyFileId: "ファイルIDをコピー"
|
copyFileId: "ファイルIDをコピー"
|
||||||
copyFolderId: "フォルダーIDをコピー"
|
copyFolderId: "フォルダーIDをコピー"
|
||||||
copyProfileUrl: "プロフィールURLをコピー"
|
copyProfileUrl: "プロフィールURLをコピー"
|
||||||
|
trustedLinkUrlPatterns: "外部サイトへのリンク警告 除外リスト"
|
||||||
|
trustedLinkUrlPatternsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。ドメイン名だけ書くと後方一致になります。"
|
||||||
|
open: "開く"
|
||||||
searchUser: "ユーザーを検索"
|
searchUser: "ユーザーを検索"
|
||||||
searchThisUsersNotes: "ユーザーのノートを検索"
|
searchThisUsersNotes: "ユーザーのノートを検索"
|
||||||
reply: "返信"
|
reply: "返信"
|
||||||
|
@ -2902,3 +2905,8 @@ _contextMenu:
|
||||||
app: "アプリケーション"
|
app: "アプリケーション"
|
||||||
appWithShift: "Shiftキーでアプリケーション"
|
appWithShift: "Shiftキーでアプリケーション"
|
||||||
native: "ブラウザのUI"
|
native: "ブラウザのUI"
|
||||||
|
|
||||||
|
_externalNavigationWarning:
|
||||||
|
title: "外部サイトに移動します"
|
||||||
|
description: "{host}を離れて外部サイトに移動します"
|
||||||
|
trustThisDomain: "このデバイスで今後このドメインを信頼する"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ExternalWebsiteWarn1711008460816 {
|
||||||
|
name = 'ExternalWebsiteWarn1711008460816'
|
||||||
|
|
||||||
|
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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,6 +120,7 @@ export class MetaEntityService {
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
})),
|
})),
|
||||||
|
trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
|
||||||
notesPerOneAd: instance.notesPerOneAd,
|
notesPerOneAd: instance.notesPerOneAd,
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
|
|
|
@ -674,4 +674,12 @@ export class MiMeta {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public urlPreviewUserAgent: string | null;
|
public urlPreviewUserAgent: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 3072,
|
||||||
|
array: true,
|
||||||
|
default: '{}',
|
||||||
|
comment: '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.',
|
||||||
|
})
|
||||||
|
public trustedLinkUrlPatterns: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,6 +273,14 @@ export const packedMetaLiteSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
default: 'local',
|
default: 'local',
|
||||||
},
|
},
|
||||||
|
trustedLinkUrlPatterns: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -526,6 +526,14 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
trustedLinkUrlPatterns: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -669,6 +677,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||||
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||||
|
trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,6 +176,11 @@ export const paramDef = {
|
||||||
urlPreviewRequireContentLength: { type: 'boolean' },
|
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||||
urlPreviewUserAgent: { type: 'string', nullable: true },
|
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||||
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||||
|
trustedLinkUrlPatterns: {
|
||||||
|
type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -665,6 +670,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ps.trustedLinkUrlPatterns)) {
|
||||||
|
set.trustedLinkUrlPatterns = ps.trustedLinkUrlPatterns.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
|
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
|
||||||
:behavior="props.navigationBehavior"
|
:behavior="props.navigationBehavior"
|
||||||
:title="url"
|
:title="url"
|
||||||
@click.prevent="self ? true : promptConfirm()"
|
@click.prevent="self ? true : warningExternalWebsite(url)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -23,7 +23,7 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -49,16 +49,6 @@ if (isEnabledUrlPreview.value) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptConfirm() {
|
|
||||||
const { canceled } = await os.confirm({
|
|
||||||
type: 'question',
|
|
||||||
text: i18n.tsx.confirmRemoteUrl({ x: props.url }),
|
|
||||||
plain: true,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
window.open(props.url, '_blank', 'nofollow noopener popup=false');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||||
|
<div :class="$style.root" class="_gaps">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<div :class="$style.icon">
|
||||||
|
<i class="ti ti-alert-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.title">{{ i18n.ts._externalNavigationWarning.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div><Mfm :text="i18n.tsx._externalNavigationWarning.description({ host: instanceName })"/></div>
|
||||||
|
<div class="_monospace" :class="$style.urlAddress">{{ url }}</div>
|
||||||
|
<div>
|
||||||
|
<MkSwitch v-model="trustThisDomain">{{ i18n.ts._externalNavigationWarning.trustThisDomain }}</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.buttons">
|
||||||
|
<MkButton data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||||
|
<MkButton data-cy-modal-dialog-ok inline primary rounded @click="ok"><i class="ti ti-external-link"></i> {{ i18n.ts.open }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
|
||||||
|
import { instanceName } from '@/config.js';
|
||||||
|
import MkModal from '@/components/MkModal.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
type Result = string | number | true | null;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
const trustThisDomain = ref(false);
|
||||||
|
|
||||||
|
const domain = computed(() => new URL(props.url).hostname);
|
||||||
|
|
||||||
|
// overload function を使いたいので lint エラーを無視する
|
||||||
|
function done(canceled: true): void;
|
||||||
|
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||||
|
function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
|
||||||
|
emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
|
||||||
|
modal.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ok() {
|
||||||
|
const result = true;
|
||||||
|
if (!defaultStore.state.trustedDomains.includes(domain.value) && trustThisDomain.value) {
|
||||||
|
await defaultStore.set('trustedDomains', defaultStore.state.trustedDomains.concat(domain.value));
|
||||||
|
}
|
||||||
|
done(false, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
done(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(evt: KeyboardEvent) {
|
||||||
|
if (evt.key === 'Escape') cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
padding: 32px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urlAddress {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--divider);
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
|
<MkTextarea v-model="trustedLinkUrlPatterns">
|
||||||
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.trustedLinkUrlPatterns }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
|
|
||||||
<MkTextarea v-model="sensitiveWords">
|
<MkTextarea v-model="sensitiveWords">
|
||||||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||||
|
@ -105,6 +111,7 @@ const bubbleTimeline = ref<string>('');
|
||||||
const tosUrl = ref<string | null>(null);
|
const tosUrl = ref<string | null>(null);
|
||||||
const privacyPolicyUrl = ref<string | null>(null);
|
const privacyPolicyUrl = ref<string | null>(null);
|
||||||
const inquiryUrl = ref<string | null>(null);
|
const inquiryUrl = ref<string | null>(null);
|
||||||
|
const trustedLinkUrlPatterns = ref<string>('');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
|
@ -120,6 +127,7 @@ async function init() {
|
||||||
bubbleTimeline.value = meta.bubbleInstances.join('\n');
|
bubbleTimeline.value = meta.bubbleInstances.join('\n');
|
||||||
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
|
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
|
||||||
inquiryUrl.value = meta.inquiryUrl;
|
inquiryUrl.value = meta.inquiryUrl;
|
||||||
|
trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -135,6 +143,7 @@ function save() {
|
||||||
hiddenTags: hiddenTags.value.split('\n'),
|
hiddenTags: hiddenTags.value.split('\n'),
|
||||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||||
bubbleInstances: bubbleTimeline.value.split('\n'),
|
bubbleInstances: bubbleTimeline.value.split('\n'),
|
||||||
|
trustedLinkUrlPatterns: trustedLinkUrlPatterns.value.split('\n'),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance(true);
|
fetchInstance(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
|
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
|
||||||
|
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||||
|
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
const pluginContexts = new Map<string, Interpreter>();
|
const pluginContexts = new Map<string, Interpreter>();
|
||||||
|
@ -92,16 +93,8 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
|
||||||
registerPageViewInterruptor({ pluginId: opts.plugin.id, handler });
|
registerPageViewInterruptor({ pluginId: opts.plugin.id, handler });
|
||||||
}),
|
}),
|
||||||
'Plugin:open_url': values.FN_NATIVE(([url]) => {
|
'Plugin:open_url': values.FN_NATIVE(([url]) => {
|
||||||
(async () => {
|
utils.assertString(url);
|
||||||
utils.assertString(url);
|
warningExternalWebsite(url.value);
|
||||||
const { canceled } = await os.confirm({
|
|
||||||
type: 'question',
|
|
||||||
text: i18n.tsx.confirmRemoteUrl({ x: url.value }),
|
|
||||||
plain: true,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
window.open(url.value, '_blank', 'noopener');
|
|
||||||
})();
|
|
||||||
}),
|
}),
|
||||||
'Plugin:config': values.OBJ(config),
|
'Plugin:config': values.OBJ(config),
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import MkUrlWarningDialog from '@/components/MkUrlWarningDialog.vue';
|
||||||
|
|
||||||
|
const extractDomain = /^(https?:\/\/|\/\/)?([^@/\s]+@)?(www\.)?([^:/\s]+)/i;
|
||||||
|
const isRegExp = /^\/(.+)\/(.*)$/;
|
||||||
|
|
||||||
|
export async function warningExternalWebsite(url: string) {
|
||||||
|
const domain = extractDomain.exec(url)?.[4];
|
||||||
|
|
||||||
|
if (!domain) return false;
|
||||||
|
|
||||||
|
const isTrustedByInstance = instance.trustedLinkUrlPatterns.some(expression => {
|
||||||
|
const r = isRegExp.exec(expression);
|
||||||
|
|
||||||
|
if (r) {
|
||||||
|
return new RegExp(r[1], r[2]).test(url);
|
||||||
|
} else if (expression.includes(' ')) return expression.split(' ').every(keyword => url.includes(keyword));
|
||||||
|
else return domain.endsWith(expression);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(domain);
|
||||||
|
|
||||||
|
if (!isTrustedByInstance && !isTrustedByUser) {
|
||||||
|
const confirm = await new Promise<{ canceled: boolean }>(resolve => {
|
||||||
|
const { dispose } = os.popup(MkUrlWarningDialog, {
|
||||||
|
url,
|
||||||
|
}, {
|
||||||
|
done: result => {
|
||||||
|
resolve(result ? result : { canceled: true });
|
||||||
|
},
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirm.canceled) return false;
|
||||||
|
|
||||||
|
window.open(url, '_blank', 'nofollow noopener popup=false');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -165,6 +165,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
default: 'public' as 'public' | 'home' | 'followers',
|
default: 'public' as 'public' | 'home' | 'followers',
|
||||||
},
|
},
|
||||||
|
trustedDomains: {
|
||||||
|
where: 'account',
|
||||||
|
default: [] as string[],
|
||||||
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
where: 'deviceAccount',
|
where: 'deviceAccount',
|
||||||
|
|
|
@ -5098,6 +5098,7 @@ export type components = {
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
noteSearchableScope: 'local' | 'global';
|
noteSearchableScope: 'local' | 'global';
|
||||||
|
trustedLinkUrlPatterns: string[];
|
||||||
};
|
};
|
||||||
MetaDetailedOnly: {
|
MetaDetailedOnly: {
|
||||||
features?: {
|
features?: {
|
||||||
|
@ -5294,6 +5295,7 @@ export type operations = {
|
||||||
urlPreviewRequireContentLength: boolean;
|
urlPreviewRequireContentLength: boolean;
|
||||||
urlPreviewUserAgent: string | null;
|
urlPreviewUserAgent: string | null;
|
||||||
urlPreviewSummaryProxyUrl: string | null;
|
urlPreviewSummaryProxyUrl: string | null;
|
||||||
|
trustedLinkUrlPatterns: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9815,6 +9817,7 @@ export type operations = {
|
||||||
urlPreviewRequireContentLength?: boolean;
|
urlPreviewRequireContentLength?: boolean;
|
||||||
urlPreviewUserAgent?: string | null;
|
urlPreviewUserAgent?: string | null;
|
||||||
urlPreviewSummaryProxyUrl?: string | null;
|
urlPreviewSummaryProxyUrl?: string | null;
|
||||||
|
trustedLinkUrlPatterns?: string[] | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue