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,
|
autolink: true,
|
||||||
defaultProtocol: 'https',
|
defaultProtocol: 'https',
|
||||||
protocols: ['http', 'https'],
|
protocols: ['http', 'https'],
|
||||||
validate: (url, ctx) => {
|
isAllowedUri: (url, ctx) => {
|
||||||
try {
|
try {
|
||||||
// construct URL
|
// construct URL
|
||||||
const parsedUrl = url.includes(':') ? new URL(url) : new URL(`${ctx.defaultProtocol}://${url}`)
|
const parsedUrl = url.includes(':') ? new URL(url) : new URL(`${ctx.defaultProtocol}://${url}`)
|
||||||
|
@ -38,52 +38,79 @@ export interface LinkOptions {
|
|||||||
* @default true
|
* @default true
|
||||||
* @example false
|
* @example false
|
||||||
*/
|
*/
|
||||||
autolink: boolean
|
autolink: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of custom protocols to be registered with linkifyjs.
|
* An array of custom protocols to be registered with linkifyjs.
|
||||||
* @default []
|
* @default []
|
||||||
* @example ['ftp', 'git']
|
* @example ['ftp', 'git']
|
||||||
*/
|
*/
|
||||||
protocols: Array<LinkProtocolOptions | string>
|
protocols: Array<LinkProtocolOptions | string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default protocol to use when no protocol is specified.
|
* Default protocol to use when no protocol is specified.
|
||||||
* @default 'http'
|
* @default 'http'
|
||||||
*/
|
*/
|
||||||
defaultProtocol: string
|
defaultProtocol: string;
|
||||||
/**
|
/**
|
||||||
* If enabled, links will be opened on click.
|
* If enabled, links will be opened on click.
|
||||||
* @default true
|
* @default true
|
||||||
* @example false
|
* @example false
|
||||||
*/
|
*/
|
||||||
openOnClick: boolean | DeprecatedOpenWhenNotEditable
|
openOnClick: boolean | DeprecatedOpenWhenNotEditable;
|
||||||
/**
|
/**
|
||||||
* Adds a link to the current selection if the pasted content only contains an url.
|
* Adds a link to the current selection if the pasted content only contains an url.
|
||||||
* @default true
|
* @default true
|
||||||
* @example false
|
* @example false
|
||||||
*/
|
*/
|
||||||
linkOnPaste: boolean
|
linkOnPaste: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML attributes to add to the link element.
|
* HTML attributes to add to the link element.
|
||||||
* @default {}
|
* @default {}
|
||||||
* @example { class: 'foo' }
|
* @example { class: 'foo' }
|
||||||
*/
|
*/
|
||||||
HTMLAttributes: Record<string, any>
|
HTMLAttributes: Record<string, any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A validation function that modifies link verification.
|
* @deprecated Use the `shouldAutoLink` option instead.
|
||||||
*
|
* A validation function that modifies link verification for the auto linker.
|
||||||
* @param {string} url - The URL to be validated.
|
* @param url - The url to be validated.
|
||||||
* @param {Object} ctx - An object containing:
|
* @returns - True if the url is valid, false otherwise.
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
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.
|
* 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.
|
* @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.
|
* @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' {
|
declare module '@tiptap/core' {
|
||||||
@ -102,19 +129,29 @@ declare module '@tiptap/core' {
|
|||||||
* @param attributes The link attributes
|
* @param attributes The link attributes
|
||||||
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
* @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
|
* Toggle a link mark
|
||||||
* @param attributes The link attributes
|
* @param attributes The link attributes
|
||||||
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
* @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
|
* Unset a link mark
|
||||||
* @example editor.commands.unsetLink()
|
* @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
|
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
|
||||||
|
|
||||||
function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
|
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) {
|
if (protocols) {
|
||||||
protocols.forEach(protocol => {
|
protocols.forEach(protocol => {
|
||||||
const nextProtocol = (typeof protocol === 'string' ? protocol : protocol.scheme)
|
const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme
|
||||||
|
|
||||||
if (nextProtocol) {
|
if (nextProtocol) {
|
||||||
allowedProtocols.push(nextProtocol)
|
allowedProtocols.push(nextProtocol)
|
||||||
@ -136,8 +184,18 @@ function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocol
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
return (
|
||||||
return !uri || uri.replace(ATTR_WHITESPACE, '').match(new RegExp(`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`, 'i'))
|
!uri
|
||||||
|
|| uri
|
||||||
|
.replace(ATTR_WHITESPACE, '')
|
||||||
|
.match(
|
||||||
|
new RegExp(
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`,
|
||||||
|
'i',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,6 +212,13 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
exitable: true,
|
exitable: true,
|
||||||
|
|
||||||
onCreate() {
|
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 => {
|
this.options.protocols.forEach(protocol => {
|
||||||
if (typeof protocol === 'string') {
|
if (typeof protocol === 'string') {
|
||||||
registerCustomProtocol(protocol)
|
registerCustomProtocol(protocol)
|
||||||
@ -183,7 +248,8 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
rel: 'noopener noreferrer nofollow',
|
rel: 'noopener noreferrer nofollow',
|
||||||
class: null,
|
class: null,
|
||||||
},
|
},
|
||||||
validate: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
||||||
|
validate: url => !!url,
|
||||||
shouldAutoLink: url => !!url,
|
shouldAutoLink: url => !!url,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -209,25 +275,44 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [{
|
return [
|
||||||
tag: 'a[href]',
|
{
|
||||||
getAttrs: dom => {
|
tag: 'a[href]',
|
||||||
const href = (dom as HTMLElement).getAttribute('href')
|
getAttrs: dom => {
|
||||||
|
const href = (dom as HTMLElement).getAttribute('href')
|
||||||
|
|
||||||
// prevent XSS attacks
|
// 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 (
|
||||||
return false
|
!href
|
||||||
}
|
|| !this.options.isAllowedUri(href, {
|
||||||
return null
|
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||||
|
protocols: this.options.protocols,
|
||||||
|
defaultProtocol: this.options.defaultProtocol,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
// prevent XSS attacks
|
// 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
|
// 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]
|
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||||
@ -265,17 +350,24 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
const foundLinks: PasteRuleMatch[] = []
|
const foundLinks: PasteRuleMatch[] = []
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
const { validate, protocols, defaultProtocol } = this.options
|
const { protocols, defaultProtocol } = this.options
|
||||||
const links = find(text).filter(item => item.isLink && validate(item.value, { defaultValidate: href => !!isAllowedUri(href, protocols), protocols, defaultProtocol }))
|
const links = find(text).filter(
|
||||||
|
item => item.isLink
|
||||||
|
&& this.options.isAllowedUri(item.value, {
|
||||||
|
defaultValidate: href => !!isAllowedUri(href, protocols),
|
||||||
|
protocols,
|
||||||
|
defaultProtocol,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
links.forEach(link => (foundLinks.push({
|
links.forEach(link => foundLinks.push({
|
||||||
text: link.value,
|
text: link.value,
|
||||||
data: {
|
data: {
|
||||||
href: link.href,
|
href: link.href,
|
||||||
},
|
},
|
||||||
index: link.start,
|
index: link.start,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,14 +385,18 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
|
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
const plugins: Plugin[] = []
|
const plugins: Plugin[] = []
|
||||||
const { validate, protocols, defaultProtocol } = this.options
|
const { protocols, defaultProtocol } = this.options
|
||||||
|
|
||||||
if (this.options.autolink) {
|
if (this.options.autolink) {
|
||||||
plugins.push(
|
plugins.push(
|
||||||
autolink({
|
autolink({
|
||||||
type: this.type,
|
type: this.type,
|
||||||
defaultProtocol: this.options.defaultProtocol,
|
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,
|
shouldAutoLink: this.options.shouldAutoLink,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user