Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions src/packages/button/button.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
...rest
} = { ...defaultProps, ...props }

const role = 'button'
const getStyle = useMemo(() => {
const style: CSSProperties = {}
if (color) {
Expand Down Expand Up @@ -161,6 +162,8 @@
className={buttonClassNames}
style={{ ...getStyle, ...style }}
onClick={(e) => handleClick(e as any)}
ariaRole={role}
ariaDisabled={disabled}

Check failure on line 166 in src/packages/button/button.taro.tsx

View workflow job for this annotation

GitHub Actions / build

Type '{ children: Element; ref: ForwardedRef<HTMLButtonElement>; className: string; style: { [x: `--${string}`]: any; accentColor?: AccentColor | undefined; ... 823 more ...; vectorEffect?: VectorEffect | undefined; }; onClick: (e: ITouchEvent) => void; ariaRole: string; ariaDisabled: boolean | undefined; id?: string | un...' is not assignable to type 'IntrinsicAttributes & ViewProps'.

Check failure on line 166 in src/packages/button/button.taro.tsx

View workflow job for this annotation

GitHub Actions / build

Type '{ children: Element; ref: ForwardedRef<HTMLButtonElement>; className: string; style: { [x: `--${string}`]: any; accentColor?: AccentColor | undefined; ... 823 more ...; vectorEffect?: VectorEffect | undefined; }; onClick: (e: ITouchEvent) => void; ariaRole: string; ariaDisabled: boolean | undefined; id?: string | un...' is not assignable to type 'IntrinsicAttributes & ViewProps'.
Copy link
Collaborator

Choose a reason for hiding this comment

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

加一个 button.tsx 下的多语言

>
<View className="nut-button-wrap">
{loading && <Loading className="nut-icon-loading" />}
Expand Down
20 changes: 20 additions & 0 deletions src/packages/countdown/countdown.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const defaultProps = {
autoStart: true,
time: 0,
destroy: false,
ariaLabel: '倒计时',
} as TaroCountDownProps

const InternalCountDown: ForwardRefRenderFunction<
Expand All @@ -48,6 +49,7 @@ const InternalCountDown: ForwardRefRenderFunction<
onRestart,
onUpdate,
children,
ariaLabel,
...rest
} = { ...defaultProps, ...props }
const classPrefix = 'nut-countdown'
Expand All @@ -64,6 +66,11 @@ const InternalCountDown: ForwardRefRenderFunction<
diffTime: 0, // 设置了 startTime 时,与 date.now() 的差异
})

const [role, setRole] = useState('')
// ARIA alert提示内容
const [alertContent, setAlertContent] = useState('')
const alertTimerRef = useRef<ReturnType<typeof setTimeout>>()

// 时间戳转换 或 获取当前时间的时间戳
const getTimeStamp = (timeStr?: string | number) => {
if (!timeStr) return Date.now()
Expand Down Expand Up @@ -102,6 +109,12 @@ const InternalCountDown: ForwardRefRenderFunction<
stateRef.current.counting = false
pause()
onEnd && onEnd()
setRole('alert')
setAlertContent(`${ariaLabel}倒计时结束`)
alertTimerRef.current = setTimeout(() => {
setRole('')
setAlertContent('')
}, 3000)
}

if (remainTime > 0) {
Expand Down Expand Up @@ -257,6 +270,9 @@ const InternalCountDown: ForwardRefRenderFunction<

const componentWillUnmount = () => {
destroy && cancelAnimationFrame(stateRef.current.timer)
if (alertTimerRef.current) {
clearTimeout(alertTimerRef.current)
}
}

const getUnit = (unit: string) => {
Expand Down Expand Up @@ -327,9 +343,13 @@ const InternalCountDown: ForwardRefRenderFunction<
<View
className={`${classPrefix} ${className}`}
style={{ ...style }}
ariaLabel={ariaLabel}
{...rest}
>
{renderTaroTime()}
<View role={role} style={{ display: 'none' }}>
{alertContent}
</View>
</View>
)}
</>
Expand Down
19 changes: 18 additions & 1 deletion src/packages/countdown/countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const defaultProps = {
autoStart: true,
time: 0,
destroy: false,
ariaLabel: '倒计时',
} as WebCountDownProps

