Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/g6/__tests__/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export { pluginToolbarIconfont } from './plugin-toolbar-iconfont';
export { pluginTooltip } from './plugin-tooltip';
export { pluginTooltipAsync } from './plugin-tooltip-async';
export { pluginTooltipDual } from './plugin-tooltip-dual';
export { pluginTooltipEnable } from './plugin-tooltip-enable';
export { pluginTooltipEnable, pluginTooltipPrefixClsEnable } from './plugin-tooltip-enable';
export { pluginTooltipWithCustomNode } from './plugin-tooltip-with-custom-node';
export { pluginWatermark } from './plugin-watermark';
export { pluginWatermarkImage } from './plugin-watermark-image';
Expand Down
36 changes: 36 additions & 0 deletions packages/g6/__tests__/demos/plugin-tooltip-enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,39 @@ export const pluginTooltipEnable: TestCase = async (context) => {

return graph;
};

export const pluginTooltipPrefixClsEnable: TestCase = async (context) => {
const graph = new Graph({
...context,
data: {
nodes: [
{ id: 'node11', style: { x: 150, y: 100 }, data: { type: 'test1', desc: 'This is a tooltip' } },
{ id: 'node22', style: { x: 150, y: 200 }, data: { type: 'test1', desc: '' } },
{ id: 'node33', style: { x: 150, y: 300 }, data: { type: 'test2', desc: 'This is a tooltip' } },
],
},
node: {
style: {
labelText: (d) => d.id,
},
},
plugins: [
{
key: 'tooltip',
type: 'tooltip',
trigger: 'click',
prefixCls: 'g6prefix-',
enable: (e: IElementEvent, items: ElementDatum[]) => {
return items[0].data?.type === 'test1';
},
getContent: (evt: IElementEvent, items: ElementDatum[]) => {
return items[0].data?.desc || '';
},
},
],
});

await graph.render();

return graph;
};
22 changes: 21 additions & 1 deletion packages/g6/__tests__/unit/plugins/tooltip.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Tooltip } from '@/src';
import { ComboEvent, EdgeEvent, NodeEvent, idOf } from '@/src';
import { pluginTooltip, pluginTooltipAsync, pluginTooltipEnable } from '@@/demos';
import { pluginTooltip, pluginTooltipAsync, pluginTooltipEnable, pluginTooltipPrefixClsEnable } from '@@/demos';
import { createDemoGraph } from '@@/utils';

