filter.filter_html.admin.es6.js 13 KB

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