const InternalCountDown: ForwardRefRenderFunction<
Expand All @@ -45,6 +46,7 @@ const InternalCountDown: ForwardRefRenderFunction<
onRestart,
onUpdate,
children,
ariaLabel,
...rest
} = { ...defaultProps, ...props }
const classPrefix = 'nut-countdown'
Expand All @@ -61,6 +63,11 @@ const InternalCountDown: ForwardRefRenderFunction<
diffTime: 0, // 设置了 startTime 时,与 date.now() 的差异
})

const [role, setRole] = useState('')
// ARIA alert提示内容
const [alertContent, setAlertContent] = useState('')
const alertTimerRef = useRef<number>()

// 时间戳转换 或 获取当前时间的时间戳
const getTimeStamp = (timeStr?: string | number) => {
if (!timeStr) return Date.now()
Expand Down Expand Up @@ -97,6 +104,12 @@ const InternalCountDown: ForwardRefRenderFunction<
stateRef.current.counting = false
pause()
onEnd && onEnd()
setRole('alert')
setAlertContent(`${ariaLabel}倒计时结束`)
alertTimerRef.current = window.setTimeout(() => {
setRole('')
setAlertContent('')
}, 3000)
}

if (remainTime > 0) {
Expand Down Expand Up @@ -270,6 +283,9 @@ const InternalCountDown: ForwardRefRenderFunction<

const componentWillUnmount = () => {
destroy && cancelAnimationFrame(stateRef.current.timer)
if (alertTimerRef.current) {
clearTimeout(alertTimerRef.current)
}
}

const renderTime = (() => {
Expand All @@ -282,9 +298,10 @@ const InternalCountDown: ForwardRefRenderFunction<
<div
className={`${classPrefix} ${className}`}
style={{ ...style }}
aria-label={ariaLabel}
{...rest}
dangerouslySetInnerHTML={{
__html: `${renderTime}`,
__html: `${renderTime}<span style="display:none" role=${role}>${alertContent}</span>`,
}}
Comment on lines 302 to 305
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

存在 XSS 安全风险,建议重构

使用 dangerouslySetInnerHTML 注入包含用户提供的 ariaLabel 的内容存在跨站脚本攻击(XSS)风险。虽然 ariaLabel 有默认值,但如果开发者传入了包含恶意脚本的字符串,这些脚本会被执行。

建议采用更安全的实现方式:

方案一:使用独立的 React 元素(推荐)

-<div
-  className={`${classPrefix} ${className}`}
-  style={{ ...style }}
-  aria-label={ariaLabel}
-  {...rest}
-  dangerouslySetInnerHTML={{
-    __html: `${renderTime}<span style="display:none" role=${role}>${alertContent}</span>`,
-  }}
-/>
+<div
+  className={`${classPrefix} ${className}`}
+  style={{ ...style }}
+  aria-label={ariaLabel}
+  {...rest}
+>
+  <span dangerouslySetInnerHTML={{ __html: renderTime }} />
+  <span style={{ display: 'none' }} role={role}>
+    {alertContent}
+  </span>
+</div>

方案二:如果必须使用 HTML,进行内容转义
使用 DOMPurify 或类似库对 ariaLabel 进行清理:

import DOMPurify from 'dompurify'
const sanitizedLabel = DOMPurify.sanitize(ariaLabel)

基于静态分析工具的提示。

🧰 Tools
🪛 ast-grep (0.39.9)

[warning] 302-302: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
In src/packages/countdown/countdown.tsx around lines 302 to 305, the code uses
dangerouslySetInnerHTML to inject renderTime plus an ariaLabel-derived string,
which creates an XSS risk; replace this with safe React elements: render the
rendered time as normal children and add a separate span element (visually
hidden if needed) with the appropriate role and the ariaLabel/alertContent as
plain text child so React escapes it automatically; if you absolutely must
inject HTML instead, sanitize ariaLabel first with a library like DOMPurify and
use the sanitized result in dangerouslySetInnerHTML.

/>
)}
Expand Down
6 changes: 6 additions & 0 deletions src/packages/countdown/demo.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Demo6 from './demos/taro/demo6'
import Demo7 from './demos/taro/demo7'
import Demo8 from './demos/taro/demo8'
import Demo9 from './demos/taro/demo9'
import Demo10 from './demos/taro/demo10'

const CountDownDemo = () => {
const [translated] = useTranslate({
Expand All @@ -25,6 +26,7 @@ const CountDownDemo = () => {
controlTime: '控制开始和暂停的倒计时',
customStyle: '自定义展示样式',
handleControl: '手动控制',
supportAria: '支持ARIA',
},
'zh-TW': {
basic: '基础用法',
Expand All @@ -36,6 +38,7 @@ const CountDownDemo = () => {
controlTime: '控製開始和暫停的倒計時',
customStyle: '自定義展示樣式',
handleControl: '手動控製',
supportAria: '支持ARIA',
},
'en-US': {
basic: 'Basic Usage',
Expand All @@ -47,6 +50,7 @@ const CountDownDemo = () => {
controlTime: 'Manual Control',
customStyle: 'Custom Style',
handleControl: 'Handle Control',
supportAria: 'support ARIA',
},
})

Expand Down Expand Up @@ -76,6 +80,8 @@ const CountDownDemo = () => {
<Demo8 />
<View className="h2">{translated.handleControl}</View>
<Demo9 />
<View className="h2">{translated.supportAria}</View>
<Demo10 />
</ScrollView>
</>
)
Expand Down
8 changes: 7 additions & 1 deletion src/packages/countdown/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Demo6 from './demos/h5/demo6'
import Demo7 from './demos/h5/demo7'
import Demo8 from './demos/h5/demo8'
import Demo9 from './demos/h5/demo9'
import Demo10 from './demos/h5/demo10'

const CountDownDemo = () => {
const [translated] = useTranslate({
Expand All @@ -22,9 +23,10 @@ const CountDownDemo = () => {
controlTime: '控制开始和暂停的倒计时',
customStyle: '自定义展示样式',
handleControl: '手动控制',
supportAria: '支持ARIA',
},
'zh-TW': {
basic: '基础用法',
basic: '基礎用法',
remainingTime: '剩余時間用法',
format: '自定義格式',
millisecond: '毫秒級渲染',
Expand All @@ -33,6 +35,7 @@ const CountDownDemo = () => {
controlTime: '控製開始和暫停的倒計時',
customStyle: '自定義展示樣式',
handleControl: '手動控製',
supportAria: '支持ARIA',
},
'en-US': {
basic: 'Basic Usage',
Expand All @@ -44,6 +47,7 @@ const CountDownDemo = () => {
controlTime: 'Manual Control',
customStyle: 'Custom Style',
handleControl: 'Handle Control',
supportAria: 'support ARIA',
},
})

Expand All @@ -68,6 +72,8 @@ const CountDownDemo = () => {
<Demo8 />
<h2>{translated.handleControl}</h2>
<Demo9 />
<h2>{translated.supportAria}</h2>
<Demo10 />
</div>
</>
)
Expand Down
39 changes: 39 additions & 0 deletions src/packages/countdown/demos/h5/demo10.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useRef } from 'react'
import { Cell, CountDown } from '@nutui/nutui-react'

const Demo1 = () => {
const stateRef = useRef({
endTime: Date.now() + 60 * 1000,
})
const onEnd = () => {
console.log('countdown: ended.')
}
return (
<>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="primary"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="text"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
</>
)
}
export default Demo1
39 changes: 39 additions & 0 deletions src/packages/countdown/demos/taro/demo10.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useRef } from 'react'
import { Cell, CountDown } from '@nutui/nutui-react-taro'

const Demo1 = () => {
const stateRef = useRef({
endTime: Date.now() + 60 * 1000,
})
const onEnd = () => {
console.log('countdown: ended.')
}
return (
<>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="primary"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="text"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
</>
)
}
export default Demo1
5 changes: 5 additions & 0 deletions src/packages/dialog/content.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
style,
className,
onClick,
ariaRole,
ariaLabel,
} = { ...defaultContentProps, ...props }

const classPrefix = 'nut-dialog'
Expand Down Expand Up @@ -59,6 +61,9 @@
className={classNames(`${classPrefix}-outer`, className)}
style={style}
onClick={(e: ITouchEvent) => handleClick(e)}
ariaRole={ariaRole}
ariaLabel={ariaLabel}
tabindex={-1}

Check failure on line 66 in src/packages/dialog/content.taro.tsx

View workflow job for this annotation

GitHub Actions / build

Type '{ children: (string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | Element | null)[]; ... 5 more ...; tabindex: number; }' is not assignable to type 'IntrinsicAttributes & ViewProps'.

Check failure on line 66 in src/packages/dialog/content.taro.tsx

View workflow job for this annotation

GitHub Actions / build

Type '{ children: (string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | Element | null)[]; ... 5 more ...; tabindex: number; }' is not assignable to type 'IntrinsicAttributes & ViewProps'.
>
{close}
{header}
Expand Down
4 changes: 4 additions & 0 deletions src/packages/dialog/dialog.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
const classPrefix = 'nut-dialog'
const { locale } = useConfig()
const [loading, setLoading] = useState(false)
const role = 'dialog'

useCustomEvent(
id as string,
Expand Down Expand Up @@ -292,6 +293,8 @@ export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
footer={renderFooter()}
footerDirection={footerDirection}
visible={visible}
ariaRole={role}
// ariaLabel={ariaLabel}
>
{content || children}
</Content>
Expand All @@ -314,6 +317,7 @@ export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
closeOnOverlayClick={closeOnOverlayClick}
lockScroll={lockScroll}
onClick={onHandleClickOverlay}
ariaLabel="背景蒙层"
Copy link
Collaborator

Choose a reason for hiding this comment

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

这里可以不要,写在overlay 组件里就行了

/>
)}
{renderContent()}
Expand Down
1 change: 1 addition & 0 deletions src/packages/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ const BaseDialog: ForwardRefRenderFunction<unknown, Partial<WebDialogProps>> = (
closeOnOverlayClick={closeOnOverlayClick}
lockScroll={lockScroll}
onClick={onHandleClickOverlay}
ariaLabel="背景蒙层"
/>
)}
{renderContent()}
Expand Down
5 changes: 5 additions & 0 deletions src/packages/image/demo.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Demo5 from './demos/taro/demo5'
import Demo6 from './demos/taro/demo6'
import Demo7 from './demos/taro/demo7'
import Demo8 from './demos/taro/demo8'
import Demo9 from './demos/taro/demo9'

const ImageDemo = () => {
const [translated] = useTranslate({
Expand All @@ -24,6 +25,7 @@ const ImageDemo = () => {
error: '加载失败',
lazyload: '图片懒加载',
imageText: 'Image + text 模式',
ARIAUsage: '无障碍使用示例',
},
'en-US': {
basic: 'Basic Usage',
Expand All @@ -34,6 +36,7 @@ const ImageDemo = () => {
error: 'Error',
lazyload: 'Lazyload',
imageText: 'Image + text ',
ARIAUsage: 'ARIA Usage',
},
})
return (
Expand Down Expand Up @@ -64,6 +67,8 @@ const ImageDemo = () => {
<Demo8 />
</>
)}
<View className="h2">{translated.ARIAUsage}</View>
<Demo9 />
</ScrollView>
</>
)
Expand Down
Loading
Loading