feat(fe): global external-link guard
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
'text-editor flex flex-col relative',
|
||||
!!readonly ? 'text-editor--read-only' : ''
|
||||
]"
|
||||
@click.capture="onRootClick"
|
||||
>
|
||||
<FormButton
|
||||
v-if="unlinkVisible"
|
||||
@@ -18,23 +17,6 @@
|
||||
Remove link
|
||||
</FormButton>
|
||||
|
||||
<LayoutDialog
|
||||
v-model:open="externalLinkDialogOpen"
|
||||
max-width="xs"
|
||||
:buttons="externalLinkDialogButtons"
|
||||
>
|
||||
<template #header>Leaving Speckle</template>
|
||||
<p class="mb-2">You're about to open the link below in a new tab:</p>
|
||||
<div class="p-3 bg-highlight-2 rounded-md font-mono break-all">
|
||||
{{ externalLinkDialogUrl }}
|
||||
</div>
|
||||
<p class="mt-2 mb-4">
|
||||
This is an external website. Speckle is not responsible for its content or
|
||||
security.
|
||||
</p>
|
||||
<p class="font-medium">Do you want to continue?</p>
|
||||
</LayoutDialog>
|
||||
|
||||
<EditorContent
|
||||
ref="editorContentRef"
|
||||
class="simple-scrollbar flex flex-1"
|
||||
@@ -60,11 +42,7 @@ import type { Nullable } from '@speckle/shared'
|
||||
// import { userProfileRoute } from '~~/lib/common/helpers/route'
|
||||
import { onKeyDown } from '@vueuse/core'
|
||||
import { noop } from 'lodash-es'
|
||||
import {
|
||||
FormButton,
|
||||
LayoutDialog,
|
||||
type LayoutDialogButton
|
||||
} from '@speckle/ui-components'
|
||||
import { FormButton } from '@speckle/ui-components'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: JSONContent): void
|
||||
@@ -89,24 +67,6 @@ const props = defineProps<{
|
||||
|
||||
const editorContentRef = ref(null as Nullable<HTMLElement>)
|
||||
const unlinkVisible = ref(false)
|
||||
const externalLinkDialogOpen = ref(false)
|
||||
const externalLinkDialogUrl = ref('')
|
||||
|
||||
const externalLinkDialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline' },
|
||||
onClick: () => (externalLinkDialogOpen.value = false)
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
props: { color: 'danger' },
|
||||
onClick: () => {
|
||||
window.open(externalLinkDialogUrl.value, '_blank', 'noopener,noreferrer')
|
||||
externalLinkDialogOpen.value = false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const isMultiLine = computed(() => !!props.schemaOptions?.multiLine)
|
||||
const isEditable = computed(() => !props.disabled && !props.readonly)
|
||||
@@ -161,22 +121,6 @@ const onEditorContentClick = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const onRootClick = (e: MouseEvent) => {
|
||||
if (!props.readonly) return
|
||||
|
||||
const anchor = (e.target as HTMLElement).closest('a') as Nullable<HTMLAnchorElement>
|
||||
if (!anchor) return
|
||||
|
||||
// Only react to left-clicks without modifier keys
|
||||
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
||||
|
||||
e.preventDefault()
|
||||
const url = new URL(anchor.href, window.location.href)
|
||||
if (url.origin === window.location.origin) return // treat as internal
|
||||
externalLinkDialogUrl.value = anchor.href
|
||||
externalLinkDialogOpen.value = true
|
||||
}
|
||||
|
||||
// TODO: No profile page to link to in FE2 yet
|
||||
// const onMentionClick = (userId: string, e: MouseEvent) => {
|
||||
// if (!props.readonly) return
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="state.open" max-width="xs" :buttons="buttons">
|
||||
<template #header>Leaving Speckle</template>
|
||||
<p class="mb-2">You're about to open the link below in a new tab:</p>
|
||||
<div class="p-3 bg-highlight-2 rounded-md font-mono break-all">
|
||||
{{ state.url }}
|
||||
</div>
|
||||
<p class="mt-2 mb-4">
|
||||
This is an external website. Speckle is not responsible for its content or
|
||||
security.
|
||||
</p>
|
||||
<p class="font-medium">Do you want to continue?</p>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalExternalLinkDialog } from '~/lib/common/composables/externalLinkDialog'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
|
||||
const { state } = useGlobalExternalLinkDialog()
|
||||
const buttons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Cancel',
|
||||
onClick: () => {
|
||||
state.value.open = false
|
||||
state.value._resolver?.(false)
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
props: { color: 'danger' },
|
||||
onClick: () => {
|
||||
window.open(state.value.url, '_blank', 'noopener,noreferrer')
|
||||
state.value.open = false
|
||||
state.value._resolver?.(true)
|
||||
}
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@
|
||||
</ClientOnly>
|
||||
<SingletonFileUploadErrorDialog />
|
||||
<SingletonAppErrorStateManager />
|
||||
<SingletonExternalLinkDialog />
|
||||
</div>
|
||||
<div v-else />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
type ExternalLinkState = {
|
||||
open: boolean
|
||||
url: string
|
||||
_resolver?: (accepted: boolean) => void
|
||||
}
|
||||
|
||||
const useGlobalExternalLinkDialog = () => {
|
||||
const state = useState<ExternalLinkState>('global_external_link', () => ({
|
||||
open: false,
|
||||
url: ''
|
||||
}))
|
||||
|
||||
const confirm = (url: string) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
state.value = { open: true, url, _resolver: resolve }
|
||||
})
|
||||
|
||||
return { state, confirm }
|
||||
}
|
||||
export { useGlobalExternalLinkDialog }
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useGlobalExternalLinkDialog } from '~/lib/common/composables/externalLinkDialog'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const { confirm } = useGlobalExternalLinkDialog()
|
||||
|
||||
const handler = (e: MouseEvent) => {
|
||||
// Ignore modified / non-left clicks
|
||||
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
||||
|
||||
const a = (e.target as HTMLElement).closest('a[href]') as HTMLAnchorElement | null
|
||||
if (!a) return
|
||||
if (a.hasAttribute('data-no-external-confirm') || a.hasAttribute('download')) return
|
||||
|
||||
const url = new URL(a.href, window.location.href)
|
||||
|
||||
if (url.origin === window.location.origin) {
|
||||
e.preventDefault()
|
||||
|
||||
const path = url.pathname + url.search + url.hash
|
||||
navigateTo(path)
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
void confirm(a.href)
|
||||
}
|
||||
|
||||
document.addEventListener('click', handler, true)
|
||||
})
|
||||
Reference in New Issue
Block a user