Skip to content

Commit f620dcd

Browse files
authored
Email integration package (#18058)
Feature (table): Introduced Layout Tables to enable constructing grids with tables, for example for email editing. These tables are designed for layout purposes, and include `role="presentation"` for accessibility. Users can insert layout tables via the editor toolbar and switch between content and layout tables. The editing view now closely matches the rendered output. Closes #18132 Feature (table): Added the ability to toggle between Content Tables and Layout Tables. Users can switch table types using a split button in the table properties UI. While captions and `<th>` elements may be lost, table structure remains intact. Closes #18131 Feature (table): Dragging and dropping a table into another table no longer merges them. Instead, the dropped table is placed as a whole inside the target cell. Pasting tables remains unchanged. Closes #18126 Feature (html-support): Introducing the ability to render `<style>` elements from the `<head>` section of editor data content using `FullPage` plugin. See #13482.
1 parent 656d749 commit f620dcd

File tree

74 files changed

+7185
-126
lines changed

Some content is hidden

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

74 files changed

+7185
-126
lines changed

docs/framework/architecture/plugins.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
---
2-
# Scope:
3-
# * Introduction to plugins.
4-
# * Exemplify use cases.
5-
# * Point to resources to learn plugin development.
6-
72
category: framework-architecture
83
menu-title: Plugins in CKEditor 5
94
meta-title: Plugins in CKEditor 5 | CKEditor 5 Documentation
105
toc-limit: 1
116
order: 10
127
---
138

14-
# Plugins in CKEditor&nbsp;5
9+
# Plugins in CKEditor 5
1510

1611
Features in CKEditor are introduced by plugins. In fact, without plugins, CKEditor&nbsp;5 is an empty API with no use. Plugins provided by the CKEditor core team are available in [npm](https://www.npmjs.com/search?q=ckeditor5) (and [GitHub](https://github.com/ckeditor?utf8=%E2%9C%93&q=ckeditor5&type=&language=), too) in the form of npm packages. A package may contain one or more plugins (for example, the [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckeditor5-image) package contains {@link features/images-overview several granular plugins}).
1712

docs/umberto.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,11 @@
613613
}
614614
]
615615
},
616+
{
617+
"name": "Email editing",
618+
"id": "features-email",
619+
"slug": "email-editing"
620+
},
616621
{
617622
"name": "File management",
618623
"id": "features-file-management",

packages/ckeditor5-html-support/docs/features/full-page-html.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,68 @@ ClassicEditor
3333
.create( document.querySelector( '#editor' ), {
3434
licenseKey: '<YOUR_LICENSE_KEY>', // Or 'GPL'.
3535
plugins: [ FullPage, /* ... */ ],
36+
htmlSupport: {
37+
fullPage: {
38+
// Configuration.
39+
}
40+
}
3641
} )
3742
.then( /* ... */ )
3843
.catch( /* ... */ );
3944
```
4045
</code-switcher>
4146

47+
## Configuration
48+
49+
### Render styles
50+
51+
By default, the full page HTML feature does not render the CSS from `<style>` that may be located in the `<head>` section edited content. To enable that possibility, set the {@link module:html-support/generalhtmlsupportconfig~FullPageConfig#allowRenderStylesFromHead `config.htmlSupport.fullPage.allowRenderStylesFromHead`} option to `true`.
52+
53+
Plugin extracts `<style>` elements from the edited content moves them to the main document `<head>`, and renders them. When CSS in `<style>` tag is changed using, for example, the {@link features/source-editing-enhanced Enhanced source code editing} feature, previously added `<style>` elements to the main document `<head>` will be replaced by the new ones.
54+
55+
However, by enabling the ability to render CSS from `<style>` elements located in the `<head>` section of the edited content, you expose the users of your system to the **risk of executing malicious code inside the editor**. Therefore, we highly recommend sanitizing your CSS using some library that will strip the malicious code from the styles before rendering them. You can plug in the sanitizer by defining the {@link module:html-support/generalhtmlsupportconfig~FullPageConfig#sanitizeCss `config.htmlSupport.fullPage.sanitizeCss`} option.
56+
57+
```js
58+
ClassicEditor
59+
.create( document.querySelector( '#editor' ), {
60+
// ... Other configuration options ...
61+
htmlSupport: {
62+
fullPage: {
63+
allowRenderStylesFromHead: true,
64+
// Strip unsafe properties and values, for example:
65+
// values like url( ... ) that may execute malicious code
66+
// from an unknown source.
67+
sanitizeCss( CssString ) {
68+
const sanitizedCss = sanitize( CssString );
69+
70+
return {
71+
css: sanitizedCss,
72+
// true or false depending on whether
73+
// the sanitizer stripped anything.
74+
hasChanged: true
75+
};
76+
}
77+
}
78+
}
79+
} )
80+
.then( /* ... */ )
81+
.catch( /* ... */ );
82+
```
83+
84+
### Security
85+
86+
It is a plain security risk. The user may provide a CSS mistakenly copied from a malicious website. It could also end up in the user’s clipboard (as it would usually be copied and pasted) by any other means.
87+
88+
You can instruct some advanced users to never paste CSS code from untrusted sources. However, in most cases, it is highly recommended to secure the system by configuring the Full page HTML feature to use a CSS sanitizer and, optionally, by setting strict Content Security Policy (CSP) rules.
89+
90+
#### Sanitizer
91+
92+
The {@link module:html-support/generalhtmlsupportconfig~FullPageConfig#sanitizeCss `config.htmlSupport.fullPage.sanitizeCss`} option allows plugging an external sanitizer.
93+
94+
#### CSP
95+
96+
In addition to using a sanitizer, you can use the built-in browser mechanism called [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). By using CSP, you can let the browser know the allowed sources that CSS can use.
97+
4298
## Additional feature information
4399

44100
Here are some examples of the HTML elements you can enable with this plugin:

packages/ckeditor5-html-support/src/fullpage.ts

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@
77
* @module html-support/fullpage
88
*/
99

10-
import { Plugin } from 'ckeditor5/src/core.js';
11-
import { UpcastWriter, type DataControllerToModelEvent, type DataControllerToViewEvent } from 'ckeditor5/src/engine.js';
10+
import { Plugin, type Editor } from 'ckeditor5/src/core.js';
11+
import { logWarning, global } from 'ckeditor5/src/utils.js';
12+
import {
13+
UpcastWriter,
14+
type DataControllerToModelEvent,
15+
type DataControllerToViewEvent,
16+
type RootElement
17+
} from 'ckeditor5/src/engine.js';
18+
1219
import HtmlPageDataProcessor from './htmlpagedataprocessor.js';
1320

1421
/**
@@ -32,11 +39,39 @@ export default class FullPage extends Plugin {
3239
/**
3340
* @inheritDoc
3441
*/
35-
public init(): void {
36-
const editor = this.editor;
37-
const properties = [ '$fullPageDocument', '$fullPageDocType', '$fullPageXmlDeclaration' ];
42+
constructor( editor: Editor ) {
43+
super( editor );
44+
45+
editor.config.define( 'htmlSupport.fullPage', {
46+
allowRenderStylesFromHead: false,
47+
sanitizeCss: rawCss => {
48+
/**
49+
* When using the Full page with the `config.htmlSupport.fullPage.allowRenderStylesFromHead` set to `true`,
50+
* it is strongly recommended to define a sanitize function that will clean up the CSS
51+
* which is present in the `<head>` in editors content in order to avoid XSS vulnerability.
52+
*
53+
* For a detailed overview, check the {@glink features/html/full-page-html Full page HTML feature} documentation.
54+
*
55+
* @error css-full-page-provide-sanitize-function
56+
*/
57+
logWarning( 'css-full-page-provide-sanitize-function' );
58+
59+
return {
60+
css: rawCss,
61+
hasChanged: false
62+
};
63+
}
64+
} );
3865

3966
editor.data.processor = new HtmlPageDataProcessor( editor.data.viewDocument );
67+
}
68+
69+
/**
70+
* @inheritDoc
71+
*/
72+
public init(): void {
73+
const editor = this.editor;
74+
const properties = [ '$fullPageDocument', '$fullPageDocType', '$fullPageXmlDeclaration', '$fullPageHeadStyles' ];
4075

4176
editor.model.schema.extend( '$root', {
4277
allowAttributes: properties
@@ -55,6 +90,10 @@ export default class FullPage extends Plugin {
5590
}
5691
}
5792
} );
93+
94+
if ( isAllowedRenderStylesFromHead( editor ) ) {
95+
this._renderStylesFromHead( root );
96+
}
5897
}, { priority: 'low' } );
5998

6099
// Apply root attributes to the view document fragment.
@@ -103,4 +142,76 @@ export default class FullPage extends Plugin {
103142
args[ 0 ].trim = false;
104143
}, { priority: 'high' } );
105144
}
145+
146+
/**
147+
* @inheritDoc
148+
*/
149+
public override destroy(): void {
150+
super.destroy();
151+
152+
if ( isAllowedRenderStylesFromHead( this.editor ) ) {
153+
this._removeStyleElementsFromDom();
154+
}
155+
}
156+
157+
/**
158+
* Checks if in the document exists any `<style>` elements injected by the plugin and removes them,
159+
* so these could be re-rendered later.
160+
* There is used `data-full-page-style-id` attribute to recognize styles injected by the feature.
161+
*/
162+
private _removeStyleElementsFromDom(): void {
163+
const existingStyleElements = Array.from(
164+
global.document.querySelectorAll( `[data-full-page-style-id="${ this.editor.id }"]` )
165+
);
166+
167+
for ( const style of existingStyleElements ) {
168+
style.remove();
169+
}
170+
}
171+
172+
/**
173+
* Extracts `<style>` elements from the full page data and renders them in the main document `<head>`.
174+
* CSS content is sanitized before rendering.
175+
*/
176+
private _renderStyleElementsInDom( root: RootElement ): void {
177+
const editor = this.editor;
178+
179+
// Get `<style>` elements list from the `<head>` from the full page data.
180+
const styleElements = root.getAttribute( '$fullPageHeadStyles' ) as Array<HTMLStyleElement> | undefined;
181+
182+
if ( !styleElements ) {
183+
return;
184+
}
185+
186+
const sanitizeCss = editor.config.get( 'htmlSupport.fullPage.sanitizeCss' )!;
187+
188+
// Add `data-full-page-style-id` attribute to the `<style>` element and render it in `<head>` in the main document.
189+
for ( const style of styleElements ) {
190+
style.setAttribute( 'data-full-page-style-id', editor.id );
191+
192+
// Sanitize the CSS content before rendering it in the editor.
193+
const sanitizedCss = sanitizeCss( style.innerText );
194+
195+
if ( sanitizedCss.hasChanged ) {
196+
style.innerText = sanitizedCss.css;
197+
}
198+
199+
global.document.head.append( style );
200+
}
201+
}
202+
203+
/**
204+
* Removes existing `<style>` elements injected by the plugin and renders new ones from the full page data.
205+
*/
206+
private _renderStylesFromHead( root: RootElement ): void {
207+
this._removeStyleElementsFromDom();
208+
this._renderStyleElementsInDom( root );
209+
}
210+
}
211+
212+
/**
213+
* Normalize the Full page configuration option `allowRenderStylesFromHead`.
214+
*/
215+
function isAllowedRenderStylesFromHead( editor: Editor ): boolean {
216+
return editor.config.get( 'htmlSupport.fullPage.allowRenderStylesFromHead' )!;
106217
}

packages/ckeditor5-html-support/src/generalhtmlsupportconfig.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,97 @@ export interface GeneralHtmlSupportConfig {
9393
* @default false
9494
*/
9595
preserveEmptyBlocksInEditingView?: boolean;
96+
97+
/**
98+
* The configuration of the Full page editing feature.
99+
* The option is used by the {@link module:html-support/fullpage~FullPage} feature.
100+
*
101+
* ```ts
102+
* ClassicEditor
103+
* .create( {
104+
* htmlSupport: {
105+
* fullPage: ... // Full page feature config.
106+
* }
107+
* } )
108+
* .then( ... )
109+
* .catch( ... );
110+
* ```
111+
*/
112+
fullPage?: FullPageConfig;
113+
}
114+
115+
/**
116+
* The configuration of the Full page editing feature.
117+
*/
118+
export interface FullPageConfig {
119+
120+
/**
121+
* Whether the feature should allow the editor to render styles from the `<head>` section of editor data content.
122+
*
123+
* When set to `true`, the editor will render styles from the `<head>` section of editor data content.
124+
*
125+
* ```ts
126+
* ClassicEditor
127+
* .create( {
128+
* htmlSupport: {
129+
* fullPage: {
130+
* allowRenderStylesFromHead: true
131+
* }
132+
* }
133+
* } )
134+
* .then( ... )
135+
* .catch( ... );
136+
* ```
137+
*
138+
* @default false
139+
*/
140+
allowRenderStylesFromHead?: boolean;
141+
142+
/**
143+
* Callback used to sanitize the CSS provided by the user in editor content
144+
* when option `htmlSupport.fullPage.allowRenderStylesFromHead` is set to `true`.
145+
*
146+
* We strongly recommend overwriting the default function to avoid XSS vulnerabilities.
147+
*
148+
* The function receives the CSS (as a string), and should return an object
149+
* that matches the {@link module:html-support/generalhtmlsupportconfig~CssSanitizeOutput} interface.
150+
*
151+
* ```ts
152+
* ClassicEditor
153+
* .create( editorElement, {
154+
* htmlSupport: {
155+
* fullPage: {
156+
* allowRenderStylesFromHead: true,
157+
*
158+
* sanitizeCss( CssString ) {
159+
* const sanitizedCss = sanitize( CssString );
160+
*
161+
* return {
162+
* css: sanitizedCss,
163+
* // true or false depending on whether the sanitizer stripped anything.
164+
* hasChanged: ...
165+
* };
166+
* }
167+
* }
168+
* }
169+
* } )
170+
* .then( ... )
171+
* .catch( ... );
172+
* ```
173+
*
174+
*/
175+
sanitizeCss?: ( css: string ) => CssSanitizeOutput;
176+
}
177+
178+
export interface CssSanitizeOutput {
179+
180+
/**
181+
* An output (safe) CSS that will be inserted into the document.
182+
*/
183+
css: string;
184+
185+
/**
186+
* A flag that indicates whether the output CSS is different than the input value.
187+
*/
188+
hasChanged: boolean;
96189
}

packages/ckeditor5-html-support/src/htmlpagedataprocessor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export default class HtmlPageDataProcessor extends HtmlDataProcessor {
5353
// Using the DOM document with body content extracted as a skeleton of the page.
5454
writer.setCustomProperty( '$fullPageDocument', domFragment.ownerDocument.documentElement.outerHTML, viewFragment );
5555

56+
// List of `<style>` elements extracted from document's `<head>` element.
57+
const headStylesElements = Array.from( domFragment.ownerDocument.querySelectorAll( 'head style' ) );
58+
59+
writer.setCustomProperty( '$fullPageHeadStyles', headStylesElements, viewFragment );
60+
5661
if ( docType ) {
5762
writer.setCustomProperty( '$fullPageDocType', docType, viewFragment );
5863
}

packages/ckeditor5-html-support/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export { default as GeneralHtmlSupport } from './generalhtmlsupport.js';
11-
export { default as DataFilter } from './datafilter.js';
11+
export { default as DataFilter, type DataFilterRegisterEvent } from './datafilter.js';
1212
export { default as DataSchema, type DataSchemaBlockElementDefinition } from './dataschema.js';
1313
export { default as HtmlComment } from './htmlcomment.js';
1414
export { default as FullPage } from './fullpage.js';

0 commit comments

Comments
 (0)