mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-24 03:39:01 +08:00
fix(link): add backwards compat by deprecating validate and using isAllowedUri instead (#5812)
This commit is contained in:
parent
88371561bb
commit
62c6dddf80
@ -21,7 +21,7 @@ export default () => {
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
protocols: ['http', 'https'],
|
||||
validate: (url, ctx) => {
|
||||
isAllowedUri: (url, ctx) => {
|
||||
try {
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(':') ? new URL(url) : new URL(`${ctx.defaultProtocol}://${url}`)
|
||||
|
@ -38,52 +38,79 @@ export interface LinkOptions {
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
autolink: boolean
|
||||
autolink: boolean;
|
||||
|
||||
/**
|
||||
* An array of custom protocols to be registered with linkifyjs.
|
||||
* @default []
|
||||
* @example ['ftp', 'git']
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
|
||||
/**
|
||||
* Default protocol to use when no protocol is specified.
|
||||
* @default 'http'
|
||||
*/
|
||||
defaultProtocol: string
|
||||
defaultProtocol: string;
|
||||
/**
|
||||
* If enabled, links will be opened on click.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
openOnClick: boolean | DeprecatedOpenWhenNotEditable
|
||||
openOnClick: boolean | DeprecatedOpenWhenNotEditable;
|
||||
/**
|
||||
* Adds a link to the current selection if the pasted content only contains an url.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
linkOnPaste: boolean
|
||||
linkOnPaste: boolean;
|
||||
|
||||
/**
|
||||
* HTML attributes to add to the link element.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>
|
||||
HTMLAttributes: Record<string, any>;
|
||||
|
||||
/**
|
||||
* A validation function that modifies link verification.
|
||||
*
|
||||
* @param {string} url - The URL to be validated.
|
||||
* @param {Object} ctx - An object containing:
|
||||
* @param {Function} ctx.defaultValidate - A function that performs the default URL validation.
|
||||
* @param {string[]} ctx.protocols - An array of allowed protocols for the URL (e.g., "http", "https").
|
||||
* @param {string} ctx.defaultProtocol - A string that represents the default protocol (e.g., 'http').
|
||||
*
|
||||
* @returns {boolean} True if the URL is valid, false otherwise.
|
||||
* @deprecated Use the `shouldAutoLink` option instead.
|
||||
* A validation function that modifies link verification for the auto linker.
|
||||
* @param url - The url to be validated.
|
||||
* @returns - True if the url is valid, false otherwise.
|
||||
*/
|
||||
validate: (url: string, ctx: { defaultValidate: (url: string) => boolean, protocols: Array<LinkProtocolOptions | string>, defaultProtocol: string }) => boolean
|
||||
validate: (url: string) => boolean;
|
||||
|
||||
/**
|
||||
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
||||
* Only modify this if you know what you're doing.
|
||||
*
|
||||
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
||||
*
|
||||
* @example
|
||||
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
||||
* return url.startsWith('./') || defaultValidate(url)
|
||||
* }
|
||||
*/
|
||||
isAllowedUri: (
|
||||
/**
|
||||
* The URL to be validated.
|
||||
*/
|
||||
url: string,
|
||||
ctx: {
|
||||
/**
|
||||
* The default validation function.
|
||||
*/
|
||||
defaultValidate: (url: string) => boolean;
|
||||
/**
|
||||
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
/**
|
||||
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
||||
*/
|
||||
defaultProtocol: string;
|
||||
}
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* Determines whether a valid link should be automatically linked in the content.
|
||||
@ -91,7 +118,7 @@ export interface LinkOptions {
|
||||
* @param {string} url - The URL that has already been validated.
|
||||
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
||||
*/
|
||||
shouldAutoLink: (url: string) => boolean
|
||||
shouldAutoLink: (url: string) => boolean;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
@ -102,19 +129,29 @@ declare module '@tiptap/core' {
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
setLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => ReturnType
|
||||
setLink: (attributes: {
|
||||
href: string;
|
||||
target?: string | null;
|
||||
rel?: string | null;
|
||||
class?: string | null;
|
||||
}) => ReturnType;
|
||||
/**
|
||||
* Toggle a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
toggleLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => ReturnType
|
||||
toggleLink: (attributes: {
|
||||
href: string;
|
||||
target?: string | null;
|
||||
rel?: string | null;
|
||||
class?: string | null;
|
||||
}) => ReturnType;
|
||||
/**
|
||||
* Unset a link mark
|
||||
* @example editor.commands.unsetLink()
|
||||
*/
|
||||
unsetLink: () => ReturnType
|
||||
}
|
||||
unsetLink: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,11 +161,22 @@ declare module '@tiptap/core' {
|
||||
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
|
||||
|
||||
function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
|
||||
const allowedProtocols: string[] = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp']
|
||||
const allowedProtocols: string[] = [
|
||||
'http',
|
||||
'https',
|
||||
'ftp',
|
||||
'ftps',
|
||||
'mailto',
|
||||
'tel',
|
||||
'callto',
|
||||
'sms',
|
||||
'cid',
|
||||
'xmpp',
|
||||
]
|
||||
|
||||
if (protocols) {
|
||||
protocols.forEach(protocol => {
|
||||
const nextProtocol = (typeof protocol === 'string' ? protocol : protocol.scheme)
|
||||
const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme
|
||||
|
||||
if (nextProtocol) {
|
||||
allowedProtocols.push(nextProtocol)
|
||||
@ -136,8 +184,18 @@ function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocol
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
!uri
|
||||
|| uri
|
||||
.replace(ATTR_WHITESPACE, '')
|
||||
.match(
|
||||
new RegExp(
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return !uri || uri.replace(ATTR_WHITESPACE, '').match(new RegExp(`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`, 'i'))
|
||||
`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`,
|
||||
'i',
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,6 +212,13 @@ export const Link = Mark.create<LinkOptions>({
|
||||
exitable: true,
|
||||
|
||||
onCreate() {
|
||||
if (this.options.validate && !this.options.shouldAutoLink) {
|
||||
// Copy the validate function to the shouldAutoLink option
|
||||
this.options.shouldAutoLink = this.options.validate
|
||||
console.warn(
|
||||
'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.',
|
||||
)
|
||||
}
|
||||
this.options.protocols.forEach(protocol => {
|
||||
if (typeof protocol === 'string') {
|
||||
registerCustomProtocol(protocol)
|
||||
@ -183,7 +248,8 @@ export const Link = Mark.create<LinkOptions>({
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
class: null,
|
||||
},
|
||||
validate: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
||||
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
||||
validate: url => !!url,
|
||||
shouldAutoLink: url => !!url,
|
||||
}
|
||||
},
|
||||
@ -209,25 +275,44 @@ export const Link = Mark.create<LinkOptions>({
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{
|
||||
return [
|
||||
{
|
||||
tag: 'a[href]',
|
||||
getAttrs: dom => {
|
||||
const href = (dom as HTMLElement).getAttribute('href')
|
||||
|
||||
// prevent XSS attacks
|
||||
if (!href || !this.options.validate(href, { defaultValidate: url => !!isAllowedUri(url, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) {
|
||||
if (
|
||||
!href
|
||||
|| !this.options.isAllowedUri(href, {
|
||||
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return null
|
||||
},
|
||||
}]
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
// prevent XSS attacks
|
||||
if (!this.options.validate(HTMLAttributes.href, { defaultValidate: href => !!isAllowedUri(href, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) {
|
||||
if (
|
||||
!this.options.isAllowedUri(HTMLAttributes.href, {
|
||||
defaultValidate: href => !!isAllowedUri(href, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
// strip out the href
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]
|
||||
return [
|
||||
'a',
|
||||
mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }),
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
@ -265,17 +350,24 @@ export const Link = Mark.create<LinkOptions>({
|
||||
const foundLinks: PasteRuleMatch[] = []
|
||||
|
||||
if (text) {
|
||||
const { validate, protocols, defaultProtocol } = this.options
|
||||
const links = find(text).filter(item => item.isLink && validate(item.value, { defaultValidate: href => !!isAllowedUri(href, protocols), protocols, defaultProtocol }))
|
||||
const { protocols, defaultProtocol } = this.options
|
||||
const links = find(text).filter(
|
||||
item => item.isLink
|
||||
&& this.options.isAllowedUri(item.value, {
|
||||
defaultValidate: href => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol,
|
||||
}),
|
||||
)
|
||||
|
||||
if (links.length) {
|
||||
links.forEach(link => (foundLinks.push({
|
||||
links.forEach(link => foundLinks.push({
|
||||
text: link.value,
|
||||
data: {
|
||||
href: link.href,
|
||||
},
|
||||
index: link.start,
|
||||
})))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,14 +385,18 @@ export const Link = Mark.create<LinkOptions>({
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const plugins: Plugin[] = []
|
||||
const { validate, protocols, defaultProtocol } = this.options
|
||||
const { protocols, defaultProtocol } = this.options
|
||||
|
||||
if (this.options.autolink) {
|
||||
plugins.push(
|
||||
autolink({
|
||||
type: this.type,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
validate: url => validate(url, { defaultValidate: href => !!isAllowedUri(href, protocols), protocols, defaultProtocol }),
|
||||
validate: url => this.options.isAllowedUri(url, {
|
||||
defaultValidate: href => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol,
|
||||
}),
|
||||
shouldAutoLink: this.options.shouldAutoLink,
|
||||
}),
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user