Skip to content

Commit f0dce27

Browse files
authored
[APP-1923][APP-1924] manage remediations, global remediations (#424)
* [APP-1923] change remediation management (#411) * [APP-1988] Add tabs * [APP-1988][APP-1989] add tabs, remove menu item * [APP-1988][APP-1989] add tabs, remove menu item * [APP-1992] add empty state and tab navigation * [APP-1993][APP-1994] Change view for manage AI fixes, change delete confirmation modal * [APP-1996] add view for alt text fixes * [APP-1996] add edit for alt text fixes * [APP-1996] add edit for alt text fixes * [APP-1996] add edit for alt text fixes * [APP-1996] add edit for alt text fixes * [APP-1996] add edit for alt text fixes * [APP-1995] Color contrast form * [APP-2001] add global remediation logic (#415) * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2001] add global remediation logic * [APP-2003] add disable across scans (#418) * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * [APP-2003] add disable across scans * add logs * add logs * add logs * remove logs * remove logs * remove logs * remove logs * remove logs * remove logs * remove logs * remove logs * remove logs * remove logs * change to theme value * change to theme value * change to theme value * change to theme value * add edit mode * add edit mode * add edit mode
1 parent 0ed0311 commit f0dce27

File tree

150 files changed

+5926
-2105
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

150 files changed

+5926
-2105
lines changed

assets/dev/js/components/confirm-dialog/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AlertTriangleFilledIcon } from '@elementor/icons';
1+
import AlertTriangleFilledIcon from '@elementor/icons/AlertTriangleFilledIcon';
22
import Button from '@elementor/ui/Button';
33
import Dialog from '@elementor/ui/Dialog';
44
import DialogActions from '@elementor/ui/DialogActions';

assets/dev/js/components/notifications/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Alert from '@elementor/ui/Alert';
22
import Snackbar from '@elementor/ui/Snackbar';
3-
import { useNotificationSettings } from '@ea11y/hooks';
3+
import { useNotificationSettings } from '@ea11y-apps/global/hooks/use-notifications';
44

55
const Notifications = ({ type, message }) => {
66
const {
File renamed without changes.

assets/dev/js/services/mixpanel/mixpanel-events.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const mixpanelEvents = {
3838
fixWithAiButtonClicked: 'fix_with_ai_button_clicked',
3939
markAsDecorativeSelected: 'mark_as_decorative_selected',
4040
resolveButtonClicked: 'resolve_button_clicked',
41-
navigationImageClicked: 'navigation_image_clicked',
41+
navigationChanged: 'navigation_changed',
4242
aiSuggestionAccepted: 'ai_suggestion_accepted',
4343
markAsResolveClicked: 'mark_as_resolve_clicked',
4444
issueSkipped: 'issue_skipped',
@@ -50,6 +50,9 @@ export const mixpanelEvents = {
5050
contrastResetClicked: 'contrast_reset_clicked',
5151
backgroundAdaptorTriggered: 'background_adaptor_triggered',
5252
backgroundAdaptorChanged: 'background_adaptor_changed',
53+
tabSelected: 'tab_selected',
54+
markAsGlobalToggled: 'mark_as_global_toggled',
55+
applyGlobalFixConfirmationClicked: 'apply_global_fix_confirmation_clicked',
5356

5457
// Accessibility assistant dashboard
5558
assistantDashboardHistoryLogsButtonClicked: 'history_logs_button_clicked',
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import postcss from 'postcss';
2+
3+
export const isValidCSS = (cssText) => {
4+
try {
5+
// Basic checks for common malicious patterns
6+
if (!cssText || typeof cssText !== 'string') {
7+
return false;
8+
}
9+
postcss.parse(cssText);
10+
return true;
11+
} catch (e) {
12+
return false;
13+
}
14+
};
15+
16+
export const rgbOrRgbaToHex = (color) => {
17+
const match = color.match(
18+
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i,
19+
);
20+
if (!match) {
21+
return null;
22+
} // Not an RGB or RGBA string
23+
24+
const r = parseInt(match[1]).toString(16).padStart(2, '0');
25+
const g = parseInt(match[2]).toString(16).padStart(2, '0');
26+
const b = parseInt(match[3]).toString(16).padStart(2, '0');
27+
28+
// If alpha present and less than 1, include it
29+
if (match[4] !== undefined && parseFloat(match[4]) < 1) {
30+
const a = Math.round(parseFloat(match[4]) * 255)
31+
.toString(16)
32+
.padStart(2, '0');
33+
return `#${r}${g}${b}${a}`.toUpperCase(); // 8-digit hex with alpha
34+
}
35+
36+
return `#${r}${g}${b}`.toUpperCase(); // 6-digit hex
37+
};
38+
39+
export const getDataFromCss = (cssRules) => {
40+
const result = { color: null, background: null };
41+
42+
const ruleMatches = cssRules.matchAll(/([^{]+)\s*\{([^}]+)\}/g);
43+
44+
for (const [, selector, declarations] of ruleMatches) {
45+
let element;
46+
try {
47+
element = document.querySelector(selector.trim());
48+
} catch {
49+
continue; // Skip invalid selectors
50+
}
51+
52+
const colorMatch = declarations.match(
53+
/(?<!background-)color\s*:\s*([^;!]+)/i,
54+
);
55+
const bgMatch = declarations.match(/background-color\s*:\s*([^;!]+)/i);
56+
57+
const colorValue = colorMatch ? colorMatch[1].trim() : null;
58+
const bgValue = bgMatch ? bgMatch[1].trim() : null;
59+
60+
// Store values when explicitly found
61+
if (colorValue && !result.color) {
62+
result.color = { item: element, value: colorValue };
63+
}
64+
if (bgValue && !result.background) {
65+
result.background = { item: element, value: bgValue };
66+
}
67+
68+
// If both found → we're done
69+
if (result.color && result.background) {
70+
break;
71+
}
72+
}
73+
74+
// If one missing → use computed style from existing element
75+
const fallbackElement = result.color?.item || result.background?.item;
76+
77+
if (fallbackElement) {
78+
const computed = window.getComputedStyle(fallbackElement);
79+
if (!result.color) {
80+
result.color = {
81+
item: fallbackElement,
82+
value: rgbOrRgbaToHex(computed.getPropertyValue('color')),
83+
};
84+
}
85+
if (!result.background) {
86+
result.background = {
87+
item: fallbackElement,
88+
value: rgbOrRgbaToHex(computed.getPropertyValue('background-color')),
89+
};
90+
}
91+
}
92+
93+
return result;
94+
};

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"ext-zlib": "*",
3636
"ext-dom": "*",
3737
"elementor/wp-notifications-package": "^1.2.0",
38-
"ext-ctype": "*"
38+
"ext-ctype": "*",
39+
"ext-mbstring": "*"
3940
},
4041
"config": {
4142
"allow-plugins": {

modules/remediation/actions/attribute.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ class Attribute extends Remediation_Base {
1717
public static string $type = 'attribute';
1818

1919
public function run() : ?DOMDocument {
20-
$element_node = $this->get_element_by_xpath( $this->data['xpath'] );
20+
$element_node = $this->data['global']
21+
? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] )
22+
: $this->get_element_by_xpath( $this->data['xpath'] );
23+
2124
if ( ! $element_node ) {
25+
$this->use_frontend = true;
2226
return null;
2327
}
28+
2429
switch ( $this->data['action'] ) {
2530
case 'update':
2631
case 'add':

modules/remediation/actions/replace.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@ class Replace extends Remediation_Base {
1717
public static string $type = 'replace';
1818

1919
public function run() : ?DOMDocument {
20-
$element_node = $this->get_element_by_xpath( $this->data['xpath'] );
20+
$element_node = $this->data['global']
21+
? $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] )
22+
: $this->get_element_by_xpath( $this->data['xpath'] );
23+
2124
if ( ! $element_node instanceof \DOMElement ) {
25+
$this->use_frontend = true;
2226
return null; // nothing to do
2327
}
2428

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

2731
if ( stripos( $outer_html, $this->data['find'] ) === false ) {
28-
return $this->dom;
32+
$this->use_frontend = true;
33+
return null;
2934
}
3035

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

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

modules/remediation/actions/styles.php

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace EA11y\Modules\Remediation\Actions;
44

55
use DOMDocument;
6+
use DOMElement;
67
use EA11y\Modules\Remediation\Classes\Remediation_Base;
78

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

18-
public function __construct( DOMDocument $dom, $data ) {
19-
parent::__construct( $dom, $data );
2020

21-
$this->use_frontend = true;
21+
/**
22+
* Build a CSS selector string for a given DOMElement.
23+
*
24+
* @param DOMElement|null $element
25+
* @return string|null
26+
*/
27+
public function get_element_css_selector( ?DOMElement $element ): ?string {
28+
if ( ! $element ) {
29+
return null;
30+
}
31+
32+
$parts = [];
33+
34+
while ( $element && XML_ELEMENT_NODE === $element->nodeType ) {
35+
$selector = strtolower( $element->tagName );
36+
37+
// If element has ID, stop here
38+
if ( $element->hasAttribute( 'id' ) ) {
39+
$selector .= '#' . $element->getAttribute( 'id' );
40+
array_unshift( $parts, $selector );
41+
break;
42+
}
43+
44+
// Add classes unless body
45+
if ( $element->hasAttribute( 'class' ) && strtolower( $element->tagName ) !== 'body' ) {
46+
$classes = preg_split( '/\s+/', trim( $element->getAttribute( 'class' ) ) );
47+
if ( ! empty( $classes ) ) {
48+
$selector .= '.' . implode( '.', $classes );
49+
}
50+
}
51+
52+
// Add nth-of-type if needed
53+
$parent = $element->parentNode;
54+
if ( $parent instanceof DOMElement ) {
55+
$tag_name = $element->tagName;
56+
$siblings = [];
57+
foreach ( $parent->childNodes as $child ) {
58+
if ( $child instanceof DOMElement && $child->tagName === $tag_name ) {
59+
$siblings[] = $child;
60+
}
61+
}
62+
63+
if ( count( $siblings ) > 1 ) {
64+
foreach ( $siblings as $i => $sibling ) {
65+
if ( $sibling->isSameNode( $element ) ) {
66+
$selector .= ':nth-of-type(' . ( $i + 1 ) . ')';
67+
break;
68+
}
69+
}
70+
}
71+
}
72+
73+
array_unshift( $parts, $selector );
74+
$element = $element->parentNode instanceof DOMElement ? $element->parentNode : null;
75+
}
76+
77+
return implode( ' > ', $parts );
78+
}
79+
80+
/**
81+
* Replace CSS selectors for color and background-color rules.
82+
*
83+
* @param string $css The original CSS string
84+
* @param string|null $color_selector New selector for color rule
85+
* @param string|null $bg_selector New selector for background-color rule
86+
* @return string The modified CSS
87+
*/
88+
public function replace_css_selectors(
89+
string $css,
90+
?string $color_selector = null,
91+
?string $bg_selector = null
92+
): string {
93+
// Match full CSS blocks like "selector { ... }"
94+
preg_match_all( '/([^{]+)\{([^}]+)\}/', $css, $matches, PREG_SET_ORDER );
95+
96+
$result = '';
97+
98+
foreach ( $matches as $match ) {
99+
$rules = trim( $match[2] );
100+
101+
// Find color value
102+
if ( $color_selector && preg_match( '/(?<!-)\bcolor\s*:\s*([#a-zA-Z0-9(),.\s%-]+)/i', $rules, $color_match ) ) {
103+
$color_value = trim( $color_match[1] );
104+
$result .= "{$color_selector} { color: {$color_value} !important; }\n";
105+
}
106+
107+
// Find background-color value
108+
if ( $bg_selector && preg_match( '/background-color\s*:\s*([#a-zA-Z0-9(),.\s%-]+)/i', $rules, $bg_match ) ) {
109+
$bg_value = trim( $bg_match[1] );
110+
$result .= "{$bg_selector} { background-color: {$bg_value} !important; }\n";
111+
}
112+
}
113+
114+
return trim( $result );
22115
}
23116

117+
24118
public function run() : ?DOMDocument {
119+
$rule = $this->data['rule'];
120+
if ( $this->data['global'] ) {
121+
$el_color = $this->get_element_by_xpath_with_snippet_fallback( $this->data['xpath'], $this->data['find'] );
122+
$el_bg = $this->get_element_by_xpath_with_snippet_fallback( $this->data['parentXPath'], $this->data['parentFind'] );
123+
$color_css_selector = $this->get_element_css_selector( $el_color );
124+
$bg_css_selector = $this->get_element_css_selector( $el_bg );
125+
126+
if ( ! $color_css_selector && ! $bg_css_selector ) {
127+
$this->use_frontend = true;
128+
return null;
129+
}
130+
131+
$rule = $this->replace_css_selectors( $rule, $color_css_selector, $bg_css_selector );
132+
}
133+
134+
// Find or create <head> element
135+
$head = $this->dom->getElementsByTagName( 'head' )->item( 0 );
136+
if ( ! $head ) {
137+
$head = $this->dom->createElement( 'head' );
138+
$html_element = $this->dom->getElementsByTagName( 'html' )->item( 0 );
139+
if ( $html_element ) {
140+
$html_element->insertBefore( $head, $this->dom->getElementsByTagName( 'body' )->item( 0 ) );
141+
} else {
142+
$this->dom->appendChild( $head );
143+
}
144+
}
145+
146+
// Create <style> tag
147+
$style = $this->dom->getElementById( self::$style_id );
148+
if ( ! $style ) {
149+
$style = $this->dom->createElement( 'style' );
150+
}
151+
$style->setAttribute( 'id', self::$style_id );
152+
$style->appendChild( $this->dom->createTextNode( $rule ) );
153+
154+
// Append to the end of <head>
155+
$head->appendChild( $style );
156+
25157
return $this->dom;
26158
}
27159
}

modules/remediation/assets/js/actions/attribute.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ export class AttributeRemediation extends RemediationBase {
88
action,
99
attribute_name: attributeName,
1010
attribute_value: attributeValue,
11+
global: isGlobal,
1112
} = this.data;
1213

1314
const xpath = originXpath.replace('svg', "*[name()='svg']");
14-
const el = this.getElementByXPath(xpath);
15+
const el =
16+
isGlobal === '1'
17+
? this.getElementByXPathFallbackSnippet(find, xpath)
18+
: this.getElementByXPath(xpath);
1519
if (!el) {
1620
return false;
1721
}

0 commit comments

Comments
 (0)