Verified Commit ad38efa7 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3364884 by codebymikey, Dylan Donkersgoed, sivakarthik229,...

Issue #3364884 by codebymikey, Dylan Donkersgoed, sivakarthik229, DamienMcKenna, _utsavsharma, ramprassad, Znak, nod_, pradhumanjain2311, Wim Leers, smustgrave, kevinvanhove: [DrupalHtmlEngine] HTML-reserved characters (>, <, &) in <script> and <style> tag are converted to HTML entities

(cherry picked from commit 762d5b9a)
parent a15b0121
Loading
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.drupalHtmlEngine=t())}(globalThis,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,n)=>{e.exports=n("dll-reference CKEditor5.dll")("./src/core.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function n(p){var r=t[p];if(void 0!==r)return r.exports;var s=t[p]={exports:{}};return e[p](s,s.exports,n),s.exports}n.d=(e,t)=>{for(var p in t)n.o(t,p)&&!n.o(e,p)&&Object.defineProperty(e,p,{enumerable:!0,get:t[p]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var p={};return(()=>{"use strict";n.d(p,{default:()=>o});var e=n("ckeditor5/src/core.js");class t{constructor(){this.chunks=[],this.selfClosingTags=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]}build(){return this.chunks.join("")}appendNode(e){e.nodeType===Node.TEXT_NODE?this._appendText(e):e.nodeType===Node.ELEMENT_NODE?this._appendElement(e):e.nodeType===Node.DOCUMENT_FRAGMENT_NODE?this._appendChildren(e):e.nodeType===Node.COMMENT_NODE&&this._appendComment(e)}_appendElement(e){const t=e.nodeName.toLowerCase();this._append("<"),this._append(t),this._appendAttributes(e),this._append(">"),this.selfClosingTags.includes(t)||(this._appendChildren(e),this._append("</"),this._append(t),this._append(">"))}_appendChildren(e){Object.keys(e.childNodes).forEach((t=>{this.appendNode(e.childNodes[t])}))}_appendAttributes(e){Object.keys(e.attributes).forEach((t=>{this._append(" "),this._append(e.attributes[t].name),this._append('="'),this._append(this.constructor._escapeAttribute(e.attributes[t].value)),this._append('"')}))}_appendText(e){const t=document.implementation.createHTMLDocument("").createElement("p");t.textContent=e.textContent,this._append(t.innerHTML)}_appendComment(e){this._append("\x3c!--"),this._append(e.textContent),this._append("--\x3e")}_append(e){this.chunks.push(e)}static _escapeAttribute(e){return e.replace(/&/g,"&amp;").replace(/'/g,"&apos;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\r\n/g,"&#13;").replace(/[\r\n]/g,"&#13;")}}class r{getHtml(e){const n=new t;return n.appendNode(e),n.build()}}class s extends e.Plugin{init(){this.editor.data.processor.htmlWriter=new r}static get pluginName(){return"DrupalHtmlEngine"}}const o={DrupalHtmlEngine:s}})(),p=p.default})()));
 No newline at end of file
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.drupalHtmlEngine=t())}(globalThis,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,n)=>{e.exports=n("dll-reference CKEditor5.dll")("./src/core.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function n(p){var r=t[p];if(void 0!==r)return r.exports;var s=t[p]={exports:{}};return e[p](s,s.exports,n),s.exports}n.d=(e,t)=>{for(var p in t)n.o(t,p)&&!n.o(e,p)&&Object.defineProperty(e,p,{enumerable:!0,get:t[p]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var p={};return(()=>{"use strict";n.d(p,{default:()=>a});var e=n("ckeditor5/src/core.js");class t{constructor(){this.chunks=[],this.selfClosingTags=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"],this.rawTags=["script","style"]}build(){return this.chunks.join("")}appendNode(e){e.nodeType===Node.TEXT_NODE?this._appendText(e):e.nodeType===Node.ELEMENT_NODE?this._appendElement(e):e.nodeType===Node.DOCUMENT_FRAGMENT_NODE?this._appendChildren(e):e.nodeType===Node.COMMENT_NODE&&this._appendComment(e)}_appendElement(e){const t=e.nodeName.toLowerCase();this._append("<"),this._append(t),this._appendAttributes(e),this._append(">"),this.selfClosingTags.includes(t)||(this._appendChildren(e),this._append("</"),this._append(t),this._append(">"))}_appendChildren(e){Object.keys(e.childNodes).forEach((t=>{this.appendNode(e.childNodes[t])}))}_appendAttributes(e){Object.keys(e.attributes).forEach((t=>{this._append(" "),this._append(e.attributes[t].name),this._append('="'),this._append(this.constructor._escapeAttribute(e.attributes[t].value)),this._append('"')}))}_appendText(e){const t=document.implementation.createHTMLDocument("").createElement("p");t.textContent=e.textContent,e.parentElement&&this.rawTags.includes(e.parentElement.tagName.toLowerCase())?this._append(t.textContent):this._append(t.innerHTML)}_appendComment(e){this._append("\x3c!--"),this._append(e.textContent),this._append("--\x3e")}_append(e){this.chunks.push(e)}static _escapeAttribute(e){return e.replace(/&/g,"&amp;").replace(/'/g,"&apos;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\r\n/g,"&#13;").replace(/[\r\n]/g,"&#13;")}}class r{getHtml(e){const n=new t;return n.appendNode(e),n.build()}}class s extends e.Plugin{init(){this.editor.data.processor.htmlWriter=new r}static get pluginName(){return"DrupalHtmlEngine"}}const a={DrupalHtmlEngine:s}})(),p=p.default})()));
 No newline at end of file
+11 −1
Original line number Diff line number Diff line
@@ -36,6 +36,9 @@ export default class DrupalHtmlBuilder {
      'track',
      'wbr',
    ];

    // @see https://html.spec.whatwg.org/multipage/syntax.html#raw-text-elements
    this.rawTags = ['script', 'style'];
  }

  /**
@@ -141,8 +144,15 @@ export default class DrupalHtmlBuilder {
    const container = doc.createElement('p');
    container.textContent = node.textContent;

    if (
      node.parentElement &&
      this.rawTags.includes(node.parentElement.tagName.toLowerCase())
    ) {
      this._append(container.textContent);
    } else {
      this._append(container.innerHTML);
    }
  }

  /**
   * Appends a comment to the value.
+146 −0
Original line number Diff line number Diff line
@@ -797,6 +797,152 @@ function (ConstraintViolation $v) {
    $assert_session->responseContains('<!-- Hamsters, alpacas, llamas, and kittens are cute! --><p>This is a <em>test!</em></p>');
  }

  /**
   * Ensures that HTML scripts and styles are properly preserved in CKEditor 5.
   */
  public function testStylesAndScripts(): void {
    $test_cases = [
      // Test cases taken from the HTML documentation.
      // @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
      'script' => [
        '<script>(function() { let x = 10, y = 5; if( y <--x ) { console.log("run me!"); }})()</script>',
        '<script>(function() { let x = 10, y = 5; if( y <--x ) { console.log("run me!"); }})()</script>',
      ],
      'script like tag' => [
        '<script>(function() { let player = 5, script = 10; if (player<script) { console.log("run me!"); }})()</script>',
        '<script>(function() { let player = 5, script = 10; if (player<script) { console.log("run me!"); }})()</script>',
      ],
      'script to escape' => [
        "<script>const example = 'Consider this string: <!-- <script>';</script>",
        "<script>const example = 'Consider this string: <!-- <script>';</script>",
      ],
      'unescaped script tag' => [
        <<<HTML
        <script>
          const example = 'Consider this string: <!-- <script>';
          console.log(example);
        </script>
        <!-- despite appearances, this is actually part of the script still! -->
        <script>
          let a = 1 + 2; // this is the same script block still...
        </script>
        HTML,
        <<<HTML
        <script>
          const example = 'Consider this string: <!-- <script>';
          console.log(example);
        </script>
        <!-- despite appearances, this is actually part of the script still! -->
        <script>
          let a = 1 + 2; // this is the same script block still...
        </script>
        HTML,
      ],
      'style' => [
        <<<HTML
        <style>
        a > span {
          /* Important comment. */
          color: red !important;
        }
        </style>
        HTML,
        <<<HTML
        <style>
        a > span {
          /* Important comment. */
          color: red !important;
        }
        </style>
        HTML,
      ],
      'script and style' => [
        <<<HTML
        <script type="text/javascript">
        let x = 10;
        let y = 5;
        if(y < x){
        console.log('is smaller')
        }
        </script>
        <style type="text/css">
        :root {
          --main-bg-color: brown;
        }
        .sections > .section {
          background: var(--main-bg-color);
        }
        </style>
        HTML,
        <<<HTML
        <script type="text/javascript">
        let x = 10;
        let y = 5;
        if(y < x){
        console.log('is smaller')
        }
        </script><style type="text/css">
        :root {
          --main-bg-color: brown;
        }
        .sections > .section {
          background: var(--main-bg-color);
        }
        </style>
        HTML,
      ],
    ];

    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();

    // Create filter.
    FilterFormat::create([
      'format' => 'ckeditor5',
      'name' => 'CKEditor 5 HTML',
      'roles' => [RoleInterface::AUTHENTICATED_ID],
    ])->save();
    Editor::create([
      'format' => 'ckeditor5',
      'editor' => 'ckeditor5',
      'settings' => [
        'toolbar' => [
          'items' => [
            'sourceEditing',
          ],
        ],
        'plugins' => [
          'ckeditor5_sourceEditing' => [
            'allowed_tags' => [],
          ],
        ],
      ],
    ])->save();
    $this->assertSame([], array_map(
      function (ConstraintViolation $v) {
        return (string) $v->getMessage();
      },
      iterator_to_array(CKEditor5::validatePair(
        Editor::load('ckeditor5'),
        FilterFormat::load('ckeditor5')
      ))
    ));

    // Add a node with text rendered via the CKEditor 5 HTML format.
    foreach ($test_cases as $test_case_name => $test_case) {
      [$markup, $expected_content] = $test_case;
      $this->drupalGet('node/add');
      $page->fillField('title[0][value]', "Style and script test - $test_case_name");
      $this->waitForEditor();
      $this->pressEditorButton('Source');
      $editor = $page->find('css', '.ck-source-editing-area textarea');
      $editor->setValue($markup);
      $page->pressButton('Save');

      $assert_session->responseContains($expected_content);
    }
  }

  /**
   * Ensures that changes are saved in CKEditor 5.
   */
+31 −0
Original line number Diff line number Diff line
@@ -72,6 +72,37 @@ module.exports = {
        'foo bar<p>foo</p><div>bar</div>',
      );
    },
  'should return correct HTML scripts and styles': function () {
    const drupalHtmlBuilder = new DrupalHtmlBuilder();
    const fragment = document.createDocumentFragment();
    const script = document.createElement('script');
    script.textContent = `let x = 10;
let y = 5;
if (y < x) {
console.log('is smaller')
}`;
    const style = document.createElement('style');
    style.setAttribute('type', 'text/css');
    style.appendChild(
      document.createTextNode(':root .sections > h2 { background: red}'),
    );

    fragment.appendChild(style);
    fragment.appendChild(document.createTextNode('\n'));
    fragment.appendChild(script);

    drupalHtmlBuilder.appendNode(fragment);

    assert.equal(
      drupalHtmlBuilder.build(),
      `<style type="text/css">:root .sections > h2 { background: red}</style>
<script>let x = 10;
let y = 5;
if (y < x) {
console.log('is smaller')
}</script>`,
    );
  },
  'should return correct HTML from fragment with comment': function () {
    const drupalHtmlBuilder = new DrupalHtmlBuilder();
    const fragment = document.createDocumentFragment();