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