describe('plugin tooltip', () => {
Expand Down Expand Up @@ -66,6 +66,26 @@ describe('plugin tooltip', () => {
graph.destroy();
});

it('enable-prefixCls', async () => {
const graph = await createDemoGraph(pluginTooltipPrefixClsEnable);
const container = graph.getCanvas().getContainer()!;
const el = container.querySelector('.tooltip');
expect(el).toBeNull();

const prefixEl = container.querySelector('.g6prefix-tooltip') as HTMLDivElement;
expect(prefixEl).not.toBeNull();

const plugin = graph.getPluginInstance<Tooltip>('tooltip');

await plugin.showById('node33');
expect(prefixEl.style.visibility).toBe('hidden');

await plugin.showById('node11');
expect(prefixEl.style.visibility).toBe('visible');

graph.destroy();
});

it('get content null', async () => {
const graph = await createDemoGraph(pluginTooltipEnable);
const container = graph.getCanvas().getContainer()!;
Expand Down
30 changes: 22 additions & 8 deletions packages/g6/src/plugins/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export interface TooltipOptions
* <en/> Callback executed when visibility of the tooltip card is changed
*/
onOpenChange: (open: boolean) => void;
/**
* <zh/> 自定义类名前缀
*
* <en/> Custom class name prefix
*/
prefixCls?: string;
}

/**
Expand All @@ -59,18 +65,24 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
enterable: false,
enable: true,
offset: [10, 10],
style: {
'.tooltip': {
visibility: 'hidden',
},
},
};
private currentTarget: string | null = null;
private tooltipElement: TooltipComponent | null = null;
private container: HTMLElement | null = null;

constructor(context: RuntimeContext, options: TooltipOptions) {
super(context, Object.assign({}, Tooltip.defaultOptions, options));
const combineOptions = Object.assign(
{
style: {
[`.${options.prefixCls || ''}tooltip`]: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The prefixCls option is used to construct a CSS selector key without any sanitization. This creates a potential CSS Injection and Cross-Site Scripting (XSS) vulnerability if an attacker can control this option, as they could inject malicious CSS rules. It is recommended to sanitize the input to allow only safe characters like alphanumeric characters, hyphens, and underscores.

Suggested change
[`.${options.prefixCls || ''}tooltip`]: {
[`.${(options.prefixCls || '').replace(/[^a-zA-Z0-9_-]/g, '')}tooltip`]: {

visibility: 'hidden',
},
},
},
Tooltip.defaultOptions,
options,
);
super(context, combineOptions);
Comment on lines +74 to +85

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for combining options might lead to issues. Object.assign performs a shallow merge. If a user provides a style object in the options, it will completely overwrite the default style that sets visibility: 'hidden'. This could cause the tooltip to be visible by default, which is likely not the intended behavior and a regression for users customizing styles.

To fix this, I recommend using a deep merge for the options, especially for the style property. This ensures that user-provided styles are merged with the default styles, preserving the essential visibility: 'hidden' rule. You'll need to import deepMix from @antv/util at the top of the file.

Suggested change
const combineOptions = Object.assign(
{
style: {
[`.${options.prefixCls || ''}tooltip`]: {
visibility: 'hidden',
},
},
},
Tooltip.defaultOptions,
options,
);
super(context, combineOptions);
const defaultStyledOptions = {
style: {
[`.${options.prefixCls || ''}tooltip`]: {
visibility: 'hidden',
},
},
};
const finalOptions = deepMix({}, defaultStyledOptions, Tooltip.defaultOptions, options);
super(context, finalOptions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@XiaoMing-xmg666 这里得改下 Object.assign 可能会意外覆盖 style

this.render();
this.bindEvents();
}
Expand Down Expand Up @@ -299,7 +311,7 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
x,
y,
style: {
'.tooltip': {
[`.${this.options.prefixCls || ''}tooltip`]: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Similar to the constructor, the show method uses the prefixCls option to construct a CSS selector without sanitization, which is vulnerable to CSS injection.

Suggested change
[`.${this.options.prefixCls || ''}tooltip`]: {
[`.${(this.options.prefixCls || '').replace(/[^a-zA-Z0-9_-]/g, '')}tooltip`]: {

visibility: 'visible',
},
},
Expand Down Expand Up @@ -350,12 +362,14 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
enterable,
offset,
style,
template: {
prefixCls: this.options.prefixCls || '',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The prefixCls is passed to the component's template without sanitization. If the underlying TooltipComponent renders this value directly into the DOM (e.g., as a class name in an unescaped template), it could lead to Cross-Site Scripting (XSS). Sanitizing the prefix ensures it remains a safe string for DOM attributes.

Suggested change
prefixCls: this.options.prefixCls || '',
prefixCls: (this.options.prefixCls || '').replace(/[^a-zA-Z0-9_-]/g, ''),

},
Comment on lines +365 to +367

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

};
}

private initTooltip = () => {
const tooltipElement = new TooltipComponent({
className: 'tooltip',
style: this.tooltipStyleProps,
});
this.container?.appendChild(tooltipElement.HTMLTooltipElement);
Expand Down
1 change: 1 addition & 0 deletions packages/site/docs/manual/plugin/Tooltip.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const graph = new Graph({
| enterable | Whether pointer can enter | boolean | false | |
| title | Title | string | - |
| style | Style object | Record<string,any> | {'.tooltip': { visibility: 'hidden'}} | |
| prefixCls | custom class name prefix | string | - | |

## Detailed Configuration

Expand Down
1 change: 1 addition & 0 deletions packages/site/docs/manual/plugin/Tooltip.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const graph = new Graph({
| enterable | 指针是否可以进入 | boolean | false | |
| title | 标题 | string | - |
| style | 样式对象 | Record<string,any> | {'.tooltip': { visibility: 'hidden'}} | |
| prefixCls | tooltip自定义类名前缀 | string | - | |

## 详细配置说明

Expand Down
Loading