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 @@ + + + + + + + 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; }