Security update to Drupal 8.4.6
[yaffs-website] / web / core / modules / filter / filter.filter_html.admin.es6.js
1 /**
2  * @file
3  * Attaches behavior for updating filter_html's settings automatically.
4  */
5
6 (function ($, Drupal, _, document) {
7   if (Drupal.filterConfiguration) {
8     /**
9      * Implement a live setting parser to prevent text editors from automatically
10      * enabling buttons that are not allowed by this filter's configuration.
11      *
12      * @namespace
13      */
14     Drupal.filterConfiguration.liveSettingParsers.filter_html = {
15
16       /**
17        * @return {Array}
18        *   An array of filter rules.
19        */
20       getRules() {
21         const currentValue = $('#edit-filters-filter-html-settings-allowed-html').val();
22         const rules = Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(currentValue);
23
24         // Build a FilterHTMLRule that reflects the hard-coded behavior that
25         // strips all "style" attribute and all "on*" attributes.
26         const rule = new Drupal.FilterHTMLRule();
27         rule.restrictedTags.tags = ['*'];
28         rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
29         rules.push(rule);
30
31         return rules;
32       },
33     };
34   }
35
36   /**
37    * Displays and updates what HTML tags are allowed to use in a filter.
38    *
39    * @type {Drupal~behavior}
40    *
41    * @todo Remove everything but 'attach' and 'detach' and make a proper object.
42    *
43    * @prop {Drupal~behaviorAttach} attach
44    *   Attaches behavior for updating allowed HTML tags.
45    */
46   Drupal.behaviors.filterFilterHtmlUpdating = {
47
48     // The form item contains the "Allowed HTML tags" setting.
49     $allowedHTMLFormItem: null,
50
51     // The description for the "Allowed HTML tags" field.
52     $allowedHTMLDescription: null,
53
54     /**
55      * The parsed, user-entered tag list of $allowedHTMLFormItem
56      *
57      * @var {Object.<string, Drupal.FilterHTMLRule>}
58      */
59     userTags: {},
60
61     // The auto-created tag list thus far added.
62     autoTags: null,
63
64     // Track which new features have been added to the text editor.
65     newFeatures: {},
66
67     attach(context, settings) {
68       const that = this;
69       $(context).find('[name="filters[filter_html][settings][allowed_html]"]').once('filter-filter_html-updating').each(function () {
70         that.$allowedHTMLFormItem = $(this);
71         that.$allowedHTMLDescription = that.$allowedHTMLFormItem.closest('.js-form-item').find('.description');
72         that.userTags = that._parseSetting(this.value);
73
74         // Update the new allowed tags based on added text editor features.
75         $(document)
76           .on('drupalEditorFeatureAdded', (e, feature) => {
77             that.newFeatures[feature.name] = feature.rules;
78             that._updateAllowedTags();
79           })
80           .on('drupalEditorFeatureModified', (e, feature) => {
81             if (that.newFeatures.hasOwnProperty(feature.name)) {
82               that.newFeatures[feature.name] = feature.rules;
83               that._updateAllowedTags();
84             }
85           })
86           .on('drupalEditorFeatureRemoved', (e, feature) => {
87             if (that.newFeatures.hasOwnProperty(feature.name)) {
88               delete that.newFeatures[feature.name];
89               that._updateAllowedTags();
90             }
91           });
92
93         // When the allowed tags list is manually changed, update userTags.
94         that.$allowedHTMLFormItem.on('change.updateUserTags', function () {
95           that.userTags = _.difference(that._parseSetting(this.value), that.autoTags);
96         });
97       });
98     },
99
100     /**
101      * Updates the "Allowed HTML tags" setting and shows an informative message.
102      */
103     _updateAllowedTags() {
104       // Update the list of auto-created tags.
105       this.autoTags = this._calculateAutoAllowedTags(this.userTags, this.newFeatures);
106
107       // Remove any previous auto-created tag message.
108       this.$allowedHTMLDescription.find('.editor-update-message').remove();
109
110       // If any auto-created tags: insert message and update form item.
111       if (!_.isEmpty(this.autoTags)) {
112         this.$allowedHTMLDescription.append(Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags));
113         const userTagsWithoutOverrides = _.omit(this.userTags, _.keys(this.autoTags));
114         this.$allowedHTMLFormItem.val(`${this._generateSetting(userTagsWithoutOverrides)} ${this._generateSetting(this.autoTags)}`);
115       }
116       // Restore to original state.
117       else {
118         this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
119       }
120     },
121
122     /**
123      * Calculates which HTML tags the added text editor buttons need to work.
124      *
125      * The filter_html filter is only concerned with the required tags, not with
126      * any properties, nor with each feature's "allowed" tags.
127      *
128      * @param {Array} userAllowedTags
129      *   The list of user-defined allowed tags.
130      * @param {object} newFeatures
131      *   A list of {@link Drupal.EditorFeature} objects' rules, keyed by
132      *   their name.
133      *
134      * @return {Array}
135      *   A list of new allowed tags.
136      */
137     _calculateAutoAllowedTags(userAllowedTags, newFeatures) {
138       let featureName;
139       let feature;
140       let featureRule;
141       let filterRule;
142       let tag;
143       const editorRequiredTags = {};
144       // Map the newly added Text Editor features to Drupal.FilterHtmlRule
145       // objects (to allow comparing userTags with autoTags).
146       for (featureName in newFeatures) {
147         if (newFeatures.hasOwnProperty(featureName)) {
148           feature = newFeatures[featureName];
149           for (let f = 0; f < feature.length; f++) {
150             featureRule = feature[f];
151             for (let t = 0; t < featureRule.required.tags.length; t++) {
152               tag = featureRule.required.tags[t];
153               if (!_.has(editorRequiredTags, tag)) {
154                 filterRule = new Drupal.FilterHTMLRule();
155                 filterRule.restrictedTags.tags = [tag];
156                 // @todo Neither Drupal.FilterHtmlRule nor
157                 //   Drupal.EditorFeatureHTMLRule allow for generic attribute
158                 //   value restrictions, only for the "class" and "style"
159                 //   attribute's values to be restricted. The filter_html filter
160                 //   always disallows the "style" attribute, so we only need to
161                 //   support "class" attribute value restrictions. Fix once
162                 //   https://www.drupal.org/node/2567801 lands.
163                 filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0);
164                 filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0);
165                 editorRequiredTags[tag] = filterRule;
166               }
167               // The tag is already allowed, add any additionally allowed
168               // attributes.
169               else {
170                 filterRule = editorRequiredTags[tag];
171                 filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes);
172                 filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes);
173               }
174             }
175           }
176         }
177       }
178
179       // Now compare userAllowedTags with editorRequiredTags, and build
180       // autoAllowedTags, which contains:
181       // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
182       //   that are additionally going to be allowed)
183       // - any tags in editorRequiredTags that already exists in userAllowedTags
184       //   but does not allow all attributes or attribute values
185       const autoAllowedTags = {};
186       for (tag in editorRequiredTags) {
187         // If userAllowedTags does not contain a rule for this editor-required
188         // tag, then add it to the list of automatically allowed tags.
189         if (!_.has(userAllowedTags, tag)) {
190           autoAllowedTags[tag] = editorRequiredTags[tag];
191         }
192         // Otherwise, if userAllowedTags already allows this tag, then check if
193         // additional attributes and classes on this tag are required by the
194         // editor.
195         else {
196           const requiredAttributes = editorRequiredTags[tag].restrictedTags.allowed.attributes;
197           const allowedAttributes = userAllowedTags[tag].restrictedTags.allowed.attributes;
198           const needsAdditionalAttributes = requiredAttributes.length && _.difference(requiredAttributes, allowedAttributes).length;
199           const requiredClasses = editorRequiredTags[tag].restrictedTags.allowed.classes;
200           const allowedClasses = userAllowedTags[tag].restrictedTags.allowed.classes;
201           const needsAdditionalClasses = requiredClasses.length && _.difference(requiredClasses, allowedClasses).length;
202           if (needsAdditionalAttributes || needsAdditionalClasses) {
203             autoAllowedTags[tag] = userAllowedTags[tag].clone();
204           }
205           if (needsAdditionalAttributes) {
206             autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(allowedAttributes, requiredAttributes);
207           }
208           if (needsAdditionalClasses) {
209             autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(allowedClasses, requiredClasses);
210           }
211         }
212       }
213
214       return autoAllowedTags;
215     },
216
217     /**
218      * Parses the value of this.$allowedHTMLFormItem.
219      *
220      * @param {string} setting
221      *   The string representation of the setting. For example:
222      *     <p class="callout"> <br> <a href hreflang>
223      *
224      * @return {Object.<string, Drupal.FilterHTMLRule>}
225      *   The corresponding text filter HTML rule objects, one per tag, keyed by
226      *   tag name.
227      */
228     _parseSetting(setting) {
229       let node;
230       let tag;
231       let rule;
232       let attributes;
233       let attribute;
234       const allowedTags = setting.match(/(<[^>]+>)/g);
235       const sandbox = document.createElement('div');
236       const rules = {};
237       for (let t = 0; t < allowedTags.length; t++) {
238         // Let the browser do the parsing work for us.
239         sandbox.innerHTML = allowedTags[t];
240         node = sandbox.firstChild;
241         tag = node.tagName.toLowerCase();
242
243         // Build the Drupal.FilterHtmlRule object.
244         rule = new Drupal.FilterHTMLRule();
245         // We create one rule per allowed tag, so always one tag.
246         rule.restrictedTags.tags = [tag];
247         // Add the attribute restrictions.
248         attributes = node.attributes;
249         for (let i = 0; i < attributes.length; i++) {
250           attribute = attributes.item(i);
251           const attributeName = attribute.nodeName;
252           // @todo Drupal.FilterHtmlRule does not allow for generic attribute
253           //   value restrictions, only for the "class" and "style" attribute's
254           //   values. The filter_html filter always disallows the "style"
255           //   attribute, so we only need to support "class" attribute value
256           //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
257           if (attributeName === 'class') {
258             const attributeValue = attribute.textContent;
259             rule.restrictedTags.allowed.classes = attributeValue.split(' ');
260           }
261           else {
262             rule.restrictedTags.allowed.attributes.push(attributeName);
263           }
264         }
265
266         rules[tag] = rule;
267       }
268       return rules;
269     },
270
271     /**
272      * Generates the value of this.$allowedHTMLFormItem.
273      *
274      * @param {Object.<string, Drupal.FilterHTMLRule>} tags
275      *   The parsed representation of the setting.
276      *
277      * @return {Array}
278      *   The string representation of the setting. e.g. "<p> <br> <a>"
279      */
280     _generateSetting(tags) {
281       return _.reduce(tags, (setting, rule, tag) => {
282         if (setting.length) {
283           setting += ' ';
284         }
285
286         setting += `<${tag}`;
287         if (rule.restrictedTags.allowed.attributes.length) {
288           setting += ` ${rule.restrictedTags.allowed.attributes.join(' ')}`;
289         }
290         // @todo Drupal.FilterHtmlRule does not allow for generic attribute
291         //   value restrictions, only for the "class" and "style" attribute's
292         //   values. The filter_html filter always disallows the "style"
293         //   attribute, so we only need to support "class" attribute value
294         //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
295         if (rule.restrictedTags.allowed.classes.length) {
296           setting += ` class="${rule.restrictedTags.allowed.classes.join(' ')}"`;
297         }
298
299         setting += '>';
300         return setting;
301       }, '');
302     },
303
304   };
305
306   /**
307    * Theme function for the filter_html update message.
308    *
309    * @param {Array} tags
310    *   An array of the new tags that are to be allowed.
311    *
312    * @return {string}
313    *   The corresponding HTML.
314    */
315   Drupal.theme.filterFilterHTMLUpdateMessage = function (tags) {
316     let html = '';
317     const tagList = Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(tags);
318     html += '<p class="editor-update-message">';
319     html += Drupal.t('Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.', { '@tag-list': tagList });
320     html += '</p>';
321     return html;
322   };
323 }(jQuery, Drupal, _, document));