Chris@0: /** Chris@0: * @file Chris@0: * Provides a JavaScript API to broadcast text editor configuration changes. Chris@0: * Chris@0: * Filter implementations may listen to the drupalEditorFeatureAdded, Chris@0: * drupalEditorFeatureRemoved, and drupalEditorFeatureRemoved events on document Chris@0: * to automatically adjust their settings based on the editor configuration. Chris@0: */ Chris@0: Chris@17: (function($, _, Drupal, document) { Chris@0: /** Chris@0: * Editor configuration namespace. Chris@0: * Chris@0: * @namespace Chris@0: */ Chris@0: Drupal.editorConfiguration = { Chris@0: /** Chris@0: * Must be called by a specific text editor's configuration whenever a Chris@0: * feature is added by the user. Chris@0: * Chris@0: * Triggers the drupalEditorFeatureAdded event on the document, which Chris@0: * receives a {@link Drupal.EditorFeature} object. Chris@0: * Chris@0: * @param {Drupal.EditorFeature} feature Chris@0: * A text editor feature object. Chris@0: * Chris@0: * @fires event:drupalEditorFeatureAdded Chris@0: */ Chris@0: addedFeature(feature) { Chris@0: $(document).trigger('drupalEditorFeatureAdded', feature); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Must be called by a specific text editor's configuration whenever a Chris@0: * feature is removed by the user. Chris@0: * Chris@0: * Triggers the drupalEditorFeatureRemoved event on the document, which Chris@0: * receives a {@link Drupal.EditorFeature} object. Chris@0: * Chris@0: * @param {Drupal.EditorFeature} feature Chris@0: * A text editor feature object. Chris@0: * Chris@0: * @fires event:drupalEditorFeatureRemoved Chris@0: */ Chris@0: removedFeature(feature) { Chris@0: $(document).trigger('drupalEditorFeatureRemoved', feature); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Must be called by a specific text editor's configuration whenever a Chris@0: * feature is modified, i.e. has different rules. Chris@0: * Chris@0: * For example when the "Bold" button is configured to use the `` tag Chris@0: * instead of the `` tag. Chris@0: * Chris@0: * Triggers the drupalEditorFeatureModified event on the document, which Chris@0: * receives a {@link Drupal.EditorFeature} object. Chris@0: * Chris@0: * @param {Drupal.EditorFeature} feature Chris@0: * A text editor feature object. Chris@0: * Chris@0: * @fires event:drupalEditorFeatureModified Chris@0: */ Chris@0: modifiedFeature(feature) { Chris@0: $(document).trigger('drupalEditorFeatureModified', feature); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * May be called by a specific text editor's configuration whenever a Chris@0: * feature is being added, to check whether it would require the filter Chris@0: * settings to be updated. Chris@0: * Chris@0: * The canonical use case is when a text editor is being enabled: Chris@0: * preferably Chris@0: * this would not cause the filter settings to be changed; rather, the Chris@0: * default set of buttons (features) for the text editor should adjust Chris@0: * itself to not cause filter setting changes. Chris@0: * Chris@0: * Note: for filters to integrate with this functionality, it is necessary Chris@0: * that they implement Chris@0: * `Drupal.filterSettingsForEditors[filterID].getRules()`. Chris@0: * Chris@0: * @param {Drupal.EditorFeature} feature Chris@0: * A text editor feature object. Chris@0: * Chris@0: * @return {bool} Chris@0: * Whether the given feature is allowed by the current filters. Chris@0: */ Chris@0: featureIsAllowedByFilters(feature) { Chris@0: /** Chris@17: * Provided a section of a feature or filter rule, checks if no property Chris@17: * values are defined for all properties: attributes, classes and styles. Chris@17: * Chris@17: * @param {object} section Chris@17: * The section to check. Chris@17: * Chris@17: * @return {bool} Chris@17: * Returns true if the section has empty properties, false otherwise. Chris@17: */ Chris@17: function emptyProperties(section) { Chris@17: return ( Chris@17: section.attributes.length === 0 && Chris@17: section.classes.length === 0 && Chris@17: section.styles.length === 0 Chris@17: ); Chris@17: } Chris@17: Chris@17: /** Chris@0: * Generate the universe U of possible values that can result from the Chris@0: * feature's rules' requirements. Chris@0: * Chris@0: * This generates an object of this form: Chris@0: * var universe = { Chris@0: * a: { Chris@0: * 'touchedByAllowedPropertyRule': false, Chris@0: * 'tag': false, Chris@0: * 'attributes:href': false, Chris@0: * 'classes:external': false, Chris@0: * }, Chris@0: * strong: { Chris@0: * 'touchedByAllowedPropertyRule': false, Chris@0: * 'tag': false, Chris@0: * }, Chris@0: * img: { Chris@0: * 'touchedByAllowedPropertyRule': false, Chris@0: * 'tag': false, Chris@0: * 'attributes:src': false Chris@0: * } Chris@0: * }; Chris@0: * Chris@0: * In this example, the given text editor feature resulted in the above Chris@0: * universe, which shows that it must be allowed to generate the a, Chris@0: * strong and img tags. For the a tag, it must be able to set the "href" Chris@0: * attribute and the "external" class. For the strong tag, no further Chris@0: * properties are required. For the img tag, the "src" attribute is Chris@0: * required. The "tag" key is used to track whether that tag was Chris@0: * explicitly allowed by one of the filter's rules. The Chris@0: * "touchedByAllowedPropertyRule" key is used for state tracking that is Chris@0: * essential for filterStatusAllowsFeature() to be able to reason: when Chris@0: * all of a filter's rules have been applied, and none of the forbidden Chris@0: * rules matched (which would have resulted in early termination) yet the Chris@0: * universe has not been made empty (which would be the end result if Chris@0: * everything in the universe were explicitly allowed), then this piece Chris@0: * of state data enables us to determine whether a tag whose properties Chris@0: * were not all explicitly allowed are in fact still allowed, because its Chris@0: * tag was explicitly allowed and there were no filter rules applying Chris@0: * "allowed tag property value" restrictions for this particular tag. Chris@0: * Chris@0: * @param {object} feature Chris@0: * The feature in question. Chris@0: * Chris@0: * @return {object} Chris@0: * The universe generated. Chris@0: * Chris@0: * @see findPropertyValueOnTag() Chris@0: * @see filterStatusAllowsFeature() Chris@0: */ Chris@0: function generateUniverseFromFeatureRequirements(feature) { Chris@0: const properties = ['attributes', 'styles', 'classes']; Chris@0: const universe = {}; Chris@0: Chris@0: for (let r = 0; r < feature.rules.length; r++) { Chris@0: const featureRule = feature.rules[r]; Chris@0: Chris@0: // For each tag required by this feature rule, create a basic entry in Chris@0: // the universe. Chris@0: const requiredTags = featureRule.required.tags; Chris@0: for (let t = 0; t < requiredTags.length; t++) { Chris@0: universe[requiredTags[t]] = { Chris@0: // Whether this tag was allowed or not. Chris@0: tag: false, Chris@0: // Whether any filter rule that applies to this tag had an allowed Chris@0: // property rule. i.e. will become true if >=1 filter rule has >=1 Chris@0: // allowed property rule. Chris@0: touchedByAllowedPropertyRule: false, Chris@0: // Analogous, but for forbidden property rule. Chris@0: touchedBytouchedByForbiddenPropertyRule: false, Chris@0: }; Chris@0: } Chris@0: Chris@0: // If no required properties are defined for this rule, we can move on Chris@0: // to the next feature. Chris@0: if (emptyProperties(featureRule.required)) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: // Expand the existing universe, assume that each tags' property Chris@0: // value is disallowed. If the filter rules allow everything in the Chris@0: // feature's universe, then the feature is allowed. Chris@0: for (let p = 0; p < properties.length; p++) { Chris@0: const property = properties[p]; Chris@0: for (let pv = 0; pv < featureRule.required[property].length; pv++) { Chris@0: const propertyValue = featureRule.required[property]; Chris@0: universe[requiredTags][`${property}:${propertyValue}`] = false; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return universe; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Finds out if a specific property value (potentially containing Chris@0: * wildcards) exists on the given tag. When the "allowing" parameter Chris@0: * equals true, the universe will be updated if that specific property Chris@0: * value exists. Returns true if found, false otherwise. Chris@0: * Chris@0: * @param {object} universe Chris@0: * The universe to check. Chris@0: * @param {string} tag Chris@0: * The tag to look for. Chris@0: * @param {string} property Chris@0: * The property to check. Chris@0: * @param {string} propertyValue Chris@0: * The property value to check. Chris@0: * @param {bool} allowing Chris@0: * Whether to update the universe or not. Chris@0: * Chris@0: * @return {bool} Chris@0: * Returns true if found, false otherwise. Chris@0: */ Chris@17: function findPropertyValueOnTag( Chris@17: universe, Chris@17: tag, Chris@17: property, Chris@17: propertyValue, Chris@17: allowing, Chris@17: ) { Chris@0: // If the tag does not exist in the universe, then it definitely can't Chris@0: // have this specific property value. Chris@0: if (!_.has(universe, tag)) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: const key = `${property}:${propertyValue}`; Chris@0: Chris@0: // Track whether a tag was touched by a filter rule that allows specific Chris@0: // property values on this particular tag. Chris@0: // @see generateUniverseFromFeatureRequirements Chris@0: if (allowing) { Chris@0: universe[tag].touchedByAllowedPropertyRule = true; Chris@0: } Chris@0: Chris@0: // The simple case: no wildcard in property value. Chris@0: if (_.indexOf(propertyValue, '*') === -1) { Chris@0: if (_.has(universe, tag) && _.has(universe[tag], key)) { Chris@0: if (allowing) { Chris@0: universe[tag][key] = true; Chris@0: } Chris@0: return true; Chris@0: } Chris@0: return false; Chris@0: } Chris@0: // The complex case: wildcard in property value. Chris@0: Chris@0: let atLeastOneFound = false; Chris@0: const regex = key.replace(/\*/g, '[^ ]*'); Chris@17: _.each(_.keys(universe[tag]), key => { Chris@0: if (key.match(regex)) { Chris@0: atLeastOneFound = true; Chris@0: if (allowing) { Chris@0: universe[tag][key] = true; Chris@0: } Chris@0: } Chris@0: }); Chris@0: return atLeastOneFound; Chris@0: } Chris@0: Chris@0: /** Chris@17: * Calls findPropertyValuesOnAllTags for all tags in the universe. Chris@17: * Chris@17: * @param {object} universe Chris@17: * The universe to check. Chris@17: * @param {string} property Chris@17: * The property to check. Chris@17: * @param {Array} propertyValues Chris@17: * Values of the property to check. Chris@17: * @param {bool} allowing Chris@17: * Whether to update the universe or not. Chris@17: * Chris@17: * @return {bool} Chris@17: * Returns true if found, false otherwise. Chris@17: */ Chris@17: function findPropertyValuesOnAllTags( Chris@17: universe, Chris@17: property, Chris@17: propertyValues, Chris@17: allowing, Chris@17: ) { Chris@17: let atLeastOneFound = false; Chris@17: _.each(_.keys(universe), tag => { Chris@17: if ( Chris@17: // eslint-disable-next-line no-use-before-define Chris@17: findPropertyValuesOnTag( Chris@17: universe, Chris@17: tag, Chris@17: property, Chris@17: propertyValues, Chris@17: allowing, Chris@17: ) Chris@17: ) { Chris@17: atLeastOneFound = true; Chris@17: } Chris@17: }); Chris@17: return atLeastOneFound; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Calls findPropertyValueOnTag on the given tag for every property value Chris@17: * that is listed in the "propertyValues" parameter. Supports the wildcard Chris@17: * tag. Chris@17: * Chris@17: * @param {object} universe Chris@17: * The universe to check. Chris@17: * @param {string} tag Chris@17: * The tag to look for. Chris@17: * @param {string} property Chris@17: * The property to check. Chris@17: * @param {Array} propertyValues Chris@17: * Values of the property to check. Chris@17: * @param {bool} allowing Chris@17: * Whether to update the universe or not. Chris@17: * Chris@17: * @return {bool} Chris@17: * Returns true if found, false otherwise. Chris@17: */ Chris@17: function findPropertyValuesOnTag( Chris@17: universe, Chris@17: tag, Chris@17: property, Chris@17: propertyValues, Chris@17: allowing, Chris@17: ) { Chris@17: // Detect the wildcard case. Chris@17: if (tag === '*') { Chris@17: return findPropertyValuesOnAllTags( Chris@17: universe, Chris@17: property, Chris@17: propertyValues, Chris@17: allowing, Chris@17: ); Chris@17: } Chris@17: Chris@17: let atLeastOneFound = false; Chris@17: _.each(propertyValues, propertyValue => { Chris@17: if ( Chris@17: findPropertyValueOnTag( Chris@17: universe, Chris@17: tag, Chris@17: property, Chris@17: propertyValue, Chris@17: allowing, Chris@17: ) Chris@17: ) { Chris@17: atLeastOneFound = true; Chris@17: } Chris@17: }); Chris@17: return atLeastOneFound; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Calls deleteFromUniverseIfAllowed for all tags in the universe. Chris@17: * Chris@17: * @param {object} universe Chris@17: * The universe to delete from. Chris@17: * Chris@17: * @return {bool} Chris@17: * Whether something was deleted from the universe. Chris@17: */ Chris@17: function deleteAllTagsFromUniverseIfAllowed(universe) { Chris@17: let atLeastOneDeleted = false; Chris@17: _.each(_.keys(universe), tag => { Chris@17: // eslint-disable-next-line no-use-before-define Chris@17: if (deleteFromUniverseIfAllowed(universe, tag)) { Chris@17: atLeastOneDeleted = true; Chris@17: } Chris@17: }); Chris@17: return atLeastOneDeleted; Chris@17: } Chris@17: Chris@17: /** Chris@0: * Deletes a tag from the universe if the tag itself and each of its Chris@0: * properties are marked as allowed. Chris@0: * Chris@0: * @param {object} universe Chris@0: * The universe to delete from. Chris@0: * @param {string} tag Chris@0: * The tag to check. Chris@0: * Chris@0: * @return {bool} Chris@0: * Whether something was deleted from the universe. Chris@0: */ Chris@0: function deleteFromUniverseIfAllowed(universe, tag) { Chris@0: // Detect the wildcard case. Chris@0: if (tag === '*') { Chris@0: return deleteAllTagsFromUniverseIfAllowed(universe); Chris@0: } Chris@17: if ( Chris@17: _.has(universe, tag) && Chris@17: _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule')) Chris@17: ) { Chris@0: delete universe[tag]; Chris@0: return true; Chris@0: } Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if any filter rule forbids either a tag or a tag property value Chris@0: * that exists in the universe. Chris@0: * Chris@0: * @param {object} universe Chris@0: * Universe to check. Chris@0: * @param {object} filterStatus Chris@0: * Filter status to use for check. Chris@0: * Chris@0: * @return {bool} Chris@0: * Whether any filter rule forbids something in the universe. Chris@0: */ Chris@0: function anyForbiddenFilterRuleMatches(universe, filterStatus) { Chris@0: const properties = ['attributes', 'styles', 'classes']; Chris@0: Chris@0: // Check if a tag in the universe is forbidden. Chris@0: const allRequiredTags = _.keys(universe); Chris@0: let filterRule; Chris@0: for (let i = 0; i < filterStatus.rules.length; i++) { Chris@0: filterRule = filterStatus.rules[i]; Chris@0: if (filterRule.allow === false) { Chris@0: if (_.intersection(allRequiredTags, filterRule.tags).length > 0) { Chris@0: return true; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Check if a property value of a tag in the universe is forbidden. Chris@0: // For all filter rules… Chris@0: for (let n = 0; n < filterStatus.rules.length; n++) { Chris@0: filterRule = filterStatus.rules[n]; Chris@0: // … if there are tags with restricted property values … Chris@17: if ( Chris@17: filterRule.restrictedTags.tags.length && Chris@17: !emptyProperties(filterRule.restrictedTags.forbidden) Chris@17: ) { Chris@0: // … for all those tags … Chris@0: for (let j = 0; j < filterRule.restrictedTags.tags.length; j++) { Chris@0: const tag = filterRule.restrictedTags.tags[j]; Chris@0: // … then iterate over all properties … Chris@0: for (let k = 0; k < properties.length; k++) { Chris@0: const property = properties[k]; Chris@0: // … and return true if just one of the forbidden property Chris@0: // values for this tag and property is listed in the universe. Chris@17: if ( Chris@17: findPropertyValuesOnTag( Chris@17: universe, Chris@17: tag, Chris@17: property, Chris@17: filterRule.restrictedTags.forbidden[property], Chris@17: false, Chris@17: ) Chris@17: ) { Chris@0: return true; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Applies every filter rule's explicit allowing of a tag or a tag Chris@0: * property value to the universe. Whenever both the tag and all of its Chris@0: * required property values are marked as explicitly allowed, they are Chris@0: * deleted from the universe. Chris@0: * Chris@0: * @param {object} universe Chris@0: * Universe to delete from. Chris@0: * @param {object} filterStatus Chris@0: * The filter status in question. Chris@0: */ Chris@0: function markAllowedTagsAndPropertyValues(universe, filterStatus) { Chris@0: const properties = ['attributes', 'styles', 'classes']; Chris@0: Chris@0: // Check if a tag in the universe is allowed. Chris@0: let filterRule; Chris@0: let tag; Chris@17: for ( Chris@17: let l = 0; Chris@17: !_.isEmpty(universe) && l < filterStatus.rules.length; Chris@17: l++ Chris@17: ) { Chris@0: filterRule = filterStatus.rules[l]; Chris@0: if (filterRule.allow === true) { Chris@17: for ( Chris@17: let m = 0; Chris@17: !_.isEmpty(universe) && m < filterRule.tags.length; Chris@17: m++ Chris@17: ) { Chris@0: tag = filterRule.tags[m]; Chris@0: if (_.has(universe, tag)) { Chris@0: universe[tag].tag = true; Chris@0: deleteFromUniverseIfAllowed(universe, tag); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Check if a property value of a tag in the universe is allowed. Chris@0: // For all filter rules… Chris@17: for ( Chris@17: let i = 0; Chris@17: !_.isEmpty(universe) && i < filterStatus.rules.length; Chris@17: i++ Chris@17: ) { Chris@0: filterRule = filterStatus.rules[i]; Chris@0: // … if there are tags with restricted property values … Chris@17: if ( Chris@17: filterRule.restrictedTags.tags.length && Chris@17: !emptyProperties(filterRule.restrictedTags.allowed) Chris@17: ) { Chris@0: // … for all those tags … Chris@17: for ( Chris@17: let j = 0; Chris@17: !_.isEmpty(universe) && j < filterRule.restrictedTags.tags.length; Chris@17: j++ Chris@17: ) { Chris@0: tag = filterRule.restrictedTags.tags[j]; Chris@0: // … then iterate over all properties … Chris@0: for (let k = 0; k < properties.length; k++) { Chris@0: const property = properties[k]; Chris@0: // … and try to delete this tag from the universe if just one Chris@0: // of the allowed property values for this tag and property is Chris@0: // listed in the universe. (Because everything might be allowed Chris@0: // now.) Chris@17: if ( Chris@17: findPropertyValuesOnTag( Chris@17: universe, Chris@17: tag, Chris@17: property, Chris@17: filterRule.restrictedTags.allowed[property], Chris@17: true, Chris@17: ) Chris@17: ) { Chris@0: deleteFromUniverseIfAllowed(universe, tag); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks whether the current status of a filter allows a specific feature Chris@0: * by building the universe of potential values from the feature's Chris@0: * requirements and then checking whether anything in the filter prevents Chris@0: * that. Chris@0: * Chris@0: * @param {object} filterStatus Chris@0: * The filter status in question. Chris@0: * @param {object} feature Chris@0: * The feature requested. Chris@0: * Chris@0: * @return {bool} Chris@0: * Whether the current status of the filter allows specified feature. Chris@0: * Chris@0: * @see generateUniverseFromFeatureRequirements() Chris@0: */ Chris@0: function filterStatusAllowsFeature(filterStatus, feature) { Chris@0: // An inactive filter by definition allows the feature. Chris@0: if (!filterStatus.active) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: // A feature that specifies no rules has no HTML requirements and is Chris@0: // hence allowed by definition. Chris@0: if (feature.rules.length === 0) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: // Analogously for a filter that specifies no rules. Chris@0: if (filterStatus.rules.length === 0) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: // Generate the universe U of possible values that can result from the Chris@0: // feature's rules' requirements. Chris@0: const universe = generateUniverseFromFeatureRequirements(feature); Chris@0: Chris@0: // If anything that is in the universe (and is thus required by the Chris@0: // feature) is forbidden by any of the filter's rules, then this filter Chris@0: // does not allow this feature. Chris@0: if (anyForbiddenFilterRuleMatches(universe, filterStatus)) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Mark anything in the universe that is allowed by any of the filter's Chris@0: // rules as allowed. If everything is explicitly allowed, then the Chris@0: // universe will become empty. Chris@0: markAllowedTagsAndPropertyValues(universe, filterStatus); Chris@0: Chris@0: // If there was at least one filter rule allowing tags, then everything Chris@0: // in the universe must be allowed for this feature to be allowed, and Chris@0: // thus by now it must be empty. However, it is still possible that the Chris@0: // filter allows the feature, due to no rules for allowing tag property Chris@0: // values and/or rules for forbidding tag property values. For details: Chris@0: // see the comments below. Chris@0: // @see generateUniverseFromFeatureRequirements() Chris@0: if (_.some(_.pluck(filterStatus.rules, 'allow'))) { Chris@0: // If the universe is empty, then everything was explicitly allowed Chris@0: // and our job is done: this filter allows this feature! Chris@0: if (_.isEmpty(universe)) { Chris@0: return true; Chris@0: } Chris@0: // Otherwise, it is still possible that this feature is allowed. Chris@0: Chris@17: // Every tag must be explicitly allowed if there are filter rules Chris@17: // doing tag whitelisting. Chris@0: if (!_.every(_.pluck(universe, 'tag'))) { Chris@0: return false; Chris@0: } Chris@17: // Every tag was explicitly allowed, but since the universe is not Chris@17: // empty, one or more tag properties are disallowed. However, if Chris@17: // only blacklisting of tag properties was applied to these tags, Chris@17: // and no whitelisting was ever applied, then it's still fine: Chris@17: // since none of the tag properties were blacklisted, we got to Chris@17: // this point, and since no whitelisting was applied, it doesn't Chris@17: // matter that the properties: this could never have happened Chris@17: // anyway. It's only this late that we can know this for certain. Chris@0: Chris@0: const tags = _.keys(universe); Chris@17: // Figure out if there was any rule applying whitelisting tag Chris@17: // restrictions to each of the remaining tags. Chris@0: for (let i = 0; i < tags.length; i++) { Chris@0: const tag = tags[i]; Chris@0: if (_.has(universe, tag)) { Chris@0: if (universe[tag].touchedByAllowedPropertyRule === false) { Chris@0: delete universe[tag]; Chris@0: } Chris@0: } Chris@0: } Chris@0: return _.isEmpty(universe); Chris@0: } Chris@0: // Otherwise, if all filter rules were doing blacklisting, then the sole Chris@0: // fact that we got to this point indicates that this filter allows for Chris@0: // everything that is required for this feature. Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: // If any filter's current status forbids the editor feature, return Chris@0: // false. Chris@0: Drupal.filterConfiguration.update(); Chris@17: return Object.keys(Drupal.filterConfiguration.statuses).every(filterID => Chris@17: filterStatusAllowsFeature( Chris@17: Drupal.filterConfiguration.statuses[filterID], Chris@17: feature, Chris@17: ), Chris@17: ); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Constructor for an editor feature HTML rule. Chris@0: * Chris@0: * Intended to be used in combination with {@link Drupal.EditorFeature}. Chris@0: * Chris@0: * A text editor feature rule object describes both: Chris@0: * - required HTML tags, attributes, styles and classes: without these, the Chris@0: * text editor feature is unable to function. It's possible that a Chris@0: * - allowed HTML tags, attributes, styles and classes: these are optional Chris@0: * in the strictest sense, but it is possible that the feature generates Chris@0: * them. Chris@0: * Chris@0: * The structure can be very clearly seen below: there's a "required" and an Chris@0: * "allowed" key. For each of those, there are objects with the "tags", Chris@0: * "attributes", "styles" and "classes" keys. For all these keys the values Chris@0: * are initialized to the empty array. List each possible value as an array Chris@0: * value. Besides the "required" and "allowed" keys, there's an optional Chris@0: * "raw" key: it allows text editor implementations to optionally pass in Chris@0: * their raw representation instead of the Drupal-defined representation for Chris@0: * HTML rules. Chris@0: * Chris@0: * @example Chris@0: * tags: [''] Chris@0: * attributes: ['href', 'alt'] Chris@0: * styles: ['color', 'text-decoration'] Chris@0: * classes: ['external', 'internal'] Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @see Drupal.EditorFeature Chris@0: */ Chris@17: Drupal.EditorFeatureHTMLRule = function() { Chris@0: /** Chris@0: * Chris@17: * @type {Object} Chris@0: * Chris@0: * @prop {Array} tags Chris@0: * @prop {Array} attributes Chris@0: * @prop {Array} styles Chris@0: * @prop {Array} classes Chris@0: */ Chris@17: this.required = { Chris@17: tags: [], Chris@17: attributes: [], Chris@17: styles: [], Chris@17: classes: [], Chris@17: }; Chris@0: Chris@0: /** Chris@0: * Chris@17: * @type {Object} Chris@0: * Chris@0: * @prop {Array} tags Chris@0: * @prop {Array} attributes Chris@0: * @prop {Array} styles Chris@0: * @prop {Array} classes Chris@0: */ Chris@17: this.allowed = { Chris@17: tags: [], Chris@17: attributes: [], Chris@17: styles: [], Chris@17: classes: [], Chris@17: }; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {null} Chris@0: */ Chris@0: this.raw = null; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * A text editor feature object. Initialized with the feature name. Chris@0: * Chris@0: * Contains a set of HTML rules ({@link Drupal.EditorFeatureHTMLRule} objects) Chris@0: * that describe which HTML tags, attributes, styles and classes are required Chris@0: * (i.e. essential for the feature to function at all) and which are allowed Chris@0: * (i.e. the feature may generate this, but they're not essential). Chris@0: * Chris@0: * It is necessary to allow for multiple HTML rules per feature: with just Chris@0: * one HTML rule per feature, there is not enough expressiveness to describe Chris@0: * certain cases. For example: a "table" feature would probably require the Chris@0: * `` tag, and might allow e.g. the "summary" attribute on that tag. Chris@0: * However, the table feature would also require the `` and ` Chris@0: * { Chris@0: * tags: ['p', 'strong', 'a'], Chris@0: * allow: true, Chris@0: * restrictedTags: { Chris@0: * tags: [], Chris@0: * allowed: { attributes: [], styles: [], classes: [] }, Chris@0: * forbidden: { attributes: [], styles: [], classes: [] } Chris@0: * } Chris@0: * } Chris@0: * @example Chris@0: * Chris@0: * { Chris@0: * tags: [], Chris@0: * allow: null, Chris@0: * restrictedTags: { Chris@0: * tags: ['a'], Chris@0: * allowed: { attributes: ['href'], styles: [], classes: ['external'] }, Chris@0: * forbidden: { attributes: ['target'], styles: [], classes: [] } Chris@0: * } Chris@0: * } Chris@0: * @example Chris@0: * Chris@0: * { Chris@0: * tags: [], Chris@0: * allow: null, Chris@0: * restrictedTags: { Chris@0: * tags: ['*'], Chris@0: * allowed: { attributes: ['data-*'], styles: [], classes: [] }, Chris@0: * forbidden: { attributes: [], styles: [], classes: [] } Chris@0: * } Chris@0: * } Chris@0: * Chris@0: * @return {object} Chris@0: * An object with the following structure: Chris@0: * ``` Chris@0: * { Chris@0: * tags: Array, Chris@0: * allow: null, Chris@0: * restrictedTags: { Chris@0: * tags: Array, Chris@0: * allowed: {attributes: Array, styles: Array, classes: Array}, Chris@0: * forbidden: {attributes: Array, styles: Array, classes: Array} Chris@0: * } Chris@0: * } Chris@0: * ``` Chris@0: * Chris@0: * @see Drupal.FilterStatus Chris@0: */ Chris@17: Drupal.FilterHTMLRule = function() { Chris@0: // Allow or forbid tags. Chris@0: this.tags = []; Chris@0: this.allow = null; Chris@0: Chris@0: // Apply restrictions to properties set on tags. Chris@0: this.restrictedTags = { Chris@0: tags: [], Chris@0: allowed: { attributes: [], styles: [], classes: [] }, Chris@0: forbidden: { attributes: [], styles: [], classes: [] }, Chris@0: }; Chris@0: Chris@0: return this; Chris@0: }; Chris@0: Chris@17: Drupal.FilterHTMLRule.prototype.clone = function() { Chris@0: const clone = new Drupal.FilterHTMLRule(); Chris@0: clone.tags = this.tags.slice(0); Chris@0: clone.allow = this.allow; Chris@0: clone.restrictedTags.tags = this.restrictedTags.tags.slice(0); Chris@17: clone.restrictedTags.allowed.attributes = this.restrictedTags.allowed.attributes.slice( Chris@17: 0, Chris@17: ); Chris@17: clone.restrictedTags.allowed.styles = this.restrictedTags.allowed.styles.slice( Chris@17: 0, Chris@17: ); Chris@17: clone.restrictedTags.allowed.classes = this.restrictedTags.allowed.classes.slice( Chris@17: 0, Chris@17: ); Chris@17: clone.restrictedTags.forbidden.attributes = this.restrictedTags.forbidden.attributes.slice( Chris@17: 0, Chris@17: ); Chris@17: clone.restrictedTags.forbidden.styles = this.restrictedTags.forbidden.styles.slice( Chris@17: 0, Chris@17: ); Chris@17: clone.restrictedTags.forbidden.classes = this.restrictedTags.forbidden.classes.slice( Chris@17: 0, Chris@17: ); Chris@0: return clone; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Tracks the configuration of all text filters in {@link Drupal.FilterStatus} Chris@0: * objects for {@link Drupal.editorConfiguration.featureIsAllowedByFilters}. Chris@0: * Chris@0: * @namespace Chris@0: */ Chris@0: Drupal.filterConfiguration = { Chris@0: /** Chris@0: * Drupal.FilterStatus objects, keyed by filter ID. Chris@0: * Chris@0: * @type {Object.} Chris@0: */ Chris@0: statuses: {}, Chris@0: Chris@0: /** Chris@0: * Live filter setting parsers. Chris@0: * Chris@0: * Object keyed by filter ID, for those filters that implement it. Chris@0: * Chris@0: * Filters should load the implementing JavaScript on the filter Chris@0: * configuration form and implement Chris@0: * `Drupal.filterSettings[filterID].getRules()`, which should return an Chris@0: * array of {@link Drupal.FilterHTMLRule} objects. Chris@0: * Chris@0: * @namespace Chris@0: */ Chris@0: liveSettingParsers: {}, Chris@0: Chris@0: /** Chris@0: * Updates all {@link Drupal.FilterStatus} objects to reflect current state. Chris@0: * Chris@0: * Automatically checks whether a filter is currently enabled or not. To Chris@0: * support more finegrained. Chris@0: * Chris@0: * If a filter implements a live setting parser, then that will be used to Chris@0: * keep the HTML rules for the {@link Drupal.FilterStatus} object Chris@0: * up-to-date. Chris@0: */ Chris@0: update() { Chris@17: Object.keys(Drupal.filterConfiguration.statuses || {}).forEach( Chris@17: filterID => { Chris@17: // Update status. Chris@17: Drupal.filterConfiguration.statuses[filterID].active = $( Chris@17: `[name="filters[${filterID}][status]"]`, Chris@17: ).is(':checked'); Chris@0: Chris@17: // Update current rules. Chris@17: if (Drupal.filterConfiguration.liveSettingParsers[filterID]) { Chris@17: Drupal.filterConfiguration.statuses[ Chris@17: filterID Chris@17: ].rules = Drupal.filterConfiguration.liveSettingParsers[ Chris@17: filterID Chris@17: ].getRules(); Chris@17: } Chris@17: }, Chris@17: ); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Initializes {@link Drupal.filterConfiguration}. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Gets filter configuration from filter form input. Chris@0: */ Chris@0: Drupal.behaviors.initializeFilterConfiguration = { Chris@0: attach(context, settings) { Chris@0: const $context = $(context); Chris@0: Chris@17: $context Chris@17: .find('#filters-status-wrapper input.form-checkbox') Chris@17: .once('filter-editor-status') Chris@17: .each(function() { Chris@17: const $checkbox = $(this); Chris@17: const nameAttribute = $checkbox.attr('name'); Chris@0: Chris@17: // The filter's checkbox has a name attribute of the form Chris@17: // "filters[][status]", parse "" Chris@17: // from it. Chris@17: const filterID = nameAttribute.substring( Chris@17: 8, Chris@17: nameAttribute.indexOf(']'), Chris@17: ); Chris@0: Chris@17: // Create a Drupal.FilterStatus object to track the state (whether it's Chris@17: // active or not and its current settings, if any) of each filter. Chris@17: Drupal.filterConfiguration.statuses[ Chris@17: filterID Chris@17: ] = new Drupal.FilterStatus(filterID); Chris@17: }); Chris@0: }, Chris@0: }; Chris@17: })(jQuery, _, Drupal, document);
` tags, Chris@0: * but it doesn't make sense to allow for a "summary" attribute on these tags. Chris@0: * Hence these would need to be split in two separate rules. Chris@0: * Chris@0: * HTML rules must be added with the `addHTMLRule()` method. A feature that Chris@0: * has zero HTML rules does not create or modify HTML. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {string} name Chris@0: * The name of the feature. Chris@0: * Chris@0: * @see Drupal.EditorFeatureHTMLRule Chris@0: */ Chris@17: Drupal.EditorFeature = function(name) { Chris@0: this.name = name; Chris@0: this.rules = []; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Adds a HTML rule to the list of HTML rules for this feature. Chris@0: * Chris@0: * @param {Drupal.EditorFeatureHTMLRule} rule Chris@0: * A text editor feature HTML rule. Chris@0: */ Chris@17: Drupal.EditorFeature.prototype.addHTMLRule = function(rule) { Chris@0: this.rules.push(rule); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Text filter status object. Initialized with the filter ID. Chris@0: * Chris@0: * Indicates whether the text filter is currently active (enabled) or not. Chris@0: * Chris@0: * Contains a set of HTML rules ({@link Drupal.FilterHTMLRule} objects) that Chris@0: * describe which HTML tags are allowed or forbidden. They can also describe Chris@0: * for a set of tags (or all tags) which attributes, styles and classes are Chris@0: * allowed and which are forbidden. Chris@0: * Chris@0: * It is necessary to allow for multiple HTML rules per feature, for Chris@0: * analogous reasons as {@link Drupal.EditorFeature}. Chris@0: * Chris@0: * HTML rules must be added with the `addHTMLRule()` method. A filter that has Chris@0: * zero HTML rules does not disallow any HTML. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {string} name Chris@0: * The name of the feature. Chris@0: * Chris@0: * @see Drupal.FilterHTMLRule Chris@0: */ Chris@17: Drupal.FilterStatus = function(name) { Chris@0: /** Chris@0: * Chris@0: * @type {string} Chris@0: */ Chris@0: this.name = name; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {bool} Chris@0: */ Chris@0: this.active = false; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {Array.} Chris@0: */ Chris@0: this.rules = []; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Adds a HTML rule to the list of HTML rules for this filter. Chris@0: * Chris@0: * @param {Drupal.FilterHTMLRule} rule Chris@0: * A text filter HTML rule. Chris@0: */ Chris@17: Drupal.FilterStatus.prototype.addHTMLRule = function(rule) { Chris@0: this.rules.push(rule); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * A text filter HTML rule object. Chris@0: * Chris@0: * Intended to be used in combination with {@link Drupal.FilterStatus}. Chris@0: * Chris@0: * A text filter rule object describes: Chris@0: * 1. allowed or forbidden tags: (optional) whitelist or blacklist HTML tags Chris@0: * 2. restricted tag properties: (optional) whitelist or blacklist Chris@0: * attributes, styles and classes on a set of HTML tags. Chris@0: * Chris@0: * Typically, each text filter rule object does either 1 or 2, not both. Chris@0: * Chris@0: * The structure can be very clearly seen below: Chris@0: * 1. use the "tags" key to list HTML tags, and set the "allow" key to Chris@0: * either true (to allow these HTML tags) or false (to forbid these HTML Chris@0: * tags). If you leave the "tags" key's default value (the empty array), Chris@0: * no restrictions are applied. Chris@0: * 2. all nested within the "restrictedTags" key: use the "tags" subkey to Chris@0: * list HTML tags to which you want to apply property restrictions, then Chris@0: * use the "allowed" subkey to whitelist specific property values, and Chris@0: * similarly use the "forbidden" subkey to blacklist specific property Chris@0: * values. Chris@0: * Chris@0: * @example Chris@0: *
Whitelist the "p", "strong" and "a" HTML tags.For the "a" HTML tag, only allow the "href" attribute Chris@0: * and the "external" class and disallow the "target" attribute.For all tags, allow the "data-*" attribute (that is, any Chris@0: * attribute that begins with "data-").