( page: Page, selector: string, property: string, value: string )
| 493 | * Uses CSS.setStyleTexts in headed mode, falls back to inline style in headless. |
| 494 | */ |
| 495 | export async function modifyStyle( |
| 496 | page: Page, |
| 497 | selector: string, |
| 498 | property: string, |
| 499 | value: string |
| 500 | ): Promise<StyleModification> { |
| 501 | // Validate CSS property name |
| 502 | if (!/^[a-zA-Z-]+$/.test(property)) { |
| 503 | throw new Error(`Invalid CSS property name: ${property}. Only letters and hyphens allowed.`); |
| 504 | } |
| 505 | |
| 506 | // Validate CSS value — block data exfiltration patterns |
| 507 | const DANGEROUS_CSS = /url\s*\(|expression\s*\(|@import|javascript:|data:/i; |
| 508 | if (DANGEROUS_CSS.test(value)) { |
| 509 | throw new Error('CSS value rejected: contains potentially dangerous pattern.'); |
| 510 | } |
| 511 | |
| 512 | let oldValue = ''; |
| 513 | let source = 'inline'; |
| 514 | let sourceLine = 0; |
| 515 | let method: 'setStyleTexts' | 'inline' = 'inline'; |
| 516 | |
| 517 | try { |
| 518 | // Try CDP approach first |
| 519 | const session = await getOrCreateSession(page); |
| 520 | const result = await inspectElement(page, selector); |
| 521 | oldValue = result.computedStyles[property] || ''; |
| 522 | |
| 523 | // Find the most-specific matching rule that has this property |
| 524 | let targetRule: InspectorResult['matchedRules'][0] | null = null; |
| 525 | for (const rule of result.matchedRules) { |
| 526 | if (rule.userAgent) continue; |
| 527 | const hasProp = rule.properties.some(p => p.name === property); |
| 528 | if (hasProp && rule.styleSheetId && rule.range) { |
| 529 | targetRule = rule; |
| 530 | break; |
| 531 | } |
| 532 | } |
| 533 | |
| 534 | if (targetRule?.styleSheetId && targetRule.range) { |
| 535 | // Modify via CSS.setStyleTexts |
| 536 | const range = targetRule.range as any; |
| 537 | |
| 538 | // Get current style text |
| 539 | const styleText = await session.send('CSS.getStyleSheetText', { |
| 540 | styleSheetId: targetRule.styleSheetId, |
| 541 | }); |
| 542 | |
| 543 | // Build new style text by replacing the property value |
| 544 | const currentProps = targetRule.properties; |
| 545 | const newPropsText = currentProps |
| 546 | .map(p => { |
| 547 | if (p.name === property) { |
| 548 | return `${p.name}: ${value}`; |
| 549 | } |
| 550 | return `${p.name}: ${p.value}`; |
| 551 | }) |
| 552 | .join('; '); |
no test coverage detected