diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 93e930490cb8..5565f2fd02bc 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -70,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:enableEmojiMenu="true"
:enableEmojiMenuReaction="true"
/>
+
@@ -82,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ appearNote.channel.name }}
@@ -202,6 +203,7 @@ import MkPoll from '@/components/MkPoll.vue';
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
+import MkPhishingCaution from '@/components/MkPhishingCaution.vue';
import { pleaseLogin } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
@@ -274,12 +276,48 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals
const translation = ref(null);
const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
-const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
+const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url.href && appearNote.value.renote?.uri !== url.href) : null;
+const previewUrls = urls.filter(url => url.preview);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref([]);
const replies = ref([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
+function isURL(str) {
+ try {
+ new URL(str);
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
+function getDomain(url) {
+ try {
+ const domain = new URL(url).hostname;
+ return domain;
+ } catch (error) {
+ return null;
+ }
+}
+
+const isSuspectPhishingLink = urls.some(url => {
+ // url.textが配列でない場合、配列に変換
+ const text = Array.isArray(url.text) ? url.text.join('') : url.text;
+
+ // textがURLでない場合、すぐに次のurlへ
+ console.log(text);
+ if (!isURL((text.startsWith('https://') || text.startsWith('http://')) ? text : `https://${text}`)) return false;
+
+ // hrefとtextのドメインを比較
+ const hrefDomain = getDomain(url.href);
+ const textDomain = getDomain(text);
+ console.log(hrefDomain, textDomain);
+
+ // ドメインが一致しない場合、フィッシングの疑いあり
+ return hrefDomain !== textDomain && textDomain;
+ });
+
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
diff --git a/packages/frontend/src/components/MkPhishingCaution.vue b/packages/frontend/src/components/MkPhishingCaution.vue
new file mode 100644
index 000000000000..ccbbfb159470
--- /dev/null
+++ b/packages/frontend/src/components/MkPhishingCaution.vue
@@ -0,0 +1,33 @@
+
+
+
+{{ cautionMessage }}
+
+
+
+
+
diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/scripts/extract-url-from-mfm.ts
index d5654ba850f8..8adc62ce9056 100644
--- a/packages/frontend/src/scripts/extract-url-from-mfm.ts
+++ b/packages/frontend/src/scripts/extract-url-from-mfm.ts
@@ -12,13 +12,44 @@ const removeHash = (x: string) => x.replace(/#[^#]*$/, '');
export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] {
const urlNodes = mfm.extract(nodes, (node) => {
- return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent));
+ return (node.type === 'url') || (node.type === 'link');
});
- const urls: string[] = unique(urlNodes.map(x => x.props.url));
+ const urls = unique(urlNodes.map(x => {
+ return x.type === 'url'
+ ? ({
+ href: x.props.url,
+ text: x.props.url,
+ preview: true,
+ })
+ : ({
+ href: x.props.url,
+ text: extractTextValues(x.children) ?? [],
+ preview: (!respectSilentFlag || !x.props.silent),
+ });
+ }));
return urls.reduce((array, url) => {
- const urlWithoutHash = removeHash(url);
- if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url);
+ const urlWithoutHash = removeHash(url.href);
+ if (!array.map(x => removeHash(x.href)).includes(urlWithoutHash)) array.push(url);
return array;
- }, [] as string[]);
+ }, []);
+}
+
+function extractTextValues(obj: mfm.MfmNode) {
+ const textValues = [];
+
+ function traverse(o) {
+ if (Array.isArray(o)) {
+ o.forEach(item => traverse(item));
+ } else if (o && typeof o === 'object') {
+ if (o.type === 'text') {
+ textValues.push(o.props.text);
+ } else if (o.children) {
+ o.children.forEach(child => traverse(child));
+ }
+ }
+ }
+
+ traverse(obj);
+ return textValues;
}