@elementor/editor-canvas 4.4.0-beta1 → 4.4.0-beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +155 -124
- package/dist/index.mjs +50 -19
- package/package.json +20 -20
- package/src/components/__tests__/style-renderer.test.tsx +1 -0
- package/src/components/style-renderer.tsx +1 -1
- package/src/hooks/__tests__/use-style-items.test.ts +162 -1
- package/src/hooks/use-pregenerated-links-handoff.ts +33 -0
- package/src/hooks/use-style-items.ts +5 -7
- package/src/mcp/resources/__tests__/selected-element-resource.test.ts +106 -0
- package/src/mcp/resources/selected-element-resource.ts +11 -1
- package/src/mcp/tools/configure-element/__tests__/tool.test.ts +53 -1
- package/src/mcp/tools/configure-element/tool.ts +1 -1
- package/src/renderers/__tests__/create-dom-renderer.test.ts +24 -0
- package/src/renderers/create-dom-renderer.ts +10 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getContainer, getSelectedElements, getWidgetsCache, type V1Element } from '@elementor/editor-elements';
|
|
2
2
|
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
3
|
+
import { Schema } from '@elementor/editor-props';
|
|
3
4
|
import {
|
|
4
5
|
__privateListenTo as listenTo,
|
|
5
6
|
commandEndEvent,
|
|
@@ -167,9 +168,14 @@ function getElementProperties(
|
|
|
167
168
|
return null;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
const
|
|
171
|
+
const widgetsCache = getWidgetsCache();
|
|
172
|
+
const widgetConfig = widgetType ? widgetsCache?.[ widgetType ] : null;
|
|
171
173
|
const controls = widgetConfig?.controls as Record< string, { default?: unknown } > | undefined;
|
|
172
174
|
|
|
175
|
+
// Atomic elements (e.g. e-div-block, e-form) have no widgetType, their config is keyed by elType.
|
|
176
|
+
const schemaType = widgetType ?? container.model.get( 'elType' ) ?? null;
|
|
177
|
+
const propsSchema = schemaType ? widgetsCache?.[ schemaType ]?.atomic_props_schema : undefined;
|
|
178
|
+
|
|
173
179
|
const filtered: Record< string, unknown > = {};
|
|
174
180
|
|
|
175
181
|
for ( const [ key, value ] of Object.entries( settings ) ) {
|
|
@@ -177,6 +183,10 @@ function getElementProperties(
|
|
|
177
183
|
continue;
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
if ( Schema.isPropSensitive( propsSchema?.[ key ] ) ) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
const controlDefault = controls?.[ key ]?.default;
|
|
181
191
|
if ( controlDefault !== undefined && JSON.stringify( value ) === JSON.stringify( controlDefault ) ) {
|
|
182
192
|
continue;
|
|
@@ -13,7 +13,7 @@ jest.mock( '@elementor/editor-mcp', () => ( {
|
|
|
13
13
|
} ) );
|
|
14
14
|
|
|
15
15
|
jest.mock( '@elementor/editor-props', () => ( {
|
|
16
|
-
Schema: { isPropKeyConfigurable: (
|
|
16
|
+
Schema: { isPropKeyConfigurable: jest.requireActual( '@elementor/editor-props' ).Schema.isPropKeyConfigurable },
|
|
17
17
|
} ) );
|
|
18
18
|
|
|
19
19
|
jest.mock( '../prompt', () => ( {
|
|
@@ -128,3 +128,55 @@ describe( 'configure-element handler unsupported props', () => {
|
|
|
128
128
|
).rejects.toThrow( /Invalid PropValue/ );
|
|
129
129
|
} );
|
|
130
130
|
} );
|
|
131
|
+
|
|
132
|
+
describe( 'configure-element handler sensitive props', () => {
|
|
133
|
+
beforeEach( () => {
|
|
134
|
+
jest.mocked( getWidgetsCache ).mockReturnValue( {
|
|
135
|
+
[ ELEMENT_TYPE ]: {
|
|
136
|
+
atomic_props_schema: {
|
|
137
|
+
title: { key: 'string', kind: 'plain', settings: {}, meta: {} },
|
|
138
|
+
api_key: { key: 'string', kind: 'plain', settings: {}, meta: { sensitive: true } },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
} as never );
|
|
142
|
+
jest.mocked( getContainer ).mockReturnValue( {
|
|
143
|
+
settings: { get: () => ELEMENT_TYPE },
|
|
144
|
+
} as never );
|
|
145
|
+
jest.mocked( doUpdateElementProperty ).mockReset();
|
|
146
|
+
} );
|
|
147
|
+
|
|
148
|
+
it( 'rejects a write to a sensitive prop', async () => {
|
|
149
|
+
// Arrange
|
|
150
|
+
const handler = getHandler();
|
|
151
|
+
|
|
152
|
+
// Act & Assert
|
|
153
|
+
await expect(
|
|
154
|
+
handler( {
|
|
155
|
+
elementId: ELEMENT_ID,
|
|
156
|
+
elementType: ELEMENT_TYPE,
|
|
157
|
+
propertiesToChange: { api_key: { $$type: 'string', value: 'secret' } },
|
|
158
|
+
style: {},
|
|
159
|
+
} )
|
|
160
|
+
).rejects.toThrow( 'Not allowed to update api_key' );
|
|
161
|
+
|
|
162
|
+
expect( doUpdateElementProperty ).not.toHaveBeenCalled();
|
|
163
|
+
} );
|
|
164
|
+
|
|
165
|
+
it( 'still writes non sensitive props', async () => {
|
|
166
|
+
// Arrange
|
|
167
|
+
jest.mocked( doUpdateElementProperty ).mockImplementation( () => undefined );
|
|
168
|
+
const handler = getHandler();
|
|
169
|
+
|
|
170
|
+
// Act
|
|
171
|
+
const result = await handler( {
|
|
172
|
+
elementId: ELEMENT_ID,
|
|
173
|
+
elementType: ELEMENT_TYPE,
|
|
174
|
+
propertiesToChange: { title: { $$type: 'string', value: 'Hi' } },
|
|
175
|
+
style: {},
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
// Assert
|
|
179
|
+
expect( result.success ).toBe( true );
|
|
180
|
+
expect( doUpdateElementProperty ).toHaveBeenCalledTimes( 1 );
|
|
181
|
+
} );
|
|
182
|
+
} );
|
|
@@ -63,7 +63,7 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
63
63
|
const toUpdate = Object.entries( propertiesToUpdate );
|
|
64
64
|
const skippedProps: string[] = [];
|
|
65
65
|
for ( const [ propertyName, propertyValue ] of toUpdate as [ string, PropValue ][] ) {
|
|
66
|
-
if ( ! Schema.isPropKeyConfigurable( propertyName ) ) {
|
|
66
|
+
if ( ! Schema.isPropKeyConfigurable( propertyName, widgetData.atomic_props_schema[ propertyName ] ) ) {
|
|
67
67
|
throw new Error( `Not allowed to update ${ propertyName }` );
|
|
68
68
|
}
|
|
69
69
|
try {
|
|
@@ -77,6 +77,30 @@ describe( 'createDomRenderer', () => {
|
|
|
77
77
|
context: { url: 'javascript:alert(123)' },
|
|
78
78
|
expected: '',
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
title: 'alphanumeric element id in an alpine expression',
|
|
82
|
+
template: `x-data="{% if (id | e( 'js_identifier' )) != '' %}eTabs{{ id | e( 'js_identifier' ) }}{% else %}{}{% endif %}"`,
|
|
83
|
+
context: { id: 'tabscontainer' },
|
|
84
|
+
expected: 'x-data="eTabstabscontainer"',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: 'element id with underscore in an alpine expression',
|
|
88
|
+
template: `x-data="{% if (id | e( 'js_identifier' )) != '' %}eForm{{ id | e( 'js_identifier' ) }}{% else %}{}{% endif %}"`,
|
|
89
|
+
context: { id: 'test_atomic_form_instance' },
|
|
90
|
+
expected: 'x-data="eFormtest_atomic_form_instance"',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
title: 'element id that is a javascript expression',
|
|
94
|
+
template: `x-data="{% if (id | e( 'js_identifier' )) != '' %}eTabs{{ id | e( 'js_identifier' ) }}{% else %}{}{% endif %}"`,
|
|
95
|
+
context: { id: '=window.alert(document.domain)' },
|
|
96
|
+
expected: 'x-data="{}"',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
title: 'element id with a hyphen',
|
|
100
|
+
template: `x-data="{% if (id | e( 'js_identifier' )) != '' %}eBackgroundVideo{{ id | e( 'js_identifier' ) }}{% else %}{}{% endif %}"`,
|
|
101
|
+
context: { id: 'foo-alert(1)' },
|
|
102
|
+
expected: 'x-data="{}"',
|
|
103
|
+
},
|
|
80
104
|
] )( 'should render template with $title', async ( { template, context, expected } ) => {
|
|
81
105
|
// Arrange.
|
|
82
106
|
const domRenderer = createDomRenderer();
|
|
@@ -11,6 +11,7 @@ export function createDomRenderer(): DomRenderer {
|
|
|
11
11
|
|
|
12
12
|
environment.registerEscapingStrategy( escapeHtmlTag, 'html_tag' );
|
|
13
13
|
environment.registerEscapingStrategy( escapeURL, 'full_url' );
|
|
14
|
+
environment.registerEscapingStrategy( escapeJsIdentifier, 'js_identifier' );
|
|
14
15
|
|
|
15
16
|
return {
|
|
16
17
|
register: loader.setTemplate,
|
|
@@ -34,6 +35,15 @@ function escapeHtmlTag( value: string ) {
|
|
|
34
35
|
return allowedTags.includes( normalizedTag ) ? value : 'div';
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const JS_IDENTIFIER_PATTERN = /^[A-Za-z0-9_]+$/;
|
|
39
|
+
|
|
40
|
+
function escapeJsIdentifier( value: unknown ): string {
|
|
41
|
+
const id = String( value ?? '' );
|
|
42
|
+
const match = id.match( JS_IDENTIFIER_PATTERN );
|
|
43
|
+
|
|
44
|
+
return match?.[ 0 ] === id ? id : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
function escapeURL( value: string ) {
|
|
38
48
|
const allowedProtocols = [ 'http:', 'https:', 'mailto:', 'tel:' ];
|
|
39
49
|
|