Skip to content
Merged
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 assets/dev/js/components/confirm-dialog/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AlertTriangleFilledIcon } from '@elementor/icons';
import AlertTriangleFilledIcon from '@elementor/icons/AlertTriangleFilledIcon';
import Button from '@elementor/ui/Button';
import Dialog from '@elementor/ui/Dialog';
import DialogActions from '@elementor/ui/DialogActions';
Expand Down
2 changes: 1 addition & 1 deletion assets/dev/js/components/notifications/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Alert from '@elementor/ui/Alert';
import Snackbar from '@elementor/ui/Snackbar';
import { useNotificationSettings } from '@ea11y/hooks';
import { useNotificationSettings } from '@ea11y-apps/global/hooks/use-notifications';

const Notifications = ({ type, message }) => {
const {
Expand Down
5 changes: 4 additions & 1 deletion assets/dev/js/services/mixpanel/mixpanel-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const mixpanelEvents = {
fixWithAiButtonClicked: 'fix_with_ai_button_clicked',
markAsDecorativeSelected: 'mark_as_decorative_selected',
resolveButtonClicked: 'resolve_button_clicked',
navigationImageClicked: 'navigation_image_clicked',
navigationChanged: 'navigation_changed',
aiSuggestionAccepted: 'ai_suggestion_accepted',
markAsResolveClicked: 'mark_as_resolve_clicked',
issueSkipped: 'issue_skipped',
Expand All @@ -50,6 +50,9 @@ export const mixpanelEvents = {
contrastResetClicked: 'contrast_reset_clicked',
backgroundAdaptorTriggered: 'background_adaptor_triggered',
backgroundAdaptorChanged: 'background_adaptor_changed',
tabSelected: 'tab_selected',
markAsGlobalToggled: 'mark_as_global_toggled',
applyGlobalFixConfirmationClicked: 'apply_global_fix_confirmation_clicked',

// Accessibility assistant dashboard
assistantDashboardHistoryLogsButtonClicked: 'history_logs_button_clicked',
Expand Down
94 changes: 94 additions & 0 deletions assets/dev/js/utils/color-contrast-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import postcss from 'postcss';

export const isValidCSS = (cssText) => {
try {
// Basic checks for common malicious patterns
if (!cssText || typeof cssText !== 'string') {
return false;
}
postcss.parse(cssText);
return true;
} catch (e) {
return false;
}
};

export const rgbOrRgbaToHex = (color) => {
const match = color.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i,
);
if (!match) {
return null;
} // Not an RGB or RGBA string

const r = parseInt(match[1]).toString(16).padStart(2, '0');
const g = parseInt(match[2]).toString(16).padStart(2, '0');
const b = parseInt(match[3]).toString(16).padStart(2, '0');

// If alpha present and less than 1, include it
if (match[4] !== undefined && parseFloat(match[4]) < 1) {
const a = Math.round(parseFloat(match[4]) * 255)
.toString(16)
.padStart(2, '0');
return `#${r}${g}${b}${a}`.toUpperCase(); // 8-digit hex with alpha
}

return `#${r}${g}${b}`.toUpperCase(); // 6-digit hex
};

export const getDataFromCss = (cssRules) => {
const result = { color: null, background: null };

const ruleMatches = cssRules.matchAll(/([^{]+)\s*\{([^}]+)\}/g);

for (const [, selector, declarations] of ruleMatches) {
let element;
try {
element = document.querySelector(selector.trim());
} catch {
continue; // Skip invalid selectors
}

const colorMatch = declarations.match(
/(?<!background-)color\s*:\s*([^;!]+)/i,
);
const bgMatch = declarations.match(/background-color\s*:\s*([^;!]+)/i);

const colorValue = colorMatch ? colorMatch[1].trim() : null;
const bgValue = bgMatch ? bgMatch[1].trim() : null;

// Store values when explicitly found
if (colorValue && !result.color) {
result.color = { item: element, value: colorValue };
}
if (bgValue && !result.background) {
result.background = { item: element, value: bgValue };
}

// If both found → we're done
if (result.color && result.background) {
break;
}
}

// If one missing → use computed style from existing element
const fallbackElement = result.color?.item || result.background?.item;

if (fallbackElement) {
const computed = window.getComputedStyle(fallbackElement);
if (!result.color) {
result.color = {
item: fallbackElement,
value: rgbOrRgbaToHex(computed.getPropertyValue('color')),
};
}
if (!result.background) {
result.background = {
item: fallbackElement,
value: rgbOrRgbaToHex(computed.getPropertyValue('background-color')),
};
}
}

return result;
};
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"ext-zlib": "*",
"ext-dom": "*",
"elementor/wp-notifications-package": "^1.2.0",
"ext-ctype": "*"
"ext-ctype": "*",
"ext-mbstring": "*"
},
"config": {
"allow-plugins": {
Expand Down
7 changes: 6 additions & 1 deletion modules/remediation/actions/attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ class Attribute extends Remediation_Base {
public static string $type = 'attribute';

public function run() : ?DOMDocument {
$element_node = $this->get_element_by_xpath( $this->data['xpath'] );
$element_node = $this->data['global']
? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] )
: $this->get_element_by_xpath( $this->data['xpath'] );

if ( ! $element_node ) {
$this->use_frontend = true;
return null;
}

switch ( $this->data['action'] ) {
case 'update':
case 'add':
Expand Down
11 changes: 8 additions & 3 deletions modules/remediation/actions/replace.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@ class Replace extends Remediation_Base {
public static string $type = 'replace';

public function run() : ?DOMDocument {
$element_node = $this->get_element_by_xpath( $this->data['xpath'] );
$element_node = $this->data['global']
? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] )
: $this->get_element_by_xpath( $this->data['xpath'] );

if ( ! $element_node instanceof \DOMElement ) {
$this->use_frontend = true;
return null; // nothing to do
}

$outer_html = $this->dom->saveHTML( $element_node );

if ( stripos( $outer_html, $this->data['find'] ) === false ) {
return $this->dom;
$this->use_frontend = true;
return null;
}

$updated_html = str_ireplace( $this->data['find'], $this->data['replace'], $outer_html );
Expand All @@ -36,7 +41,7 @@ public function run() : ?DOMDocument {

$tmp_dom = new DOMDocument( '1.0', 'UTF-8' );
$tmp_dom->loadHTML(
mb_convert_encoding( $updated_html, 'HTML-ENTITIES', 'UTF-8' ),
mb_encode_numericentity( $updated_html, [ 0x80, 0x10FFFF, 0, 0x1FFFFF ], 'UTF-8' ),
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING
);

Expand Down
138 changes: 135 additions & 3 deletions modules/remediation/actions/styles.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace EA11y\Modules\Remediation\Actions;

use DOMDocument;
use DOMElement;
use EA11y\Modules\Remediation\Classes\Remediation_Base;

if ( ! defined( 'ABSPATH' ) ) {
Expand All @@ -14,14 +15,145 @@
*/
class Styles extends Remediation_Base {
public static string $type = 'styles';
public static string $style_id = 'ea11y-remediation-styles';

public function __construct( DOMDocument $dom, $data ) {
parent::__construct( $dom, $data );

$this->use_frontend = true;
/**
* Build a CSS selector string for a given DOMElement.
*
* @param DOMElement|null $element
* @return string|null
*/
public function get_element_css_selector( ?DOMElement $element ): ?string {
if ( ! $element ) {
return null;
}

$parts = [];

while ( $element && XML_ELEMENT_NODE === $element->nodeType ) {
$selector = strtolower( $element->tagName );

// If element has ID, stop here
if ( $element->hasAttribute( 'id' ) ) {
$selector .= '#' . $element->getAttribute( 'id' );
array_unshift( $parts, $selector );
break;
}

// Add classes unless body
if ( $element->hasAttribute( 'class' ) && strtolower( $element->tagName ) !== 'body' ) {
$classes = preg_split( '/\s+/', trim( $element->getAttribute( 'class' ) ) );
if ( ! empty( $classes ) ) {
$selector .= '.' . implode( '.', $classes );
}
}

// Add nth-of-type if needed
$parent = $element->parentNode;
if ( $parent instanceof DOMElement ) {
$tag_name = $element->tagName;
$siblings = [];
foreach ( $parent->childNodes as $child ) {
if ( $child instanceof DOMElement && $child->tagName === $tag_name ) {
$siblings[] = $child;
}
}

if ( count( $siblings ) > 1 ) {
foreach ( $siblings as $i => $sibling ) {
if ( $sibling->isSameNode( $element ) ) {
$selector .= ':nth-of-type(' . ( $i + 1 ) . ')';
break;
}
}
}
}

array_unshift( $parts, $selector );
$element = $element->parentNode instanceof DOMElement ? $element->parentNode : null;
}

return implode( ' > ', $parts );
}

/**
* Replace CSS selectors for color and background-color rules.
*
* @param string $css The original CSS string
* @param string|null $color_selector New selector for color rule
* @param string|null $bg_selector New selector for background-color rule
* @return string The modified CSS
*/
public function replace_css_selectors(
string $css,
?string $color_selector = null,
?string $bg_selector = null
): string {
// Match full CSS blocks like "selector { ... }"
preg_match_all( '/([^{]+)\{([^}]+)\}/', $css, $matches, PREG_SET_ORDER );

$result = '';

foreach ( $matches as $match ) {
$rules = trim( $match[2] );

// Find color value
if ( $color_selector && preg_match( '/(?<!-)\bcolor\s*:\s*([#a-zA-Z0-9(),.\s%-]+)/i', $rules, $color_match ) ) {
$color_value = trim( $color_match[1] );
$result .= "{$color_selector} { color: {$color_value} !important; }\n";
}

// Find background-color value
if ( $bg_selector && preg_match( '/background-color\s*:\s*([#a-zA-Z0-9(),.\s%-]+)/i', $rules, $bg_match ) ) {
$bg_value = trim( $bg_match[1] );
$result .= "{$bg_selector} { background-color: {$bg_value} !important; }\n";
}
}

return trim( $result );
}


public function run() : ?DOMDocument {
$rule = $this->data['rule'];
if ( $this->data['global'] ) {
$el_color = $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] );
$el_bg = $this->get_element_by_xpath_with_snippet_fallback( $this->data['parentXPath'], $this->data['parentFind'] );
$color_css_selector = $this->get_element_css_selector( $el_color );
$bg_css_selector = $this->get_element_css_selector( $el_bg );

if ( ! $color_css_selector && ! $bg_css_selector ) {
$this->use_frontend = true;
return null;
}

$rule = $this->replace_css_selectors( $rule, $color_css_selector, $bg_css_selector );
}

// Find or create <head> element
$head = $this->dom->getElementsByTagName( 'head' )->item( 0 );
if ( ! $head ) {
$head = $this->dom->createElement( 'head' );
$html_element = $this->dom->getElementsByTagName( 'html' )->item( 0 );
if ( $html_element ) {
$html_element->insertBefore( $head, $this->dom->getElementsByTagName( 'body' )->item( 0 ) );
} else {
$this->dom->appendChild( $head );
}
}

// Create <style> tag
$style = $this->dom->getElementById( self::$style_id );
if ( ! $style ) {
$style = $this->dom->createElement( 'style' );
}
$style->setAttribute( 'id', self::$style_id );
$style->appendChild( $this->dom->createTextNode( $rule ) );

// Append to the end of <head>
$head->appendChild( $style );

return $this->dom;
}
}
6 changes: 5 additions & 1 deletion modules/remediation/assets/js/actions/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ export class AttributeRemediation extends RemediationBase {
action,
attribute_name: attributeName,
attribute_value: attributeValue,
global: isGlobal,
} = this.data;

const xpath = originXpath.replace('svg', "*[name()='svg']");
const el = this.getElementByXPath(xpath);
const el =
isGlobal === '1'
? this.getElementByXPathFallbackSnippet(find, xpath)
: this.getElementByXPath(xpath);
if (!el) {
return false;
}
Expand Down
Loading
Loading