Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / filter / filter.filter_html.admin.es6.js
diff --git a/web/core/modules/filter/filter.filter_html.admin.es6.js b/web/core/modules/filter/filter.filter_html.admin.es6.js
new file mode 100644 (file)
index 0000000..c5809c0
--- /dev/null
@@ -0,0 +1,323 @@
+/**
+ * @file
+ * Attaches behavior for updating filter_html's settings automatically.
+ */
+
+(function ($, Drupal, _, document) {
+  if (Drupal.filterConfiguration) {
+    /**
+     * Implement a live setting parser to prevent text editors from automatically
+     * enabling buttons that are not allowed by this filter's configuration.
+     *
+     * @namespace
+     */
+    Drupal.filterConfiguration.liveSettingParsers.filter_html = {
+
+      /**
+       * @return {Array}
+       *   An array of filter rules.
+       */
+      getRules() {
+        const currentValue = $('#edit-filters-filter-html-settings-allowed-html').val();
+        const rules = Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(currentValue);
+
+        // Build a FilterHTMLRule that reflects the hard-coded behavior that
+        // strips all "style" attribute and all "on*" attributes.
+        const rule = new Drupal.FilterHTMLRule();
+        rule.restrictedTags.tags = ['*'];
+        rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
+        rules.push(rule);
+
+        return rules;
+      },
+    };
+  }
+
+  /**
+   * Displays and updates what HTML tags are allowed to use in a filter.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @todo Remove everything but 'attach' and 'detach' and make a proper object.
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior for updating allowed HTML tags.
+   */
+  Drupal.behaviors.filterFilterHtmlUpdating = {
+
+    // The form item contains the "Allowed HTML tags" setting.
+    $allowedHTMLFormItem: null,
+
+    // The description for the "Allowed HTML tags" field.
+    $allowedHTMLDescription: null,
+
+    /**
+     * The parsed, user-entered tag list of $allowedHTMLFormItem
+     *
+     * @var {Object.<string, Drupal.FilterHTMLRule>}
+     */
+    userTags: {},
+
+    // The auto-created tag list thus far added.
+    autoTags: null,
+
+    // Track which new features have been added to the text editor.
+    newFeatures: {},
+
+    attach(context, settings) {
+      const that = this;
+      $(context).find('[name="filters[filter_html][settings][allowed_html]"]').once('filter-filter_html-updating').each(function () {
+        that.$allowedHTMLFormItem = $(this);
+        that.$allowedHTMLDescription = that.$allowedHTMLFormItem.closest('.js-form-item').find('.description');
+        that.userTags = that._parseSetting(this.value);
+
+        // Update the new allowed tags based on added text editor features.
+        $(document)
+          .on('drupalEditorFeatureAdded', (e, feature) => {
+            that.newFeatures[feature.name] = feature.rules;
+            that._updateAllowedTags();
+          })
+          .on('drupalEditorFeatureModified', (e, feature) => {
+            if (that.newFeatures.hasOwnProperty(feature.name)) {
+              that.newFeatures[feature.name] = feature.rules;
+              that._updateAllowedTags();
+            }
+          })
+          .on('drupalEditorFeatureRemoved', (e, feature) => {
+            if (that.newFeatures.hasOwnProperty(feature.name)) {
+              delete that.newFeatures[feature.name];
+              that._updateAllowedTags();
+            }
+          });
+
+        // When the allowed tags list is manually changed, update userTags.
+        that.$allowedHTMLFormItem.on('change.updateUserTags', function () {
+          that.userTags = _.difference(that._parseSetting(this.value), that.autoTags);
+        });
+      });
+    },
+
+    /**
+     * Updates the "Allowed HTML tags" setting and shows an informative message.
+     */
+    _updateAllowedTags() {
+      // Update the list of auto-created tags.
+      this.autoTags = this._calculateAutoAllowedTags(this.userTags, this.newFeatures);
+
+      // Remove any previous auto-created tag message.
+      this.$allowedHTMLDescription.find('.editor-update-message').remove();
+
+      // If any auto-created tags: insert message and update form item.
+      if (!_.isEmpty(this.autoTags)) {
+        this.$allowedHTMLDescription.append(Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags));
+        const userTagsWithoutOverrides = _.omit(this.userTags, _.keys(this.autoTags));
+        this.$allowedHTMLFormItem.val(`${this._generateSetting(userTagsWithoutOverrides)} ${this._generateSetting(this.autoTags)}`);
+      }
+      // Restore to original state.
+      else {
+        this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
+      }
+    },
+
+    /**
+     * Calculates which HTML tags the added text editor buttons need to work.
+     *
+     * The filter_html filter is only concerned with the required tags, not with
+     * any properties, nor with each feature's "allowed" tags.
+     *
+     * @param {Array} userAllowedTags
+     *   The list of user-defined allowed tags.
+     * @param {object} newFeatures
+     *   A list of {@link Drupal.EditorFeature} objects' rules, keyed by
+     *   their name.
+     *
+     * @return {Array}
+     *   A list of new allowed tags.
+     */
+    _calculateAutoAllowedTags(userAllowedTags, newFeatures) {
+      let featureName;
+      let feature;
+      let featureRule;
+      let filterRule;
+      let tag;
+      const editorRequiredTags = {};
+      // Map the newly added Text Editor features to Drupal.FilterHtmlRule
+      // objects (to allow comparing userTags with autoTags).
+      for (featureName in newFeatures) {
+        if (newFeatures.hasOwnProperty(featureName)) {
+          feature = newFeatures[featureName];
+          for (let f = 0; f < feature.length; f++) {
+            featureRule = feature[f];
+            for (let t = 0; t < featureRule.required.tags.length; t++) {
+              tag = featureRule.required.tags[t];
+              if (!_.has(editorRequiredTags, tag)) {
+                filterRule = new Drupal.FilterHTMLRule();
+                filterRule.restrictedTags.tags = [tag];
+                // @todo Neither Drupal.FilterHtmlRule nor
+                //   Drupal.EditorFeatureHTMLRule allow for generic attribute
+                //   value restrictions, only for the "class" and "style"
+                //   attribute's values to be restricted. The filter_html filter
+                //   always disallows the "style" attribute, so we only need to
+                //   support "class" attribute value restrictions. Fix once
+                //   https://www.drupal.org/node/2567801 lands.
+                filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0);
+                filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0);
+                editorRequiredTags[tag] = filterRule;
+              }
+              // The tag is already allowed, add any additionally allowed
+              // attributes.
+              else {
+                filterRule = editorRequiredTags[tag];
+                filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes);
+                filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes);
+              }
+            }
+          }
+        }
+      }
+
+      // Now compare userAllowedTags with editorRequiredTags, and build
+      // autoAllowedTags, which contains:
+      // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
+      //   that are additionally going to be allowed)
+      // - any tags in editorRequiredTags that already exists in userAllowedTags
+      //   but does not allow all attributes or attribute values
+      const autoAllowedTags = {};
+      for (tag in editorRequiredTags) {
+        // If userAllowedTags does not contain a rule for this editor-required
+        // tag, then add it to the list of automatically allowed tags.
+        if (!_.has(userAllowedTags, tag)) {
+          autoAllowedTags[tag] = editorRequiredTags[tag];
+        }
+        // Otherwise, if userAllowedTags already allows this tag, then check if
+        // additional attributes and classes on this tag are required by the
+        // editor.
+        else {
+          const requiredAttributes = editorRequiredTags[tag].restrictedTags.allowed.attributes;
+          const allowedAttributes = userAllowedTags[tag].restrictedTags.allowed.attributes;
+          const needsAdditionalAttributes = requiredAttributes.length && _.difference(requiredAttributes, allowedAttributes).length;
+          const requiredClasses = editorRequiredTags[tag].restrictedTags.allowed.classes;
+          const allowedClasses = userAllowedTags[tag].restrictedTags.allowed.classes;
+          const needsAdditionalClasses = requiredClasses.length && _.difference(requiredClasses, allowedClasses).length;
+          if (needsAdditionalAttributes || needsAdditionalClasses) {
+            autoAllowedTags[tag] = userAllowedTags[tag].clone();
+          }
+          if (needsAdditionalAttributes) {
+            autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(allowedAttributes, requiredAttributes);
+          }
+          if (needsAdditionalClasses) {
+            autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(allowedClasses, requiredClasses);
+          }
+        }
+      }
+
+      return autoAllowedTags;
+    },
+
+    /**
+     * Parses the value of this.$allowedHTMLFormItem.
+     *
+     * @param {string} setting
+     *   The string representation of the setting. For example:
+     *     <p class="callout"> <br> <a href hreflang>
+     *
+     * @return {Object.<string, Drupal.FilterHTMLRule>}
+     *   The corresponding text filter HTML rule objects, one per tag, keyed by
+     *   tag name.
+     */
+    _parseSetting(setting) {
+      let node;
+      let tag;
+      let rule;
+      let attributes;
+      let attribute;
+      const allowedTags = setting.match(/(<[^>]+>)/g);
+      const sandbox = document.createElement('div');
+      const rules = {};
+      for (let t = 0; t < allowedTags.length; t++) {
+        // Let the browser do the parsing work for us.
+        sandbox.innerHTML = allowedTags[t];
+        node = sandbox.firstChild;
+        tag = node.tagName.toLowerCase();
+
+        // Build the Drupal.FilterHtmlRule object.
+        rule = new Drupal.FilterHTMLRule();
+        // We create one rule per allowed tag, so always one tag.
+        rule.restrictedTags.tags = [tag];
+        // Add the attribute restrictions.
+        attributes = node.attributes;
+        for (let i = 0; i < attributes.length; i++) {
+          attribute = attributes.item(i);
+          const attributeName = attribute.nodeName;
+          // @todo Drupal.FilterHtmlRule does not allow for generic attribute
+          //   value restrictions, only for the "class" and "style" attribute's
+          //   values. The filter_html filter always disallows the "style"
+          //   attribute, so we only need to support "class" attribute value
+          //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
+          if (attributeName === 'class') {
+            const attributeValue = attribute.textContent;
+            rule.restrictedTags.allowed.classes = attributeValue.split(' ');
+          }
+          else {
+            rule.restrictedTags.allowed.attributes.push(attributeName);
+          }
+        }
+
+        rules[tag] = rule;
+      }
+      return rules;
+    },
+
+    /**
+     * Generates the value of this.$allowedHTMLFormItem.
+     *
+     * @param {Object.<string, Drupal.FilterHTMLRule>} tags
+     *   The parsed representation of the setting.
+     *
+     * @return {Array}
+     *   The string representation of the setting. e.g. "<p> <br> <a>"
+     */
+    _generateSetting(tags) {
+      return _.reduce(tags, (setting, rule, tag) => {
+        if (setting.length) {
+          setting += ' ';
+        }
+
+        setting += `<${tag}`;
+        if (rule.restrictedTags.allowed.attributes.length) {
+          setting += ` ${rule.restrictedTags.allowed.attributes.join(' ')}`;
+        }
+        // @todo Drupal.FilterHtmlRule does not allow for generic attribute
+        //   value restrictions, only for the "class" and "style" attribute's
+        //   values. The filter_html filter always disallows the "style"
+        //   attribute, so we only need to support "class" attribute value
+        //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
+        if (rule.restrictedTags.allowed.classes.length) {
+          setting += ` class="${rule.restrictedTags.allowed.classes.join(' ')}"`;
+        }
+
+        setting += '>';
+        return setting;
+      }, '');
+    },
+
+  };
+
+  /**
+   * Theme function for the filter_html update message.
+   *
+   * @param {Array} tags
+   *   An array of the new tags that are to be allowed.
+   *
+   * @return {string}
+   *   The corresponding HTML.
+   */
+  Drupal.theme.filterFilterHTMLUpdateMessage = function (tags) {
+    let html = '';
+    const tagList = Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(tags);
+    html += '<p class="editor-update-message">';
+    html += Drupal.t('Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.', { '@tag-list': tagList });
+    html += '</p>';
+    return html;
+  };
+}(jQuery, Drupal, _, document));