Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render <style> elements from <head> section of editor data content when using fullPage plugin. #17880

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
110 changes: 108 additions & 2 deletions packages/ckeditor5-html-support/src/fullpage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@
* @module html-support/fullpage
*/

import { type Editor, Plugin } from 'ckeditor5/src/core.js';
import { Plugin, type Editor } from 'ckeditor5/src/core.js';
import { logWarning, global } from 'ckeditor5/src/utils.js';
import { UpcastWriter, type DataControllerToModelEvent, type DataControllerToViewEvent } from 'ckeditor5/src/engine.js';

import HtmlPageDataProcessor from './htmlpagedataprocessor.js';

/**
* The full page editing feature. It preserves the whole HTML page in the editor data.
*/
export default class FullPage extends Plugin {
/**
* @inheritDoc
*/
public htmlDataProcessor: HtmlPageDataProcessor;

/**
* @inheritDoc
*/
Expand All @@ -35,7 +42,29 @@ export default class FullPage extends Plugin {
constructor( editor: Editor ) {
super( editor );

editor.data.processor = new HtmlPageDataProcessor( editor.data.viewDocument );
editor.config.define( 'htmlSupport.fullPage', {
allowRenderStylesFromHead: false,
sanitizeCss: rawCss => {
/**
* When using the Full page with the `config.htmlSupport.fullPage.allowRenderStylesFromHead` set to `true`,
* it is strongly recommended to define a sanitize function that will clean up the CSS
* which is present in the `<head>` in editors content in order to avoid XSS vulnerability.
*
* For a detailed overview, check the {@glink TODO} documentation.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When docs will be updated, proper link will be added.

*
* @error html-embed-provide-sanitize-function
*/
logWarning( 'css-full-page-provide-sanitize-function' );

return {
css: rawCss,
hasChanged: false
};
}
} );

this.htmlDataProcessor = new HtmlPageDataProcessor( editor.data.viewDocument );
editor.data.processor = this.htmlDataProcessor;
}

/**
Expand All @@ -62,6 +91,10 @@ export default class FullPage extends Plugin {
}
}
} );

if ( isAllowedRenderStylesFromHead( editor ) ) {
this._renderStylesFromHead();
}
}, { priority: 'low' } );

// Apply root attributes to the view document fragment.
Expand Down Expand Up @@ -110,4 +143,77 @@ export default class FullPage extends Plugin {
args[ 0 ].trim = false;
}, { priority: 'high' } );
}

/**
* @inheritDoc
*/
public override destroy(): void {
super.destroy();

if ( isAllowedRenderStylesFromHead( this.editor ) ) {
this._removeStyleElementsFromDom();
}
}

/**
* Checks if in the document exists any `<style>` elements injected by the plugin and removes them,
* so these could be re-rendered later.
* There is used `data-full-page-style-id` attribute to recognize styles injected by the feature.
*/
private _removeStyleElementsFromDom(): void {
const existingStyleElements = Array.from(
global.document.querySelectorAll( `[data-full-page-style-id="${ this.editor.id }"]` )
);

for ( const style of existingStyleElements ) {
style.remove();
}
}

/**
* Extracts `<style>` elements from the full page data and renders them in the main document `<head>`.
* CSS content is sanitized before rendering.
*/
private _renderStyleElementsInDom(): void {
const editor = this.editor;
const parsedDocument = this.htmlDataProcessor.parsedDocument;

if ( !parsedDocument ) {
return;
}

const sanitizeCss = editor.config.get( 'htmlSupport.fullPage.sanitizeCss' )!;

// Extract `<style>` elements from the `<head>` from the full page data.
const styleElements: Array<HTMLStyleElement> = Array.from( parsedDocument.querySelectorAll( 'head style' ) );

// Add `data-full-page-style-id` attribute to the `<style>` element and render it in `<head>` in the main document.
for ( const style of styleElements ) {
style.setAttribute( 'data-full-page-style-id', editor.id );

// Sanitize the CSS content before rendering it in the editor.
const sanitizedCss = sanitizeCss( style.innerText );

if ( sanitizedCss.hasChanged ) {
style.innerText = sanitizedCss.css;
}

global.document.head.append( style );
}
}

/**
* Removes existing `<style>` elements injected by the plugin and renders new ones from the full page data.
*/
private _renderStylesFromHead() {
this._removeStyleElementsFromDom();
this._renderStyleElementsInDom();
}
}

/**
* Normalize the Full page configuration option `allowRenderStylesFromHead`.
*/
function isAllowedRenderStylesFromHead( editor: Editor ): boolean {
return editor.config.get( 'htmlSupport.fullPage.allowRenderStylesFromHead' )!;
}
93 changes: 93 additions & 0 deletions packages/ckeditor5-html-support/src/generalhtmlsupportconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,97 @@ export interface GeneralHtmlSupportConfig {
* @default false
*/
preserveEmptyBlocksInEditingView?: boolean;

/**
* The configuration of the Full page editing feature.
* The option is used by the {@link module:html-support/fullpage~FullPage} feature.
*
* ```ts
* ClassicEditor
* .create( {
* htmlSupport: {
* fullPage: ... // Full page feature config.
* }
* } )
* .then( ... )
* .catch( ... );
* ```
*/
fullPage?: FullPageConfig;
}

/**
* The configuration of the Full page editing feature.
*/
export interface FullPageConfig {

/**
* Whether the feature should allow the editor to render styles from the `<head>` section of editor data content.
*
* When set to `true`, the editor will render styles from the `<head>` section of editor data content.
*
* ```ts
* ClassicEditor
* .create( {
* htmlSupport: {
* fullPage: {
* allowRenderStylesFromHead: true
* }
* }
* } )
* .then( ... )
* .catch( ... );
* ```
*
* @default false
*/
allowRenderStylesFromHead?: boolean;

/**
* Callback used to sanitize the CSS provided by the user in editor content
* when option `htmlSupport.fullPage.allowRenderStylesFromHead` is set to `true`.
*
* We strongly recommend overwriting the default function to avoid XSS vulnerabilities.
*
* The function receives the CSS (as a string), and should return an object
* that matches the {@link module:html-support/generalhtmlsupportconfig~CssSanitizeOutput} interface.
*
* ```ts
* ClassicEditor
* .create( editorElement, {
* htmlSupport: {
* fullPage: {
* allowRenderStylesFromHead: true,
*
* sanitizeCss( CssString ) {
* const sanitizedCss = sanitize( CssString );
*
* return {
* css: sanitizedCss,
* // true or false depending on whether the sanitizer stripped anything.
* hasChanged: ...
* };
* }
* }
* }
* } )
* .then( ... )
* .catch( ... );
* ```
*
*/
sanitizeCss?: ( css: string ) => CssSanitizeOutput;
}

export interface CssSanitizeOutput {

/**
* An output (safe) CSS that will be inserted into the document.
*/
css: string;

/**
* A flag that indicates whether the output CSS is different than the input value.
*/
hasChanged: boolean;
}
7 changes: 7 additions & 0 deletions packages/ckeditor5-html-support/src/htmlpagedataprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { HtmlDataProcessor, UpcastWriter, type ViewDocumentFragment } from 'cked
* This data processor implementation uses HTML as input and output data.
*/
export default class HtmlPageDataProcessor extends HtmlDataProcessor {
/**
* Contains the `documentElement` property of the `Document` interface.
*/
public parsedDocument: HTMLElement | null = null;

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -53,6 +58,8 @@ export default class HtmlPageDataProcessor extends HtmlDataProcessor {
// Using the DOM document with body content extracted as a skeleton of the page.
writer.setCustomProperty( '$fullPageDocument', domFragment.ownerDocument.documentElement.outerHTML, viewFragment );

this.parsedDocument = domFragment.ownerDocument.documentElement;

if ( docType ) {
writer.setCustomProperty( '$fullPageDocType', docType, viewFragment );
}
Expand Down
Loading