Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / editor / js / editor.es6.js
diff --git a/web/core/modules/editor/js/editor.es6.js b/web/core/modules/editor/js/editor.es6.js
new file mode 100644 (file)
index 0000000..03c50d9
--- /dev/null
@@ -0,0 +1,314 @@
+/**
+ * @file
+ * Attaches behavior for the Editor module.
+ */
+
+(function ($, Drupal, drupalSettings) {
+  /**
+   * Finds the text area field associated with the given text format selector.
+   *
+   * @param {jQuery} $formatSelector
+   *   A text format selector DOM element.
+   *
+   * @return {HTMLElement}
+   *   The text area DOM element, if it was found.
+   */
+  function findFieldForFormatSelector($formatSelector) {
+    const field_id = $formatSelector.attr('data-editor-for');
+    // This selector will only find text areas in the top-level document. We do
+    // not support attaching editors on text areas within iframes.
+    return $(`#${field_id}`).get(0);
+  }
+
+  /**
+   * Changes the text editor on a text area.
+   *
+   * @param {HTMLElement} field
+   *   The text area DOM element.
+   * @param {string} newFormatID
+   *   The text format we're changing to; the text editor for the currently
+   *   active text format will be detached, and the text editor for the new text
+   *   format will be attached.
+   */
+  function changeTextEditor(field, newFormatID) {
+    const previousFormatID = field.getAttribute('data-editor-active-text-format');
+
+    // Detach the current editor (if any) and attach a new editor.
+    if (drupalSettings.editor.formats[previousFormatID]) {
+      Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]);
+    }
+    // When no text editor is currently active, stop tracking changes.
+    else {
+      $(field).off('.editor');
+    }
+
+    // Attach the new text editor (if any).
+    if (drupalSettings.editor.formats[newFormatID]) {
+      const format = drupalSettings.editor.formats[newFormatID];
+      filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach);
+    }
+
+    // Store the new active format.
+    field.setAttribute('data-editor-active-text-format', newFormatID);
+  }
+
+  /**
+   * Handles changes in text format.
+   *
+   * @param {jQuery.Event} event
+   *   The text format change event.
+   */
+  function onTextFormatChange(event) {
+    const $select = $(event.target);
+    const field = event.data.field;
+    const activeFormatID = field.getAttribute('data-editor-active-text-format');
+    const newFormatID = $select.val();
+
+    // Prevent double-attaching if the change event is triggered manually.
+    if (newFormatID === activeFormatID) {
+      return;
+    }
+
+    // When changing to a text format that has a text editor associated
+    // with it that supports content filtering, then first ask for
+    // confirmation, because switching text formats might cause certain
+    // markup to be stripped away.
+    const supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
+    // If there is no content yet, it's always safe to change the text format.
+    const hasContent = field.value !== '';
+    if (hasContent && supportContentFiltering) {
+      const message = Drupal.t('Changing the text format to %text_format will permanently remove content that is not allowed in that text format.<br><br>Save your changes before switching the text format to avoid losing data.', {
+        '%text_format': $select.find('option:selected').text(),
+      });
+      var confirmationDialog = Drupal.dialog(`<div>${message}</div>`, {
+        title: Drupal.t('Change text format?'),
+        dialogClass: 'editor-change-text-format-modal',
+        resizable: false,
+        buttons: [
+          {
+            text: Drupal.t('Continue'),
+            class: 'button button--primary',
+            click() {
+              changeTextEditor(field, newFormatID);
+              confirmationDialog.close();
+            },
+          },
+          {
+            text: Drupal.t('Cancel'),
+            class: 'button',
+            click() {
+              // Restore the active format ID: cancel changing text format. We
+              // cannot simply call event.preventDefault() because jQuery's
+              // change event is only triggered after the change has already
+              // been accepted.
+              $select.val(activeFormatID);
+              confirmationDialog.close();
+            },
+          },
+        ],
+        // Prevent this modal from being closed without the user making a choice
+        // as per http://stackoverflow.com/a/5438771.
+        closeOnEscape: false,
+        create() {
+          $(this).parent().find('.ui-dialog-titlebar-close').remove();
+        },
+        beforeClose: false,
+        close(event) {
+          // Automatically destroy the DOM element that was used for the dialog.
+          $(event.target).remove();
+        },
+      });
+
+      confirmationDialog.showModal();
+    }
+    else {
+      changeTextEditor(field, newFormatID);
+    }
+  }
+
+  /**
+   * Initialize an empty object for editors to place their attachment code.
+   *
+   * @namespace
+   */
+  Drupal.editors = {};
+
+  /**
+   * Enables editors on text_format elements.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches an editor to an input element.
+   * @prop {Drupal~behaviorDetach} detach
+   *   Detaches an editor from an input element.
+   */
+  Drupal.behaviors.editor = {
+    attach(context, settings) {
+      // If there are no editor settings, there are no editors to enable.
+      if (!settings.editor) {
+        return;
+      }
+
+      $(context).find('[data-editor-for]').once('editor').each(function () {
+        const $this = $(this);
+        const field = findFieldForFormatSelector($this);
+
+        // Opt-out if no supported text area was found.
+        if (!field) {
+          return;
+        }
+
+        // Store the current active format.
+        const activeFormatID = $this.val();
+        field.setAttribute('data-editor-active-text-format', activeFormatID);
+
+        // Directly attach this text editor, if the text format is enabled.
+        if (settings.editor.formats[activeFormatID]) {
+          // XSS protection for the current text format/editor is performed on
+          // the server side, so we don't need to do anything special here.
+          Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
+        }
+        // When there is no text editor for this text format, still track
+        // changes, because the user has the ability to switch to some text
+        // editor, otherwise this code would not be executed.
+        $(field).on('change.editor keypress.editor', () => {
+          field.setAttribute('data-editor-value-is-changed', 'true');
+          // Just knowing that the value was changed is enough, stop tracking.
+          $(field).off('.editor');
+        });
+
+        // Attach onChange handler to text format selector element.
+        if ($this.is('select')) {
+          $this.on('change.editorAttach', { field }, onTextFormatChange);
+        }
+        // Detach any editor when the containing form is submitted.
+        $this.parents('form').on('submit', (event) => {
+          // Do not detach if the event was canceled.
+          if (event.isDefaultPrevented()) {
+            return;
+          }
+          // Detach the current editor (if any).
+          if (settings.editor.formats[activeFormatID]) {
+            Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize');
+          }
+        });
+      });
+    },
+
+    detach(context, settings, trigger) {
+      let editors;
+      // The 'serialize' trigger indicates that we should simply update the
+      // underlying element with the new text, without destroying the editor.
+      if (trigger === 'serialize') {
+        // Removing the editor-processed class guarantees that the editor will
+        // be reattached. Only do this if we're planning to destroy the editor.
+        editors = $(context).find('[data-editor-for]').findOnce('editor');
+      }
+      else {
+        editors = $(context).find('[data-editor-for]').removeOnce('editor');
+      }
+
+      editors.each(function () {
+        const $this = $(this);
+        const activeFormatID = $this.val();
+        const field = findFieldForFormatSelector($this);
+        if (field && activeFormatID in settings.editor.formats) {
+          Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger);
+        }
+      });
+    },
+  };
+
+  /**
+   * Attaches editor behaviors to the field.
+   *
+   * @param {HTMLElement} field
+   *   The textarea DOM element.
+   * @param {object} format
+   *   The text format that's being activated, from
+   *   drupalSettings.editor.formats.
+   *
+   * @listens event:change
+   *
+   * @fires event:formUpdated
+   */
+  Drupal.editorAttach = function (field, format) {
+    if (format.editor) {
+      // Attach the text editor.
+      Drupal.editors[format.editor].attach(field, format);
+
+      // Ensures form.js' 'formUpdated' event is triggered even for changes that
+      // happen within the text editor.
+      Drupal.editors[format.editor].onChange(field, () => {
+        $(field).trigger('formUpdated');
+
+        // Keep track of changes, so we know what to do when switching text
+        // formats and guaranteeing XSS protection.
+        field.setAttribute('data-editor-value-is-changed', 'true');
+      });
+    }
+  };
+
+  /**
+   * Detaches editor behaviors from the field.
+   *
+   * @param {HTMLElement} field
+   *   The textarea DOM element.
+   * @param {object} format
+   *   The text format that's being activated, from
+   *   drupalSettings.editor.formats.
+   * @param {string} trigger
+   *   Trigger value from the detach behavior.
+   */
+  Drupal.editorDetach = function (field, format, trigger) {
+    if (format.editor) {
+      Drupal.editors[format.editor].detach(field, format, trigger);
+
+      // Restore the original value if the user didn't make any changes yet.
+      if (field.getAttribute('data-editor-value-is-changed') === 'false') {
+        field.value = field.getAttribute('data-editor-value-original');
+      }
+    }
+  };
+
+  /**
+   * Filter away XSS attack vectors when switching text formats.
+   *
+   * @param {HTMLElement} field
+   *   The textarea DOM element.
+   * @param {object} format
+   *   The text format that's being activated, from
+   *   drupalSettings.editor.formats.
+   * @param {string} originalFormatID
+   *   The text format ID of the original text format.
+   * @param {function} callback
+   *   A callback to be called (with no parameters) after the field's value has
+   *   been XSS filtered.
+   */
+  function filterXssWhenSwitching(field, format, originalFormatID, callback) {
+    // A text editor that already is XSS-safe needs no additional measures.
+    if (format.editor.isXssSafe) {
+      callback(field, format);
+    }
+    // Otherwise, ensure XSS safety: let the server XSS filter this value.
+    else {
+      $.ajax({
+        url: Drupal.url(`editor/filter_xss/${format.format}`),
+        type: 'POST',
+        data: {
+          value: field.value,
+          original_format_id: originalFormatID,
+        },
+        dataType: 'json',
+        success(xssFilteredValue) {
+          // If the server returns false, then no XSS filtering is needed.
+          if (xssFilteredValue !== false) {
+            field.value = xssFilteredValue;
+          }
+          callback(field, format);
+        },
+      });
+    }
+  }
+}(jQuery, Drupal, drupalSettings));