@elementor/editor-canvas 4.4.0-1149 → 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.
@@ -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 widgetConfig = widgetType ? getWidgetsCache()?.[ widgetType ] : null;
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: () => true },
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