upgrades core to 8.4.2
This commit is contained in:
@@ -51,7 +51,7 @@ function hook_editor_js_settings_alter(array &$settings) {
|
||||
* @param \Drupal\filter\FilterFormatInterface $format
|
||||
* The text format configuration entity. Provides context based upon which
|
||||
* one may want to adjust the filtering.
|
||||
* @param \Drupal\filter\FilterFormatInterface $original_format|null
|
||||
* @param \Drupal\filter\FilterFormatInterface|null $original_format
|
||||
* (optional) The original text format configuration entity (when switching
|
||||
* text formats/editors). Also provides context based upon which one may want
|
||||
* to adjust the filtering.
|
||||
|
||||
@@ -9,8 +9,8 @@ dependencies:
|
||||
- file
|
||||
configure: filter.admin_overview
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-08-16
|
||||
version: '8.3.7'
|
||||
# Information added by Drupal.org packaging script on 2017-11-03
|
||||
version: '8.4.2'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1502903957
|
||||
datestamp: 1509719929
|
||||
|
||||
@@ -245,7 +245,9 @@ function editor_form_filter_admin_format_submit($form, FormStateInterface $form_
|
||||
// Ensure the text format is set: when creating a new text format, this
|
||||
// would equal the empty string.
|
||||
$editor->set('format', $format_id);
|
||||
$editor->setSettings($form_state->getValue(['editor', 'settings']));
|
||||
if ($settings = $form_state->getValue(['editor', 'settings'])) {
|
||||
$editor->setSettings($settings);
|
||||
}
|
||||
$editor->save();
|
||||
}
|
||||
}
|
||||
@@ -276,7 +278,7 @@ function editor_load($format_id) {
|
||||
* @param \Drupal\filter\FilterFormatInterface|null $format
|
||||
* The text format whose text editor will be used or NULL if the previously
|
||||
* defined text format is now disabled.
|
||||
* @param \Drupal\filter\FilterFormatInterface $original_format|null
|
||||
* @param \Drupal\filter\FilterFormatInterface|null $original_format
|
||||
* (optional) The original text format (i.e. when switching text formats,
|
||||
* $format is the text format that is going to be used, $original_format is
|
||||
* the one that was being used initially, the one that is stored in the
|
||||
|
||||
@@ -0,0 +1,924 @@
|
||||
/**
|
||||
* @file
|
||||
* Provides a JavaScript API to broadcast text editor configuration changes.
|
||||
*
|
||||
* Filter implementations may listen to the drupalEditorFeatureAdded,
|
||||
* drupalEditorFeatureRemoved, and drupalEditorFeatureRemoved events on document
|
||||
* to automatically adjust their settings based on the editor configuration.
|
||||
*/
|
||||
|
||||
(function ($, _, Drupal, document) {
|
||||
/**
|
||||
* Editor configuration namespace.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editorConfiguration = {
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is added by the user.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureAdded event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureAdded
|
||||
*/
|
||||
addedFeature(feature) {
|
||||
$(document).trigger('drupalEditorFeatureAdded', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is removed by the user.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureRemoved event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureRemoved
|
||||
*/
|
||||
removedFeature(feature) {
|
||||
$(document).trigger('drupalEditorFeatureRemoved', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is modified, i.e. has different rules.
|
||||
*
|
||||
* For example when the "Bold" button is configured to use the `<b>` tag
|
||||
* instead of the `<strong>` tag.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureModified event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureModified
|
||||
*/
|
||||
modifiedFeature(feature) {
|
||||
$(document).trigger('drupalEditorFeatureModified', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* May be called by a specific text editor's configuration whenever a
|
||||
* feature is being added, to check whether it would require the filter
|
||||
* settings to be updated.
|
||||
*
|
||||
* The canonical use case is when a text editor is being enabled:
|
||||
* preferably
|
||||
* this would not cause the filter settings to be changed; rather, the
|
||||
* default set of buttons (features) for the text editor should adjust
|
||||
* itself to not cause filter setting changes.
|
||||
*
|
||||
* Note: for filters to integrate with this functionality, it is necessary
|
||||
* that they implement
|
||||
* `Drupal.filterSettingsForEditors[filterID].getRules()`.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the given feature is allowed by the current filters.
|
||||
*/
|
||||
featureIsAllowedByFilters(feature) {
|
||||
/**
|
||||
* Generate the universe U of possible values that can result from the
|
||||
* feature's rules' requirements.
|
||||
*
|
||||
* This generates an object of this form:
|
||||
* var universe = {
|
||||
* a: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* 'attributes:href': false,
|
||||
* 'classes:external': false,
|
||||
* },
|
||||
* strong: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* },
|
||||
* img: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* 'attributes:src': false
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* In this example, the given text editor feature resulted in the above
|
||||
* universe, which shows that it must be allowed to generate the a,
|
||||
* strong and img tags. For the a tag, it must be able to set the "href"
|
||||
* attribute and the "external" class. For the strong tag, no further
|
||||
* properties are required. For the img tag, the "src" attribute is
|
||||
* required. The "tag" key is used to track whether that tag was
|
||||
* explicitly allowed by one of the filter's rules. The
|
||||
* "touchedByAllowedPropertyRule" key is used for state tracking that is
|
||||
* essential for filterStatusAllowsFeature() to be able to reason: when
|
||||
* all of a filter's rules have been applied, and none of the forbidden
|
||||
* rules matched (which would have resulted in early termination) yet the
|
||||
* universe has not been made empty (which would be the end result if
|
||||
* everything in the universe were explicitly allowed), then this piece
|
||||
* of state data enables us to determine whether a tag whose properties
|
||||
* were not all explicitly allowed are in fact still allowed, because its
|
||||
* tag was explicitly allowed and there were no filter rules applying
|
||||
* "allowed tag property value" restrictions for this particular tag.
|
||||
*
|
||||
* @param {object} feature
|
||||
* The feature in question.
|
||||
*
|
||||
* @return {object}
|
||||
* The universe generated.
|
||||
*
|
||||
* @see findPropertyValueOnTag()
|
||||
* @see filterStatusAllowsFeature()
|
||||
*/
|
||||
function generateUniverseFromFeatureRequirements(feature) {
|
||||
const properties = ['attributes', 'styles', 'classes'];
|
||||
const universe = {};
|
||||
|
||||
for (let r = 0; r < feature.rules.length; r++) {
|
||||
const featureRule = feature.rules[r];
|
||||
|
||||
// For each tag required by this feature rule, create a basic entry in
|
||||
// the universe.
|
||||
const requiredTags = featureRule.required.tags;
|
||||
for (let t = 0; t < requiredTags.length; t++) {
|
||||
universe[requiredTags[t]] = {
|
||||
// Whether this tag was allowed or not.
|
||||
tag: false,
|
||||
// Whether any filter rule that applies to this tag had an allowed
|
||||
// property rule. i.e. will become true if >=1 filter rule has >=1
|
||||
// allowed property rule.
|
||||
touchedByAllowedPropertyRule: false,
|
||||
// Analogous, but for forbidden property rule.
|
||||
touchedBytouchedByForbiddenPropertyRule: false,
|
||||
};
|
||||
}
|
||||
|
||||
// If no required properties are defined for this rule, we can move on
|
||||
// to the next feature.
|
||||
if (emptyProperties(featureRule.required)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Expand the existing universe, assume that each tags' property
|
||||
// value is disallowed. If the filter rules allow everything in the
|
||||
// feature's universe, then the feature is allowed.
|
||||
for (let p = 0; p < properties.length; p++) {
|
||||
const property = properties[p];
|
||||
for (let pv = 0; pv < featureRule.required[property].length; pv++) {
|
||||
const propertyValue = featureRule.required[property];
|
||||
universe[requiredTags][`${property}:${propertyValue}`] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return universe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided a section of a feature or filter rule, checks if no property
|
||||
* values are defined for all properties: attributes, classes and styles.
|
||||
*
|
||||
* @param {object} section
|
||||
* The section to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if the section has empty properties, false otherwise.
|
||||
*/
|
||||
function emptyProperties(section) {
|
||||
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValueOnTag on the given tag for every property value
|
||||
* that is listed in the "propertyValues" parameter. Supports the wildcard
|
||||
* tag.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} tag
|
||||
* The tag to look for.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
|
||||
// Detect the wildcard case.
|
||||
if (tag === '*') {
|
||||
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
|
||||
}
|
||||
|
||||
let atLeastOneFound = false;
|
||||
_.each(propertyValues, (propertyValue) => {
|
||||
if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValuesOnAllTags for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
|
||||
let atLeastOneFound = false;
|
||||
_.each(_.keys(universe), (tag) => {
|
||||
if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out if a specific property value (potentially containing
|
||||
* wildcards) exists on the given tag. When the "allowing" parameter
|
||||
* equals true, the universe will be updated if that specific property
|
||||
* value exists. Returns true if found, false otherwise.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} tag
|
||||
* The tag to look for.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {string} propertyValue
|
||||
* The property value to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) {
|
||||
// If the tag does not exist in the universe, then it definitely can't
|
||||
// have this specific property value.
|
||||
if (!_.has(universe, tag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = `${property}:${propertyValue}`;
|
||||
|
||||
// Track whether a tag was touched by a filter rule that allows specific
|
||||
// property values on this particular tag.
|
||||
// @see generateUniverseFromFeatureRequirements
|
||||
if (allowing) {
|
||||
universe[tag].touchedByAllowedPropertyRule = true;
|
||||
}
|
||||
|
||||
// The simple case: no wildcard in property value.
|
||||
if (_.indexOf(propertyValue, '*') === -1) {
|
||||
if (_.has(universe, tag) && _.has(universe[tag], key)) {
|
||||
if (allowing) {
|
||||
universe[tag][key] = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// The complex case: wildcard in property value.
|
||||
|
||||
let atLeastOneFound = false;
|
||||
const regex = key.replace(/\*/g, '[^ ]*');
|
||||
_.each(_.keys(universe[tag]), (key) => {
|
||||
if (key.match(regex)) {
|
||||
atLeastOneFound = true;
|
||||
if (allowing) {
|
||||
universe[tag][key] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tag from the universe if the tag itself and each of its
|
||||
* properties are marked as allowed.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to delete from.
|
||||
* @param {string} tag
|
||||
* The tag to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether something was deleted from the universe.
|
||||
*/
|
||||
function deleteFromUniverseIfAllowed(universe, tag) {
|
||||
// Detect the wildcard case.
|
||||
if (tag === '*') {
|
||||
return deleteAllTagsFromUniverseIfAllowed(universe);
|
||||
}
|
||||
if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
|
||||
delete universe[tag];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls deleteFromUniverseIfAllowed for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to delete from.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether something was deleted from the universe.
|
||||
*/
|
||||
function deleteAllTagsFromUniverseIfAllowed(universe) {
|
||||
let atLeastOneDeleted = false;
|
||||
_.each(_.keys(universe), (tag) => {
|
||||
if (deleteFromUniverseIfAllowed(universe, tag)) {
|
||||
atLeastOneDeleted = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any filter rule forbids either a tag or a tag property value
|
||||
* that exists in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* Universe to check.
|
||||
* @param {object} filterStatus
|
||||
* Filter status to use for check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether any filter rule forbids something in the universe.
|
||||
*/
|
||||
function anyForbiddenFilterRuleMatches(universe, filterStatus) {
|
||||
const properties = ['attributes', 'styles', 'classes'];
|
||||
|
||||
// Check if a tag in the universe is forbidden.
|
||||
const allRequiredTags = _.keys(universe);
|
||||
let filterRule;
|
||||
for (let i = 0; i < filterStatus.rules.length; i++) {
|
||||
filterRule = filterStatus.rules[i];
|
||||
if (filterRule.allow === false) {
|
||||
if (_.intersection(allRequiredTags, filterRule.tags).length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a property value of a tag in the universe is forbidden.
|
||||
// For all filter rules…
|
||||
for (let n = 0; n < filterStatus.rules.length; n++) {
|
||||
filterRule = filterStatus.rules[n];
|
||||
// … if there are tags with restricted property values …
|
||||
if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.forbidden)) {
|
||||
// … for all those tags …
|
||||
for (let j = 0; j < filterRule.restrictedTags.tags.length; j++) {
|
||||
const tag = filterRule.restrictedTags.tags[j];
|
||||
// … then iterate over all properties …
|
||||
for (let k = 0; k < properties.length; k++) {
|
||||
const property = properties[k];
|
||||
// … and return true if just one of the forbidden property
|
||||
// values for this tag and property is listed in the universe.
|
||||
if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.forbidden[property], false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies every filter rule's explicit allowing of a tag or a tag
|
||||
* property value to the universe. Whenever both the tag and all of its
|
||||
* required property values are marked as explicitly allowed, they are
|
||||
* deleted from the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* Universe to delete from.
|
||||
* @param {object} filterStatus
|
||||
* The filter status in question.
|
||||
*/
|
||||
function markAllowedTagsAndPropertyValues(universe, filterStatus) {
|
||||
const properties = ['attributes', 'styles', 'classes'];
|
||||
|
||||
// Check if a tag in the universe is allowed.
|
||||
let filterRule;
|
||||
let tag;
|
||||
for (let l = 0; !_.isEmpty(universe) && l < filterStatus.rules.length; l++) {
|
||||
filterRule = filterStatus.rules[l];
|
||||
if (filterRule.allow === true) {
|
||||
for (let m = 0; !_.isEmpty(universe) && m < filterRule.tags.length; m++) {
|
||||
tag = filterRule.tags[m];
|
||||
if (_.has(universe, tag)) {
|
||||
universe[tag].tag = true;
|
||||
deleteFromUniverseIfAllowed(universe, tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a property value of a tag in the universe is allowed.
|
||||
// For all filter rules…
|
||||
for (let i = 0; !_.isEmpty(universe) && i < filterStatus.rules.length; i++) {
|
||||
filterRule = filterStatus.rules[i];
|
||||
// … if there are tags with restricted property values …
|
||||
if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.allowed)) {
|
||||
// … for all those tags …
|
||||
for (let j = 0; !_.isEmpty(universe) && j < filterRule.restrictedTags.tags.length; j++) {
|
||||
tag = filterRule.restrictedTags.tags[j];
|
||||
// … then iterate over all properties …
|
||||
for (let k = 0; k < properties.length; k++) {
|
||||
const property = properties[k];
|
||||
// … and try to delete this tag from the universe if just one
|
||||
// of the allowed property values for this tag and property is
|
||||
// listed in the universe. (Because everything might be allowed
|
||||
// now.)
|
||||
if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.allowed[property], true)) {
|
||||
deleteFromUniverseIfAllowed(universe, tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current status of a filter allows a specific feature
|
||||
* by building the universe of potential values from the feature's
|
||||
* requirements and then checking whether anything in the filter prevents
|
||||
* that.
|
||||
*
|
||||
* @param {object} filterStatus
|
||||
* The filter status in question.
|
||||
* @param {object} feature
|
||||
* The feature requested.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the current status of the filter allows specified feature.
|
||||
*
|
||||
* @see generateUniverseFromFeatureRequirements()
|
||||
*/
|
||||
function filterStatusAllowsFeature(filterStatus, feature) {
|
||||
// An inactive filter by definition allows the feature.
|
||||
if (!filterStatus.active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A feature that specifies no rules has no HTML requirements and is
|
||||
// hence allowed by definition.
|
||||
if (feature.rules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Analogously for a filter that specifies no rules.
|
||||
if (filterStatus.rules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate the universe U of possible values that can result from the
|
||||
// feature's rules' requirements.
|
||||
const universe = generateUniverseFromFeatureRequirements(feature);
|
||||
|
||||
// If anything that is in the universe (and is thus required by the
|
||||
// feature) is forbidden by any of the filter's rules, then this filter
|
||||
// does not allow this feature.
|
||||
if (anyForbiddenFilterRuleMatches(universe, filterStatus)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark anything in the universe that is allowed by any of the filter's
|
||||
// rules as allowed. If everything is explicitly allowed, then the
|
||||
// universe will become empty.
|
||||
markAllowedTagsAndPropertyValues(universe, filterStatus);
|
||||
|
||||
// If there was at least one filter rule allowing tags, then everything
|
||||
// in the universe must be allowed for this feature to be allowed, and
|
||||
// thus by now it must be empty. However, it is still possible that the
|
||||
// filter allows the feature, due to no rules for allowing tag property
|
||||
// values and/or rules for forbidding tag property values. For details:
|
||||
// see the comments below.
|
||||
// @see generateUniverseFromFeatureRequirements()
|
||||
if (_.some(_.pluck(filterStatus.rules, 'allow'))) {
|
||||
// If the universe is empty, then everything was explicitly allowed
|
||||
// and our job is done: this filter allows this feature!
|
||||
if (_.isEmpty(universe)) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, it is still possible that this feature is allowed.
|
||||
|
||||
// Every tag must be explicitly allowed if there are filter rules
|
||||
// doing tag whitelisting.
|
||||
if (!_.every(_.pluck(universe, 'tag'))) {
|
||||
return false;
|
||||
}
|
||||
// Every tag was explicitly allowed, but since the universe is not
|
||||
// empty, one or more tag properties are disallowed. However, if
|
||||
// only blacklisting of tag properties was applied to these tags,
|
||||
// and no whitelisting was ever applied, then it's still fine:
|
||||
// since none of the tag properties were blacklisted, we got to
|
||||
// this point, and since no whitelisting was applied, it doesn't
|
||||
// matter that the properties: this could never have happened
|
||||
// anyway. It's only this late that we can know this for certain.
|
||||
|
||||
const tags = _.keys(universe);
|
||||
// Figure out if there was any rule applying whitelisting tag
|
||||
// restrictions to each of the remaining tags.
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tag = tags[i];
|
||||
if (_.has(universe, tag)) {
|
||||
if (universe[tag].touchedByAllowedPropertyRule === false) {
|
||||
delete universe[tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.isEmpty(universe);
|
||||
}
|
||||
// Otherwise, if all filter rules were doing blacklisting, then the sole
|
||||
// fact that we got to this point indicates that this filter allows for
|
||||
// everything that is required for this feature.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If any filter's current status forbids the editor feature, return
|
||||
// false.
|
||||
Drupal.filterConfiguration.update();
|
||||
for (const filterID in Drupal.filterConfiguration.statuses) {
|
||||
if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
|
||||
const filterStatus = Drupal.filterConfiguration.statuses[filterID];
|
||||
if (!(filterStatusAllowsFeature(filterStatus, feature))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for an editor feature HTML rule.
|
||||
*
|
||||
* Intended to be used in combination with {@link Drupal.EditorFeature}.
|
||||
*
|
||||
* A text editor feature rule object describes both:
|
||||
* - required HTML tags, attributes, styles and classes: without these, the
|
||||
* text editor feature is unable to function. It's possible that a
|
||||
* - allowed HTML tags, attributes, styles and classes: these are optional
|
||||
* in the strictest sense, but it is possible that the feature generates
|
||||
* them.
|
||||
*
|
||||
* The structure can be very clearly seen below: there's a "required" and an
|
||||
* "allowed" key. For each of those, there are objects with the "tags",
|
||||
* "attributes", "styles" and "classes" keys. For all these keys the values
|
||||
* are initialized to the empty array. List each possible value as an array
|
||||
* value. Besides the "required" and "allowed" keys, there's an optional
|
||||
* "raw" key: it allows text editor implementations to optionally pass in
|
||||
* their raw representation instead of the Drupal-defined representation for
|
||||
* HTML rules.
|
||||
*
|
||||
* @example
|
||||
* tags: ['<a>']
|
||||
* attributes: ['href', 'alt']
|
||||
* styles: ['color', 'text-decoration']
|
||||
* classes: ['external', 'internal']
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @see Drupal.EditorFeature
|
||||
*/
|
||||
Drupal.EditorFeatureHTMLRule = function () {
|
||||
/**
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array} tags
|
||||
* @prop {Array} attributes
|
||||
* @prop {Array} styles
|
||||
* @prop {Array} classes
|
||||
*/
|
||||
this.required = { tags: [], attributes: [], styles: [], classes: [] };
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array} tags
|
||||
* @prop {Array} attributes
|
||||
* @prop {Array} styles
|
||||
* @prop {Array} classes
|
||||
*/
|
||||
this.allowed = { tags: [], attributes: [], styles: [], classes: [] };
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {null}
|
||||
*/
|
||||
this.raw = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A text editor feature object. Initialized with the feature name.
|
||||
*
|
||||
* Contains a set of HTML rules ({@link Drupal.EditorFeatureHTMLRule} objects)
|
||||
* that describe which HTML tags, attributes, styles and classes are required
|
||||
* (i.e. essential for the feature to function at all) and which are allowed
|
||||
* (i.e. the feature may generate this, but they're not essential).
|
||||
*
|
||||
* It is necessary to allow for multiple HTML rules per feature: with just
|
||||
* one HTML rule per feature, there is not enough expressiveness to describe
|
||||
* certain cases. For example: a "table" feature would probably require the
|
||||
* `<table>` tag, and might allow e.g. the "summary" attribute on that tag.
|
||||
* However, the table feature would also require the `<tr>` and `<td>` tags,
|
||||
* but it doesn't make sense to allow for a "summary" attribute on these tags.
|
||||
* Hence these would need to be split in two separate rules.
|
||||
*
|
||||
* HTML rules must be added with the `addHTMLRule()` method. A feature that
|
||||
* has zero HTML rules does not create or modify HTML.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the feature.
|
||||
*
|
||||
* @see Drupal.EditorFeatureHTMLRule
|
||||
*/
|
||||
Drupal.EditorFeature = function (name) {
|
||||
this.name = name;
|
||||
this.rules = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a HTML rule to the list of HTML rules for this feature.
|
||||
*
|
||||
* @param {Drupal.EditorFeatureHTMLRule} rule
|
||||
* A text editor feature HTML rule.
|
||||
*/
|
||||
Drupal.EditorFeature.prototype.addHTMLRule = function (rule) {
|
||||
this.rules.push(rule);
|
||||
};
|
||||
|
||||
/**
|
||||
* Text filter status object. Initialized with the filter ID.
|
||||
*
|
||||
* Indicates whether the text filter is currently active (enabled) or not.
|
||||
*
|
||||
* Contains a set of HTML rules ({@link Drupal.FilterHTMLRule} objects) that
|
||||
* describe which HTML tags are allowed or forbidden. They can also describe
|
||||
* for a set of tags (or all tags) which attributes, styles and classes are
|
||||
* allowed and which are forbidden.
|
||||
*
|
||||
* It is necessary to allow for multiple HTML rules per feature, for
|
||||
* analogous reasons as {@link Drupal.EditorFeature}.
|
||||
*
|
||||
* HTML rules must be added with the `addHTMLRule()` method. A filter that has
|
||||
* zero HTML rules does not disallow any HTML.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the feature.
|
||||
*
|
||||
* @see Drupal.FilterHTMLRule
|
||||
*/
|
||||
Drupal.FilterStatus = function (name) {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = name;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
this.active = false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Array.<Drupal.FilterHTMLRule>}
|
||||
*/
|
||||
this.rules = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a HTML rule to the list of HTML rules for this filter.
|
||||
*
|
||||
* @param {Drupal.FilterHTMLRule} rule
|
||||
* A text filter HTML rule.
|
||||
*/
|
||||
Drupal.FilterStatus.prototype.addHTMLRule = function (rule) {
|
||||
this.rules.push(rule);
|
||||
};
|
||||
|
||||
/**
|
||||
* A text filter HTML rule object.
|
||||
*
|
||||
* Intended to be used in combination with {@link Drupal.FilterStatus}.
|
||||
*
|
||||
* A text filter rule object describes:
|
||||
* 1. allowed or forbidden tags: (optional) whitelist or blacklist HTML tags
|
||||
* 2. restricted tag properties: (optional) whitelist or blacklist
|
||||
* attributes, styles and classes on a set of HTML tags.
|
||||
*
|
||||
* Typically, each text filter rule object does either 1 or 2, not both.
|
||||
*
|
||||
* The structure can be very clearly seen below:
|
||||
* 1. use the "tags" key to list HTML tags, and set the "allow" key to
|
||||
* either true (to allow these HTML tags) or false (to forbid these HTML
|
||||
* tags). If you leave the "tags" key's default value (the empty array),
|
||||
* no restrictions are applied.
|
||||
* 2. all nested within the "restrictedTags" key: use the "tags" subkey to
|
||||
* list HTML tags to which you want to apply property restrictions, then
|
||||
* use the "allowed" subkey to whitelist specific property values, and
|
||||
* similarly use the "forbidden" subkey to blacklist specific property
|
||||
* values.
|
||||
*
|
||||
* @example
|
||||
* <caption>Whitelist the "p", "strong" and "a" HTML tags.</caption>
|
||||
* {
|
||||
* tags: ['p', 'strong', 'a'],
|
||||
* allow: true,
|
||||
* restrictedTags: {
|
||||
* tags: [],
|
||||
* allowed: { attributes: [], styles: [], classes: [] },
|
||||
* forbidden: { attributes: [], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
* @example
|
||||
* <caption>For the "a" HTML tag, only allow the "href" attribute
|
||||
* and the "external" class and disallow the "target" attribute.</caption>
|
||||
* {
|
||||
* tags: [],
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: ['a'],
|
||||
* allowed: { attributes: ['href'], styles: [], classes: ['external'] },
|
||||
* forbidden: { attributes: ['target'], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
* @example
|
||||
* <caption>For all tags, allow the "data-*" attribute (that is, any
|
||||
* attribute that begins with "data-").</caption>
|
||||
* {
|
||||
* tags: [],
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: ['*'],
|
||||
* allowed: { attributes: ['data-*'], styles: [], classes: [] },
|
||||
* forbidden: { attributes: [], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return {object}
|
||||
* An object with the following structure:
|
||||
* ```
|
||||
* {
|
||||
* tags: Array,
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: Array,
|
||||
* allowed: {attributes: Array, styles: Array, classes: Array},
|
||||
* forbidden: {attributes: Array, styles: Array, classes: Array}
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see Drupal.FilterStatus
|
||||
*/
|
||||
Drupal.FilterHTMLRule = function () {
|
||||
// Allow or forbid tags.
|
||||
this.tags = [];
|
||||
this.allow = null;
|
||||
|
||||
// Apply restrictions to properties set on tags.
|
||||
this.restrictedTags = {
|
||||
tags: [],
|
||||
allowed: { attributes: [], styles: [], classes: [] },
|
||||
forbidden: { attributes: [], styles: [], classes: [] },
|
||||
};
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
Drupal.FilterHTMLRule.prototype.clone = function () {
|
||||
const clone = new Drupal.FilterHTMLRule();
|
||||
clone.tags = this.tags.slice(0);
|
||||
clone.allow = this.allow;
|
||||
clone.restrictedTags.tags = this.restrictedTags.tags.slice(0);
|
||||
clone.restrictedTags.allowed.attributes = this.restrictedTags.allowed.attributes.slice(0);
|
||||
clone.restrictedTags.allowed.styles = this.restrictedTags.allowed.styles.slice(0);
|
||||
clone.restrictedTags.allowed.classes = this.restrictedTags.allowed.classes.slice(0);
|
||||
clone.restrictedTags.forbidden.attributes = this.restrictedTags.forbidden.attributes.slice(0);
|
||||
clone.restrictedTags.forbidden.styles = this.restrictedTags.forbidden.styles.slice(0);
|
||||
clone.restrictedTags.forbidden.classes = this.restrictedTags.forbidden.classes.slice(0);
|
||||
return clone;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the configuration of all text filters in {@link Drupal.FilterStatus}
|
||||
* objects for {@link Drupal.editorConfiguration.featureIsAllowedByFilters}.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.filterConfiguration = {
|
||||
|
||||
/**
|
||||
* Drupal.FilterStatus objects, keyed by filter ID.
|
||||
*
|
||||
* @type {Object.<string, Drupal.FilterStatus>}
|
||||
*/
|
||||
statuses: {},
|
||||
|
||||
/**
|
||||
* Live filter setting parsers.
|
||||
*
|
||||
* Object keyed by filter ID, for those filters that implement it.
|
||||
*
|
||||
* Filters should load the implementing JavaScript on the filter
|
||||
* configuration form and implement
|
||||
* `Drupal.filterSettings[filterID].getRules()`, which should return an
|
||||
* array of {@link Drupal.FilterHTMLRule} objects.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
liveSettingParsers: {},
|
||||
|
||||
/**
|
||||
* Updates all {@link Drupal.FilterStatus} objects to reflect current state.
|
||||
*
|
||||
* Automatically checks whether a filter is currently enabled or not. To
|
||||
* support more finegrained.
|
||||
*
|
||||
* If a filter implements a live setting parser, then that will be used to
|
||||
* keep the HTML rules for the {@link Drupal.FilterStatus} object
|
||||
* up-to-date.
|
||||
*/
|
||||
update() {
|
||||
for (const filterID in Drupal.filterConfiguration.statuses) {
|
||||
if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
|
||||
// Update status.
|
||||
Drupal.filterConfiguration.statuses[filterID].active = $(`[name="filters[${filterID}][status]"]`).is(':checked');
|
||||
|
||||
// Update current rules.
|
||||
if (Drupal.filterConfiguration.liveSettingParsers[filterID]) {
|
||||
Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes {@link Drupal.filterConfiguration}.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Gets filter configuration from filter form input.
|
||||
*/
|
||||
Drupal.behaviors.initializeFilterConfiguration = {
|
||||
attach(context, settings) {
|
||||
const $context = $(context);
|
||||
|
||||
$context.find('#filters-status-wrapper input.form-checkbox').once('filter-editor-status').each(function () {
|
||||
const $checkbox = $(this);
|
||||
const nameAttribute = $checkbox.attr('name');
|
||||
|
||||
// The filter's checkbox has a name attribute of the form
|
||||
// "filters[<name of filter>][status]", parse "<name of filter>"
|
||||
// from it.
|
||||
const filterID = nameAttribute.substring(8, nameAttribute.indexOf(']'));
|
||||
|
||||
// Create a Drupal.FilterStatus object to track the state (whether it's
|
||||
// active or not and its current settings, if any) of each filter.
|
||||
Drupal.filterConfiguration.statuses[filterID] = new Drupal.FilterStatus(filterID);
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, _, Drupal, document));
|
||||
@@ -1,147 +1,22 @@
|
||||
/**
|
||||
* @file
|
||||
* Provides a JavaScript API to broadcast text editor configuration changes.
|
||||
*
|
||||
* Filter implementations may listen to the drupalEditorFeatureAdded,
|
||||
* drupalEditorFeatureRemoved, and drupalEditorFeatureRemoved events on document
|
||||
* to automatically adjust their settings based on the editor configuration.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, _, Drupal, document) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Editor configuration namespace.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editorConfiguration = {
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is added by the user.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureAdded event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureAdded
|
||||
*/
|
||||
addedFeature: function (feature) {
|
||||
addedFeature: function addedFeature(feature) {
|
||||
$(document).trigger('drupalEditorFeatureAdded', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is removed by the user.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureRemoved event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureRemoved
|
||||
*/
|
||||
removedFeature: function (feature) {
|
||||
removedFeature: function removedFeature(feature) {
|
||||
$(document).trigger('drupalEditorFeatureRemoved', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is modified, i.e. has different rules.
|
||||
*
|
||||
* For example when the "Bold" button is configured to use the `<b>` tag
|
||||
* instead of the `<strong>` tag.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureModified event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureModified
|
||||
*/
|
||||
modifiedFeature: function (feature) {
|
||||
modifiedFeature: function modifiedFeature(feature) {
|
||||
$(document).trigger('drupalEditorFeatureModified', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* May be called by a specific text editor's configuration whenever a
|
||||
* feature is being added, to check whether it would require the filter
|
||||
* settings to be updated.
|
||||
*
|
||||
* The canonical use case is when a text editor is being enabled:
|
||||
* preferably
|
||||
* this would not cause the filter settings to be changed; rather, the
|
||||
* default set of buttons (features) for the text editor should adjust
|
||||
* itself to not cause filter setting changes.
|
||||
*
|
||||
* Note: for filters to integrate with this functionality, it is necessary
|
||||
* that they implement
|
||||
* `Drupal.filterSettingsForEditors[filterID].getRules()`.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the given feature is allowed by the current filters.
|
||||
*/
|
||||
featureIsAllowedByFilters: function (feature) {
|
||||
|
||||
/**
|
||||
* Generate the universe U of possible values that can result from the
|
||||
* feature's rules' requirements.
|
||||
*
|
||||
* This generates an object of this form:
|
||||
* var universe = {
|
||||
* a: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* 'attributes:href': false,
|
||||
* 'classes:external': false,
|
||||
* },
|
||||
* strong: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* },
|
||||
* img: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* 'attributes:src': false
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* In this example, the given text editor feature resulted in the above
|
||||
* universe, which shows that it must be allowed to generate the a,
|
||||
* strong and img tags. For the a tag, it must be able to set the "href"
|
||||
* attribute and the "external" class. For the strong tag, no further
|
||||
* properties are required. For the img tag, the "src" attribute is
|
||||
* required. The "tag" key is used to track whether that tag was
|
||||
* explicitly allowed by one of the filter's rules. The
|
||||
* "touchedByAllowedPropertyRule" key is used for state tracking that is
|
||||
* essential for filterStatusAllowsFeature() to be able to reason: when
|
||||
* all of a filter's rules have been applied, and none of the forbidden
|
||||
* rules matched (which would have resulted in early termination) yet the
|
||||
* universe has not been made empty (which would be the end result if
|
||||
* everything in the universe were explicitly allowed), then this piece
|
||||
* of state data enables us to determine whether a tag whose properties
|
||||
* were not all explicitly allowed are in fact still allowed, because its
|
||||
* tag was explicitly allowed and there were no filter rules applying
|
||||
* "allowed tag property value" restrictions for this particular tag.
|
||||
*
|
||||
* @param {object} feature
|
||||
* The feature in question.
|
||||
*
|
||||
* @return {object}
|
||||
* The universe generated.
|
||||
*
|
||||
* @see findPropertyValueOnTag()
|
||||
* @see filterStatusAllowsFeature()
|
||||
*/
|
||||
featureIsAllowedByFilters: function featureIsAllowedByFilters(feature) {
|
||||
function generateUniverseFromFeatureRequirements(feature) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
var universe = {};
|
||||
@@ -149,31 +24,21 @@
|
||||
for (var r = 0; r < feature.rules.length; r++) {
|
||||
var featureRule = feature.rules[r];
|
||||
|
||||
// For each tag required by this feature rule, create a basic entry in
|
||||
// the universe.
|
||||
var requiredTags = featureRule.required.tags;
|
||||
for (var t = 0; t < requiredTags.length; t++) {
|
||||
universe[requiredTags[t]] = {
|
||||
// Whether this tag was allowed or not.
|
||||
tag: false,
|
||||
// Whether any filter rule that applies to this tag had an allowed
|
||||
// property rule. i.e. will become true if >=1 filter rule has >=1
|
||||
// allowed property rule.
|
||||
|
||||
touchedByAllowedPropertyRule: false,
|
||||
// Analogous, but for forbidden property rule.
|
||||
|
||||
touchedBytouchedByForbiddenPropertyRule: false
|
||||
};
|
||||
}
|
||||
|
||||
// If no required properties are defined for this rule, we can move on
|
||||
// to the next feature.
|
||||
if (emptyProperties(featureRule.required)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Expand the existing universe, assume that each tags' property
|
||||
// value is disallowed. If the filter rules allow everything in the
|
||||
// feature's universe, then the feature is allowed.
|
||||
for (var p = 0; p < properties.length; p++) {
|
||||
var property = properties[p];
|
||||
for (var pv = 0; pv < featureRule.required[property].length; pv++) {
|
||||
@@ -186,41 +51,11 @@
|
||||
return universe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided a section of a feature or filter rule, checks if no property
|
||||
* values are defined for all properties: attributes, classes and styles.
|
||||
*
|
||||
* @param {object} section
|
||||
* The section to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if the section has empty properties, false otherwise.
|
||||
*/
|
||||
function emptyProperties(section) {
|
||||
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValueOnTag on the given tag for every property value
|
||||
* that is listed in the "propertyValues" parameter. Supports the wildcard
|
||||
* tag.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} tag
|
||||
* The tag to look for.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
|
||||
// Detect the wildcard case.
|
||||
if (tag === '*') {
|
||||
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
|
||||
}
|
||||
@@ -234,21 +69,6 @@
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValuesOnAllTags for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
|
||||
var atLeastOneFound = false;
|
||||
_.each(_.keys(universe), function (tag) {
|
||||
@@ -259,43 +79,17 @@
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out if a specific property value (potentially containing
|
||||
* wildcards) exists on the given tag. When the "allowing" parameter
|
||||
* equals true, the universe will be updated if that specific property
|
||||
* value exists. Returns true if found, false otherwise.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} tag
|
||||
* The tag to look for.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {string} propertyValue
|
||||
* The property value to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) {
|
||||
// If the tag does not exist in the universe, then it definitely can't
|
||||
// have this specific property value.
|
||||
if (!_.has(universe, tag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = property + ':' + propertyValue;
|
||||
|
||||
// Track whether a tag was touched by a filter rule that allows specific
|
||||
// property values on this particular tag.
|
||||
// @see generateUniverseFromFeatureRequirements
|
||||
if (allowing) {
|
||||
universe[tag].touchedByAllowedPropertyRule = true;
|
||||
}
|
||||
|
||||
// The simple case: no wildcard in property value.
|
||||
if (_.indexOf(propertyValue, '*') === -1) {
|
||||
if (_.has(universe, tag) && _.has(universe[tag], key)) {
|
||||
if (allowing) {
|
||||
@@ -305,36 +99,22 @@
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// The complex case: wildcard in property value.
|
||||
else {
|
||||
var atLeastOneFound = false;
|
||||
var regex = key.replace(/\*/g, '[^ ]*');
|
||||
_.each(_.keys(universe[tag]), function (key) {
|
||||
if (key.match(regex)) {
|
||||
atLeastOneFound = true;
|
||||
if (allowing) {
|
||||
universe[tag][key] = true;
|
||||
}
|
||||
|
||||
|
||||
var atLeastOneFound = false;
|
||||
var regex = key.replace(/\*/g, '[^ ]*');
|
||||
_.each(_.keys(universe[tag]), function (key) {
|
||||
if (key.match(regex)) {
|
||||
atLeastOneFound = true;
|
||||
if (allowing) {
|
||||
universe[tag][key] = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tag from the universe if the tag itself and each of its
|
||||
* properties are marked as allowed.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to delete from.
|
||||
* @param {string} tag
|
||||
* The tag to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether something was deleted from the universe.
|
||||
*/
|
||||
function deleteFromUniverseIfAllowed(universe, tag) {
|
||||
// Detect the wildcard case.
|
||||
if (tag === '*') {
|
||||
return deleteAllTagsFromUniverseIfAllowed(universe);
|
||||
}
|
||||
@@ -345,15 +125,6 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls deleteFromUniverseIfAllowed for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to delete from.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether something was deleted from the universe.
|
||||
*/
|
||||
function deleteAllTagsFromUniverseIfAllowed(universe) {
|
||||
var atLeastOneDeleted = false;
|
||||
_.each(_.keys(universe), function (tag) {
|
||||
@@ -364,24 +135,11 @@
|
||||
return atLeastOneDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any filter rule forbids either a tag or a tag property value
|
||||
* that exists in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* Universe to check.
|
||||
* @param {object} filterStatus
|
||||
* Filter status to use for check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether any filter rule forbids something in the universe.
|
||||
*/
|
||||
function anyForbiddenFilterRuleMatches(universe, filterStatus) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
|
||||
// Check if a tag in the universe is forbidden.
|
||||
var allRequiredTags = _.keys(universe);
|
||||
var filterRule;
|
||||
var filterRule = void 0;
|
||||
for (var i = 0; i < filterStatus.rules.length; i++) {
|
||||
filterRule = filterStatus.rules[i];
|
||||
if (filterRule.allow === false) {
|
||||
@@ -391,20 +149,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a property value of a tag in the universe is forbidden.
|
||||
// For all filter rules…
|
||||
for (var n = 0; n < filterStatus.rules.length; n++) {
|
||||
filterRule = filterStatus.rules[n];
|
||||
// … if there are tags with restricted property values …
|
||||
|
||||
if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.forbidden)) {
|
||||
// … for all those tags …
|
||||
for (var j = 0; j < filterRule.restrictedTags.tags.length; j++) {
|
||||
var tag = filterRule.restrictedTags.tags[j];
|
||||
// … then iterate over all properties …
|
||||
|
||||
for (var k = 0; k < properties.length; k++) {
|
||||
var property = properties[k];
|
||||
// … and return true if just one of the forbidden property
|
||||
// values for this tag and property is listed in the universe.
|
||||
|
||||
if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.forbidden[property], false)) {
|
||||
return true;
|
||||
}
|
||||
@@ -416,23 +170,11 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies every filter rule's explicit allowing of a tag or a tag
|
||||
* property value to the universe. Whenever both the tag and all of its
|
||||
* required property values are marked as explicitly allowed, they are
|
||||
* deleted from the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* Universe to delete from.
|
||||
* @param {object} filterStatus
|
||||
* The filter status in question.
|
||||
*/
|
||||
function markAllowedTagsAndPropertyValues(universe, filterStatus) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
|
||||
// Check if a tag in the universe is allowed.
|
||||
var filterRule;
|
||||
var tag;
|
||||
var filterRule = void 0;
|
||||
var tag = void 0;
|
||||
for (var l = 0; !_.isEmpty(universe) && l < filterStatus.rules.length; l++) {
|
||||
filterRule = filterStatus.rules[l];
|
||||
if (filterRule.allow === true) {
|
||||
@@ -446,22 +188,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a property value of a tag in the universe is allowed.
|
||||
// For all filter rules…
|
||||
for (var i = 0; !_.isEmpty(universe) && i < filterStatus.rules.length; i++) {
|
||||
filterRule = filterStatus.rules[i];
|
||||
// … if there are tags with restricted property values …
|
||||
|
||||
if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.allowed)) {
|
||||
// … for all those tags …
|
||||
for (var j = 0; !_.isEmpty(universe) && j < filterRule.restrictedTags.tags.length; j++) {
|
||||
tag = filterRule.restrictedTags.tags[j];
|
||||
// … then iterate over all properties …
|
||||
|
||||
for (var k = 0; k < properties.length; k++) {
|
||||
var property = properties[k];
|
||||
// … and try to delete this tag from the universe if just one
|
||||
// of the allowed property values for this tag and property is
|
||||
// listed in the universe. (Because everything might be allowed
|
||||
// now.)
|
||||
|
||||
if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.allowed[property], true)) {
|
||||
deleteFromUniverseIfAllowed(universe, tag);
|
||||
}
|
||||
@@ -471,114 +207,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current status of a filter allows a specific feature
|
||||
* by building the universe of potential values from the feature's
|
||||
* requirements and then checking whether anything in the filter prevents
|
||||
* that.
|
||||
*
|
||||
* @param {object} filterStatus
|
||||
* The filter status in question.
|
||||
* @param {object} feature
|
||||
* The feature requested.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the current status of the filter allows specified feature.
|
||||
*
|
||||
* @see generateUniverseFromFeatureRequirements()
|
||||
*/
|
||||
function filterStatusAllowsFeature(filterStatus, feature) {
|
||||
// An inactive filter by definition allows the feature.
|
||||
if (!filterStatus.active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A feature that specifies no rules has no HTML requirements and is
|
||||
// hence allowed by definition.
|
||||
if (feature.rules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Analogously for a filter that specifies no rules.
|
||||
if (filterStatus.rules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate the universe U of possible values that can result from the
|
||||
// feature's rules' requirements.
|
||||
var universe = generateUniverseFromFeatureRequirements(feature);
|
||||
|
||||
// If anything that is in the universe (and is thus required by the
|
||||
// feature) is forbidden by any of the filter's rules, then this filter
|
||||
// does not allow this feature.
|
||||
if (anyForbiddenFilterRuleMatches(universe, filterStatus)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark anything in the universe that is allowed by any of the filter's
|
||||
// rules as allowed. If everything is explicitly allowed, then the
|
||||
// universe will become empty.
|
||||
markAllowedTagsAndPropertyValues(universe, filterStatus);
|
||||
|
||||
// If there was at least one filter rule allowing tags, then everything
|
||||
// in the universe must be allowed for this feature to be allowed, and
|
||||
// thus by now it must be empty. However, it is still possible that the
|
||||
// filter allows the feature, due to no rules for allowing tag property
|
||||
// values and/or rules for forbidding tag property values. For details:
|
||||
// see the comments below.
|
||||
// @see generateUniverseFromFeatureRequirements()
|
||||
if (_.some(_.pluck(filterStatus.rules, 'allow'))) {
|
||||
// If the universe is empty, then everything was explicitly allowed
|
||||
// and our job is done: this filter allows this feature!
|
||||
if (_.isEmpty(universe)) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, it is still possible that this feature is allowed.
|
||||
else {
|
||||
// Every tag must be explicitly allowed if there are filter rules
|
||||
// doing tag whitelisting.
|
||||
if (!_.every(_.pluck(universe, 'tag'))) {
|
||||
return false;
|
||||
}
|
||||
// Every tag was explicitly allowed, but since the universe is not
|
||||
// empty, one or more tag properties are disallowed. However, if
|
||||
// only blacklisting of tag properties was applied to these tags,
|
||||
// and no whitelisting was ever applied, then it's still fine:
|
||||
// since none of the tag properties were blacklisted, we got to
|
||||
// this point, and since no whitelisting was applied, it doesn't
|
||||
// matter that the properties: this could never have happened
|
||||
// anyway. It's only this late that we can know this for certain.
|
||||
else {
|
||||
var tags = _.keys(universe);
|
||||
// Figure out if there was any rule applying whitelisting tag
|
||||
// restrictions to each of the remaining tags.
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
var tag = tags[i];
|
||||
if (_.has(universe, tag)) {
|
||||
if (universe[tag].touchedByAllowedPropertyRule === false) {
|
||||
delete universe[tag];
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.every(_.pluck(universe, 'tag'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
var tags = _.keys(universe);
|
||||
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
var tag = tags[i];
|
||||
if (_.has(universe, tag)) {
|
||||
if (universe[tag].touchedByAllowedPropertyRule === false) {
|
||||
delete universe[tag];
|
||||
}
|
||||
return _.isEmpty(universe);
|
||||
}
|
||||
}
|
||||
return _.isEmpty(universe);
|
||||
}
|
||||
// Otherwise, if all filter rules were doing blacklisting, then the sole
|
||||
// fact that we got to this point indicates that this filter allows for
|
||||
// everything that is required for this feature.
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If any filter's current status forbids the editor feature, return
|
||||
// false.
|
||||
Drupal.filterConfiguration.update();
|
||||
for (var filterID in Drupal.filterConfiguration.statuses) {
|
||||
if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
|
||||
var filterStatus = Drupal.filterConfiguration.statuses[filterID];
|
||||
if (!(filterStatusAllowsFeature(filterStatus, feature))) {
|
||||
if (!filterStatusAllowsFeature(filterStatus, feature)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -588,248 +269,43 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for an editor feature HTML rule.
|
||||
*
|
||||
* Intended to be used in combination with {@link Drupal.EditorFeature}.
|
||||
*
|
||||
* A text editor feature rule object describes both:
|
||||
* - required HTML tags, attributes, styles and classes: without these, the
|
||||
* text editor feature is unable to function. It's possible that a
|
||||
* - allowed HTML tags, attributes, styles and classes: these are optional
|
||||
* in the strictest sense, but it is possible that the feature generates
|
||||
* them.
|
||||
*
|
||||
* The structure can be very clearly seen below: there's a "required" and an
|
||||
* "allowed" key. For each of those, there are objects with the "tags",
|
||||
* "attributes", "styles" and "classes" keys. For all these keys the values
|
||||
* are initialized to the empty array. List each possible value as an array
|
||||
* value. Besides the "required" and "allowed" keys, there's an optional
|
||||
* "raw" key: it allows text editor implementations to optionally pass in
|
||||
* their raw representation instead of the Drupal-defined representation for
|
||||
* HTML rules.
|
||||
*
|
||||
* @example
|
||||
* tags: ['<a>']
|
||||
* attributes: ['href', 'alt']
|
||||
* styles: ['color', 'text-decoration']
|
||||
* classes: ['external', 'internal']
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @see Drupal.EditorFeature
|
||||
*/
|
||||
Drupal.EditorFeatureHTMLRule = function () {
|
||||
this.required = { tags: [], attributes: [], styles: [], classes: [] };
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array} tags
|
||||
* @prop {Array} attributes
|
||||
* @prop {Array} styles
|
||||
* @prop {Array} classes
|
||||
*/
|
||||
this.required = {tags: [], attributes: [], styles: [], classes: []};
|
||||
this.allowed = { tags: [], attributes: [], styles: [], classes: [] };
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array} tags
|
||||
* @prop {Array} attributes
|
||||
* @prop {Array} styles
|
||||
* @prop {Array} classes
|
||||
*/
|
||||
this.allowed = {tags: [], attributes: [], styles: [], classes: []};
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {null}
|
||||
*/
|
||||
this.raw = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A text editor feature object. Initialized with the feature name.
|
||||
*
|
||||
* Contains a set of HTML rules ({@link Drupal.EditorFeatureHTMLRule} objects)
|
||||
* that describe which HTML tags, attributes, styles and classes are required
|
||||
* (i.e. essential for the feature to function at all) and which are allowed
|
||||
* (i.e. the feature may generate this, but they're not essential).
|
||||
*
|
||||
* It is necessary to allow for multiple HTML rules per feature: with just
|
||||
* one HTML rule per feature, there is not enough expressiveness to describe
|
||||
* certain cases. For example: a "table" feature would probably require the
|
||||
* `<table>` tag, and might allow e.g. the "summary" attribute on that tag.
|
||||
* However, the table feature would also require the `<tr>` and `<td>` tags,
|
||||
* but it doesn't make sense to allow for a "summary" attribute on these tags.
|
||||
* Hence these would need to be split in two separate rules.
|
||||
*
|
||||
* HTML rules must be added with the `addHTMLRule()` method. A feature that
|
||||
* has zero HTML rules does not create or modify HTML.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the feature.
|
||||
*
|
||||
* @see Drupal.EditorFeatureHTMLRule
|
||||
*/
|
||||
Drupal.EditorFeature = function (name) {
|
||||
this.name = name;
|
||||
this.rules = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a HTML rule to the list of HTML rules for this feature.
|
||||
*
|
||||
* @param {Drupal.EditorFeatureHTMLRule} rule
|
||||
* A text editor feature HTML rule.
|
||||
*/
|
||||
Drupal.EditorFeature.prototype.addHTMLRule = function (rule) {
|
||||
this.rules.push(rule);
|
||||
};
|
||||
|
||||
/**
|
||||
* Text filter status object. Initialized with the filter ID.
|
||||
*
|
||||
* Indicates whether the text filter is currently active (enabled) or not.
|
||||
*
|
||||
* Contains a set of HTML rules ({@link Drupal.FilterHTMLRule} objects) that
|
||||
* describe which HTML tags are allowed or forbidden. They can also describe
|
||||
* for a set of tags (or all tags) which attributes, styles and classes are
|
||||
* allowed and which are forbidden.
|
||||
*
|
||||
* It is necessary to allow for multiple HTML rules per feature, for
|
||||
* analogous reasons as {@link Drupal.EditorFeature}.
|
||||
*
|
||||
* HTML rules must be added with the `addHTMLRule()` method. A filter that has
|
||||
* zero HTML rules does not disallow any HTML.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the feature.
|
||||
*
|
||||
* @see Drupal.FilterHTMLRule
|
||||
*/
|
||||
Drupal.FilterStatus = function (name) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = name;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
this.active = false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Array.<Drupal.FilterHTMLRule>}
|
||||
*/
|
||||
this.rules = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a HTML rule to the list of HTML rules for this filter.
|
||||
*
|
||||
* @param {Drupal.FilterHTMLRule} rule
|
||||
* A text filter HTML rule.
|
||||
*/
|
||||
Drupal.FilterStatus.prototype.addHTMLRule = function (rule) {
|
||||
this.rules.push(rule);
|
||||
};
|
||||
|
||||
/**
|
||||
* A text filter HTML rule object.
|
||||
*
|
||||
* Intended to be used in combination with {@link Drupal.FilterStatus}.
|
||||
*
|
||||
* A text filter rule object describes:
|
||||
* 1. allowed or forbidden tags: (optional) whitelist or blacklist HTML tags
|
||||
* 2. restricted tag properties: (optional) whitelist or blacklist
|
||||
* attributes, styles and classes on a set of HTML tags.
|
||||
*
|
||||
* Typically, each text filter rule object does either 1 or 2, not both.
|
||||
*
|
||||
* The structure can be very clearly seen below:
|
||||
* 1. use the "tags" key to list HTML tags, and set the "allow" key to
|
||||
* either true (to allow these HTML tags) or false (to forbid these HTML
|
||||
* tags). If you leave the "tags" key's default value (the empty array),
|
||||
* no restrictions are applied.
|
||||
* 2. all nested within the "restrictedTags" key: use the "tags" subkey to
|
||||
* list HTML tags to which you want to apply property restrictions, then
|
||||
* use the "allowed" subkey to whitelist specific property values, and
|
||||
* similarly use the "forbidden" subkey to blacklist specific property
|
||||
* values.
|
||||
*
|
||||
* @example
|
||||
* <caption>Whitelist the "p", "strong" and "a" HTML tags.</caption>
|
||||
* {
|
||||
* tags: ['p', 'strong', 'a'],
|
||||
* allow: true,
|
||||
* restrictedTags: {
|
||||
* tags: [],
|
||||
* allowed: { attributes: [], styles: [], classes: [] },
|
||||
* forbidden: { attributes: [], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
* @example
|
||||
* <caption>For the "a" HTML tag, only allow the "href" attribute
|
||||
* and the "external" class and disallow the "target" attribute.</caption>
|
||||
* {
|
||||
* tags: [],
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: ['a'],
|
||||
* allowed: { attributes: ['href'], styles: [], classes: ['external'] },
|
||||
* forbidden: { attributes: ['target'], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
* @example
|
||||
* <caption>For all tags, allow the "data-*" attribute (that is, any
|
||||
* attribute that begins with "data-").</caption>
|
||||
* {
|
||||
* tags: [],
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: ['*'],
|
||||
* allowed: { attributes: ['data-*'], styles: [], classes: [] },
|
||||
* forbidden: { attributes: [], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return {object}
|
||||
* An object with the following structure:
|
||||
* ```
|
||||
* {
|
||||
* tags: Array,
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: Array,
|
||||
* allowed: {attributes: Array, styles: Array, classes: Array},
|
||||
* forbidden: {attributes: Array, styles: Array, classes: Array}
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see Drupal.FilterStatus
|
||||
*/
|
||||
Drupal.FilterHTMLRule = function () {
|
||||
// Allow or forbid tags.
|
||||
this.tags = [];
|
||||
this.allow = null;
|
||||
|
||||
// Apply restrictions to properties set on tags.
|
||||
this.restrictedTags = {
|
||||
tags: [],
|
||||
allowed: {attributes: [], styles: [], classes: []},
|
||||
forbidden: {attributes: [], styles: [], classes: []}
|
||||
allowed: { attributes: [], styles: [], classes: [] },
|
||||
forbidden: { attributes: [], styles: [], classes: [] }
|
||||
};
|
||||
|
||||
return this;
|
||||
@@ -849,87 +325,36 @@
|
||||
return clone;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the configuration of all text filters in {@link Drupal.FilterStatus}
|
||||
* objects for {@link Drupal.editorConfiguration.featureIsAllowedByFilters}.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.filterConfiguration = {
|
||||
|
||||
/**
|
||||
* Drupal.FilterStatus objects, keyed by filter ID.
|
||||
*
|
||||
* @type {Object.<string, Drupal.FilterStatus>}
|
||||
*/
|
||||
statuses: {},
|
||||
|
||||
/**
|
||||
* Live filter setting parsers.
|
||||
*
|
||||
* Object keyed by filter ID, for those filters that implement it.
|
||||
*
|
||||
* Filters should load the implementing JavaScript on the filter
|
||||
* configuration form and implement
|
||||
* `Drupal.filterSettings[filterID].getRules()`, which should return an
|
||||
* array of {@link Drupal.FilterHTMLRule} objects.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
liveSettingParsers: {},
|
||||
|
||||
/**
|
||||
* Updates all {@link Drupal.FilterStatus} objects to reflect current state.
|
||||
*
|
||||
* Automatically checks whether a filter is currently enabled or not. To
|
||||
* support more finegrained.
|
||||
*
|
||||
* If a filter implements a live setting parser, then that will be used to
|
||||
* keep the HTML rules for the {@link Drupal.FilterStatus} object
|
||||
* up-to-date.
|
||||
*/
|
||||
update: function () {
|
||||
update: function update() {
|
||||
for (var filterID in Drupal.filterConfiguration.statuses) {
|
||||
if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
|
||||
// Update status.
|
||||
Drupal.filterConfiguration.statuses[filterID].active = $('[name="filters[' + filterID + '][status]"]').is(':checked');
|
||||
|
||||
// Update current rules.
|
||||
if (Drupal.filterConfiguration.liveSettingParsers[filterID]) {
|
||||
Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes {@link Drupal.filterConfiguration}.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Gets filter configuration from filter form input.
|
||||
*/
|
||||
Drupal.behaviors.initializeFilterConfiguration = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var $context = $(context);
|
||||
|
||||
$context.find('#filters-status-wrapper input.form-checkbox').once('filter-editor-status').each(function () {
|
||||
var $checkbox = $(this);
|
||||
var nameAttribute = $checkbox.attr('name');
|
||||
|
||||
// The filter's checkbox has a name attribute of the form
|
||||
// "filters[<name of filter>][status]", parse "<name of filter>"
|
||||
// from it.
|
||||
var filterID = nameAttribute.substring(8, nameAttribute.indexOf(']'));
|
||||
|
||||
// Create a Drupal.FilterStatus object to track the state (whether it's
|
||||
// active or not and its current settings, if any) of each filter.
|
||||
Drupal.filterConfiguration.statuses[filterID] = new Drupal.FilterStatus(filterID);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, _, Drupal, document);
|
||||
})(jQuery, _, Drupal, document);
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @file
|
||||
* AJAX commands used by Editor module.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Command to save the contents of an editor-provided modal.
|
||||
*
|
||||
* This command does not close the open modal. It should be followed by a
|
||||
* call to `Drupal.AjaxCommands.prototype.closeDialog`. Editors that are
|
||||
* integrated with dialogs must independently listen for an
|
||||
* `editor:dialogsave` event to save the changes into the contents of their
|
||||
* interface.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* The Drupal.Ajax object.
|
||||
* @param {object} response
|
||||
* The server response from the ajax request.
|
||||
* @param {Array} response.values
|
||||
* The values that were saved.
|
||||
* @param {number} [status]
|
||||
* The status code from the ajax request.
|
||||
*
|
||||
* @fires event:editor:dialogsave
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.editorDialogSave = function (ajax, response, status) {
|
||||
$(window).trigger('editor:dialogsave', [response.values]);
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
@@ -1,34 +1,12 @@
|
||||
/**
|
||||
* @file
|
||||
* AJAX commands used by Editor module.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Command to save the contents of an editor-provided modal.
|
||||
*
|
||||
* This command does not close the open modal. It should be followed by a
|
||||
* call to `Drupal.AjaxCommands.prototype.closeDialog`. Editors that are
|
||||
* integrated with dialogs must independently listen for an
|
||||
* `editor:dialogsave` event to save the changes into the contents of their
|
||||
* interface.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* The Drupal.Ajax object.
|
||||
* @param {object} response
|
||||
* The server response from the ajax request.
|
||||
* @param {Array} response.values
|
||||
* The values that were saved.
|
||||
* @param {number} [status]
|
||||
* The status code from the ajax request.
|
||||
*
|
||||
* @fires event:editor:dialogsave
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.editorDialogSave = function (ajax, response, status) {
|
||||
$(window).trigger('editor:dialogsave', [response.values]);
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* @file
|
||||
* Attaches behavior for the Editor module.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Finds the text area field associated with the given text format selector.
|
||||
*
|
||||
* @param {jQuery} $formatSelector
|
||||
* A text format selector DOM element.
|
||||
*
|
||||
* @return {HTMLElement}
|
||||
* The text area DOM element, if it was found.
|
||||
*/
|
||||
function findFieldForFormatSelector($formatSelector) {
|
||||
const field_id = $formatSelector.attr('data-editor-for');
|
||||
// This selector will only find text areas in the top-level document. We do
|
||||
// not support attaching editors on text areas within iframes.
|
||||
return $(`#${field_id}`).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the text editor on a text area.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The text area DOM element.
|
||||
* @param {string} newFormatID
|
||||
* The text format we're changing to; the text editor for the currently
|
||||
* active text format will be detached, and the text editor for the new text
|
||||
* format will be attached.
|
||||
*/
|
||||
function changeTextEditor(field, newFormatID) {
|
||||
const previousFormatID = field.getAttribute('data-editor-active-text-format');
|
||||
|
||||
// Detach the current editor (if any) and attach a new editor.
|
||||
if (drupalSettings.editor.formats[previousFormatID]) {
|
||||
Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]);
|
||||
}
|
||||
// When no text editor is currently active, stop tracking changes.
|
||||
else {
|
||||
$(field).off('.editor');
|
||||
}
|
||||
|
||||
// Attach the new text editor (if any).
|
||||
if (drupalSettings.editor.formats[newFormatID]) {
|
||||
const format = drupalSettings.editor.formats[newFormatID];
|
||||
filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach);
|
||||
}
|
||||
|
||||
// Store the new active format.
|
||||
field.setAttribute('data-editor-active-text-format', newFormatID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes in text format.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The text format change event.
|
||||
*/
|
||||
function onTextFormatChange(event) {
|
||||
const $select = $(event.target);
|
||||
const field = event.data.field;
|
||||
const activeFormatID = field.getAttribute('data-editor-active-text-format');
|
||||
const newFormatID = $select.val();
|
||||
|
||||
// Prevent double-attaching if the change event is triggered manually.
|
||||
if (newFormatID === activeFormatID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When changing to a text format that has a text editor associated
|
||||
// with it that supports content filtering, then first ask for
|
||||
// confirmation, because switching text formats might cause certain
|
||||
// markup to be stripped away.
|
||||
const supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
|
||||
// If there is no content yet, it's always safe to change the text format.
|
||||
const hasContent = field.value !== '';
|
||||
if (hasContent && supportContentFiltering) {
|
||||
const message = Drupal.t('Changing the text format to %text_format will permanently remove content that is not allowed in that text format.<br><br>Save your changes before switching the text format to avoid losing data.', {
|
||||
'%text_format': $select.find('option:selected').text(),
|
||||
});
|
||||
var confirmationDialog = Drupal.dialog(`<div>${message}</div>`, {
|
||||
title: Drupal.t('Change text format?'),
|
||||
dialogClass: 'editor-change-text-format-modal',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Continue'),
|
||||
class: 'button button--primary',
|
||||
click() {
|
||||
changeTextEditor(field, newFormatID);
|
||||
confirmationDialog.close();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Cancel'),
|
||||
class: 'button',
|
||||
click() {
|
||||
// Restore the active format ID: cancel changing text format. We
|
||||
// cannot simply call event.preventDefault() because jQuery's
|
||||
// change event is only triggered after the change has already
|
||||
// been accepted.
|
||||
$select.val(activeFormatID);
|
||||
confirmationDialog.close();
|
||||
},
|
||||
},
|
||||
],
|
||||
// Prevent this modal from being closed without the user making a choice
|
||||
// as per http://stackoverflow.com/a/5438771.
|
||||
closeOnEscape: false,
|
||||
create() {
|
||||
$(this).parent().find('.ui-dialog-titlebar-close').remove();
|
||||
},
|
||||
beforeClose: false,
|
||||
close(event) {
|
||||
// Automatically destroy the DOM element that was used for the dialog.
|
||||
$(event.target).remove();
|
||||
},
|
||||
});
|
||||
|
||||
confirmationDialog.showModal();
|
||||
}
|
||||
else {
|
||||
changeTextEditor(field, newFormatID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an empty object for editors to place their attachment code.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editors = {};
|
||||
|
||||
/**
|
||||
* Enables editors on text_format elements.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches an editor to an input element.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches an editor from an input element.
|
||||
*/
|
||||
Drupal.behaviors.editor = {
|
||||
attach(context, settings) {
|
||||
// If there are no editor settings, there are no editors to enable.
|
||||
if (!settings.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(context).find('[data-editor-for]').once('editor').each(function () {
|
||||
const $this = $(this);
|
||||
const field = findFieldForFormatSelector($this);
|
||||
|
||||
// Opt-out if no supported text area was found.
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the current active format.
|
||||
const activeFormatID = $this.val();
|
||||
field.setAttribute('data-editor-active-text-format', activeFormatID);
|
||||
|
||||
// Directly attach this text editor, if the text format is enabled.
|
||||
if (settings.editor.formats[activeFormatID]) {
|
||||
// XSS protection for the current text format/editor is performed on
|
||||
// the server side, so we don't need to do anything special here.
|
||||
Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
|
||||
}
|
||||
// When there is no text editor for this text format, still track
|
||||
// changes, because the user has the ability to switch to some text
|
||||
// editor, otherwise this code would not be executed.
|
||||
$(field).on('change.editor keypress.editor', () => {
|
||||
field.setAttribute('data-editor-value-is-changed', 'true');
|
||||
// Just knowing that the value was changed is enough, stop tracking.
|
||||
$(field).off('.editor');
|
||||
});
|
||||
|
||||
// Attach onChange handler to text format selector element.
|
||||
if ($this.is('select')) {
|
||||
$this.on('change.editorAttach', { field }, onTextFormatChange);
|
||||
}
|
||||
// Detach any editor when the containing form is submitted.
|
||||
$this.parents('form').on('submit', (event) => {
|
||||
// Do not detach if the event was canceled.
|
||||
if (event.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
// Detach the current editor (if any).
|
||||
if (settings.editor.formats[activeFormatID]) {
|
||||
Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
detach(context, settings, trigger) {
|
||||
let editors;
|
||||
// The 'serialize' trigger indicates that we should simply update the
|
||||
// underlying element with the new text, without destroying the editor.
|
||||
if (trigger === 'serialize') {
|
||||
// Removing the editor-processed class guarantees that the editor will
|
||||
// be reattached. Only do this if we're planning to destroy the editor.
|
||||
editors = $(context).find('[data-editor-for]').findOnce('editor');
|
||||
}
|
||||
else {
|
||||
editors = $(context).find('[data-editor-for]').removeOnce('editor');
|
||||
}
|
||||
|
||||
editors.each(function () {
|
||||
const $this = $(this);
|
||||
const activeFormatID = $this.val();
|
||||
const field = findFieldForFormatSelector($this);
|
||||
if (field && activeFormatID in settings.editor.formats) {
|
||||
Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches editor behaviors to the field.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
*
|
||||
* @listens event:change
|
||||
*
|
||||
* @fires event:formUpdated
|
||||
*/
|
||||
Drupal.editorAttach = function (field, format) {
|
||||
if (format.editor) {
|
||||
// Attach the text editor.
|
||||
Drupal.editors[format.editor].attach(field, format);
|
||||
|
||||
// Ensures form.js' 'formUpdated' event is triggered even for changes that
|
||||
// happen within the text editor.
|
||||
Drupal.editors[format.editor].onChange(field, () => {
|
||||
$(field).trigger('formUpdated');
|
||||
|
||||
// Keep track of changes, so we know what to do when switching text
|
||||
// formats and guaranteeing XSS protection.
|
||||
field.setAttribute('data-editor-value-is-changed', 'true');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches editor behaviors from the field.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} trigger
|
||||
* Trigger value from the detach behavior.
|
||||
*/
|
||||
Drupal.editorDetach = function (field, format, trigger) {
|
||||
if (format.editor) {
|
||||
Drupal.editors[format.editor].detach(field, format, trigger);
|
||||
|
||||
// Restore the original value if the user didn't make any changes yet.
|
||||
if (field.getAttribute('data-editor-value-is-changed') === 'false') {
|
||||
field.value = field.getAttribute('data-editor-value-original');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter away XSS attack vectors when switching text formats.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} originalFormatID
|
||||
* The text format ID of the original text format.
|
||||
* @param {function} callback
|
||||
* A callback to be called (with no parameters) after the field's value has
|
||||
* been XSS filtered.
|
||||
*/
|
||||
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
|
||||
// A text editor that already is XSS-safe needs no additional measures.
|
||||
if (format.editor.isXssSafe) {
|
||||
callback(field, format);
|
||||
}
|
||||
// Otherwise, ensure XSS safety: let the server XSS filter this value.
|
||||
else {
|
||||
$.ajax({
|
||||
url: Drupal.url(`editor/filter_xss/${format.format}`),
|
||||
type: 'POST',
|
||||
data: {
|
||||
value: field.value,
|
||||
original_format_id: originalFormatID,
|
||||
},
|
||||
dataType: 'json',
|
||||
success(xssFilteredValue) {
|
||||
// If the server returns false, then no XSS filtering is needed.
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
}
|
||||
callback(field, format);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @file
|
||||
* Text editor-based in-place editor for formatted text content in Drupal.
|
||||
*
|
||||
* Depends on editor.module. Works with any (WYSIWYG) editor that implements the
|
||||
* editor.js API, including the optional attachInlineEditor() and onChange()
|
||||
* methods.
|
||||
* For example, assuming that a hypothetical editor's name was "Magical Editor"
|
||||
* and its editor.js API implementation lived at Drupal.editors.magical, this
|
||||
* JavaScript would use:
|
||||
* - Drupal.editors.magical.attachInlineEditor()
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
Drupal.quickedit.editors.editor = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.editor# */{
|
||||
|
||||
/**
|
||||
* The text format for this field.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
textFormat: null,
|
||||
|
||||
/**
|
||||
* Indicates whether this text format has transformations.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
textFormatHasTransformations: null,
|
||||
|
||||
/**
|
||||
* Stores a reference to the text editor object for this field.
|
||||
*
|
||||
* @type {Drupal.quickedit.EditorModel}
|
||||
*/
|
||||
textEditor: null,
|
||||
|
||||
/**
|
||||
* Stores the textual DOM element that is being in-place edited.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$textElement: null,
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Drupal.quickedit.EditorView
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the editor view.
|
||||
*/
|
||||
initialize(options) {
|
||||
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
|
||||
|
||||
const metadata = Drupal.quickedit.metadata.get(this.fieldModel.get('fieldID'), 'custom');
|
||||
this.textFormat = drupalSettings.editor.formats[metadata.format];
|
||||
this.textFormatHasTransformations = metadata.formatHasTransformations;
|
||||
this.textEditor = Drupal.editors[this.textFormat.editor];
|
||||
|
||||
// Store the actual value of this field. We'll need this to restore the
|
||||
// original value when the user discards his modifications.
|
||||
const $fieldItems = this.$el.find('.quickedit-field');
|
||||
if ($fieldItems.length) {
|
||||
this.$textElement = $fieldItems.eq(0);
|
||||
}
|
||||
else {
|
||||
this.$textElement = this.$el;
|
||||
}
|
||||
this.model.set('originalValue', this.$textElement.html());
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The text element edited.
|
||||
*/
|
||||
getEditedElement() {
|
||||
return this.$textElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} fieldModel
|
||||
* The field model.
|
||||
* @param {string} state
|
||||
* The current state.
|
||||
*/
|
||||
stateChange(fieldModel, state) {
|
||||
const editorModel = this.model;
|
||||
const from = fieldModel.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
// Detach the text editor when entering the 'candidate' state from one
|
||||
// of the states where it could have been attached.
|
||||
if (from !== 'inactive' && from !== 'highlighted') {
|
||||
this.textEditor.detach(this.$textElement.get(0), this.textFormat);
|
||||
}
|
||||
// A field model's editor view revert() method is invoked when an
|
||||
// 'active' field becomes a 'candidate' field. But, in the case of
|
||||
// this in-place editor, the content will have been *replaced* if the
|
||||
// text format has transformation filters. Therefore, if we stop
|
||||
// in-place editing this entity, revert explicitly.
|
||||
if (from === 'active' && this.textFormatHasTransformations) {
|
||||
this.revert();
|
||||
}
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
// When transformation filters have been applied to the formatted text
|
||||
// of this field, then we'll need to load a re-formatted version of it
|
||||
// without the transformation filters.
|
||||
if (this.textFormatHasTransformations) {
|
||||
const $textElement = this.$textElement;
|
||||
this._getUntransformedText((untransformedText) => {
|
||||
$textElement.html(untransformedText);
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
}
|
||||
// When no transformation filters have been applied: start WYSIWYG
|
||||
// editing immediately!
|
||||
else {
|
||||
// Defer updating the model until the current state change has
|
||||
// propagated, to not trigger a nested state change event.
|
||||
_.defer(() => {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
var textElement = this.$textElement.get(0);
|
||||
var toolbarView = fieldModel.toolbarView;
|
||||
this.textEditor.attachInlineEditor(
|
||||
textElement,
|
||||
this.textFormat,
|
||||
toolbarView.getMainWysiwygToolgroupId(),
|
||||
toolbarView.getFloatedWysiwygToolgroupId(),
|
||||
);
|
||||
// Set the state to 'changed' whenever the content has changed.
|
||||
this.textEditor.onChange(textElement, (htmlText) => {
|
||||
editorModel.set('currentValue', htmlText);
|
||||
fieldModel.set('state', 'changed');
|
||||
});
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
this.save();
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {object}
|
||||
* The sttings for the quick edit UI.
|
||||
*/
|
||||
getQuickEditUISettings() {
|
||||
return { padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: false };
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
revert() {
|
||||
this.$textElement.html(this.model.get('originalValue'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads untransformed text for this field.
|
||||
*
|
||||
* More accurately: it re-filters formatted text to exclude transformation
|
||||
* filters used by the text format.
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that will receive the untransformed text.
|
||||
*
|
||||
* @see \Drupal\editor\Ajax\GetUntransformedTextCommand
|
||||
*/
|
||||
_getUntransformedText(callback) {
|
||||
const fieldID = this.fieldModel.get('fieldID');
|
||||
|
||||
// Create a Drupal.ajax instance to load the form.
|
||||
const textLoaderAjax = Drupal.ajax({
|
||||
url: Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('editor/!entity_type/!id/!field_name/!langcode/!view_mode')),
|
||||
submit: { nocssjs: true },
|
||||
});
|
||||
|
||||
// Implement a scoped editorGetUntransformedText AJAX command: calls the
|
||||
// callback.
|
||||
textLoaderAjax.commands.editorGetUntransformedText = function (ajax, response, status) {
|
||||
callback(response.data);
|
||||
};
|
||||
|
||||
// This will ensure our scoped editorGetUntransformedText AJAX command
|
||||
// gets called.
|
||||
textLoaderAjax.execute();
|
||||
},
|
||||
|
||||
});
|
||||
}(jQuery, Drupal, drupalSettings, _));
|
||||
@@ -1,59 +1,21 @@
|
||||
/**
|
||||
* @file
|
||||
* Text editor-based in-place editor for formatted text content in Drupal.
|
||||
*
|
||||
* Depends on editor.module. Works with any (WYSIWYG) editor that implements the
|
||||
* editor.js API, including the optional attachInlineEditor() and onChange()
|
||||
* methods.
|
||||
* For example, assuming that a hypothetical editor's name was "Magical Editor"
|
||||
* and its editor.js API implementation lived at Drupal.editors.magical, this
|
||||
* JavaScript would use:
|
||||
* - Drupal.editors.magical.attachInlineEditor()
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.quickedit.editors.editor = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.editor# */{
|
||||
|
||||
/**
|
||||
* The text format for this field.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
Drupal.quickedit.editors.editor = Drupal.quickedit.EditorView.extend({
|
||||
textFormat: null,
|
||||
|
||||
/**
|
||||
* Indicates whether this text format has transformations.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
textFormatHasTransformations: null,
|
||||
|
||||
/**
|
||||
* Stores a reference to the text editor object for this field.
|
||||
*
|
||||
* @type {Drupal.quickedit.EditorModel}
|
||||
*/
|
||||
textEditor: null,
|
||||
|
||||
/**
|
||||
* Stores the textual DOM element that is being in-place edited.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$textElement: null,
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Drupal.quickedit.EditorView
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the editor view.
|
||||
*/
|
||||
initialize: function (options) {
|
||||
initialize: function initialize(options) {
|
||||
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
|
||||
|
||||
var metadata = Drupal.quickedit.metadata.get(this.fieldModel.get('fieldID'), 'custom');
|
||||
@@ -61,37 +23,18 @@
|
||||
this.textFormatHasTransformations = metadata.formatHasTransformations;
|
||||
this.textEditor = Drupal.editors[this.textFormat.editor];
|
||||
|
||||
// Store the actual value of this field. We'll need this to restore the
|
||||
// original value when the user discards his modifications.
|
||||
var $fieldItems = this.$el.find('.quickedit-field');
|
||||
if ($fieldItems.length) {
|
||||
this.$textElement = $fieldItems.eq(0);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.$textElement = this.$el;
|
||||
}
|
||||
this.model.set('originalValue', this.$textElement.html());
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The text element edited.
|
||||
*/
|
||||
getEditedElement: function () {
|
||||
getEditedElement: function getEditedElement() {
|
||||
return this.$textElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} fieldModel
|
||||
* The field model.
|
||||
* @param {string} state
|
||||
* The current state.
|
||||
*/
|
||||
stateChange: function (fieldModel, state) {
|
||||
stateChange: function stateChange(fieldModel, state) {
|
||||
var editorModel = this.model;
|
||||
var from = fieldModel.previous('state');
|
||||
var to = state;
|
||||
@@ -100,16 +43,10 @@
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
// Detach the text editor when entering the 'candidate' state from one
|
||||
// of the states where it could have been attached.
|
||||
if (from !== 'inactive' && from !== 'highlighted') {
|
||||
this.textEditor.detach(this.$textElement.get(0), this.textFormat);
|
||||
}
|
||||
// A field model's editor view revert() method is invoked when an
|
||||
// 'active' field becomes a 'candidate' field. But, in the case of
|
||||
// this in-place editor, the content will have been *replaced* if the
|
||||
// text format has transformation filters. Therefore, if we stop
|
||||
// in-place editing this entity, revert explicitly.
|
||||
|
||||
if (from === 'active' && this.textFormatHasTransformations) {
|
||||
this.revert();
|
||||
}
|
||||
@@ -122,37 +59,24 @@
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
// When transformation filters have been applied to the formatted text
|
||||
// of this field, then we'll need to load a re-formatted version of it
|
||||
// without the transformation filters.
|
||||
if (this.textFormatHasTransformations) {
|
||||
var $textElement = this.$textElement;
|
||||
this._getUntransformedText(function (untransformedText) {
|
||||
$textElement.html(untransformedText);
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
}
|
||||
// When no transformation filters have been applied: start WYSIWYG
|
||||
// editing immediately!
|
||||
else {
|
||||
// Defer updating the model until the current state change has
|
||||
// propagated, to not trigger a nested state change event.
|
||||
_.defer(function () {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_.defer(function () {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
var textElement = this.$textElement.get(0);
|
||||
var toolbarView = fieldModel.toolbarView;
|
||||
this.textEditor.attachInlineEditor(
|
||||
textElement,
|
||||
this.textFormat,
|
||||
toolbarView.getMainWysiwygToolgroupId(),
|
||||
toolbarView.getFloatedWysiwygToolgroupId()
|
||||
);
|
||||
// Set the state to 'changed' whenever the content has changed.
|
||||
this.textEditor.attachInlineEditor(textElement, this.textFormat, toolbarView.getMainWysiwygToolgroupId(), toolbarView.getFloatedWysiwygToolgroupId());
|
||||
|
||||
this.textEditor.onChange(textElement, function (htmlText) {
|
||||
editorModel.set('currentValue', htmlText);
|
||||
fieldModel.set('state', 'changed');
|
||||
@@ -177,55 +101,25 @@
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {object}
|
||||
* The sttings for the quick edit UI.
|
||||
*/
|
||||
getQuickEditUISettings: function () {
|
||||
return {padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: false};
|
||||
getQuickEditUISettings: function getQuickEditUISettings() {
|
||||
return { padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: false };
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
revert: function () {
|
||||
revert: function revert() {
|
||||
this.$textElement.html(this.model.get('originalValue'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads untransformed text for this field.
|
||||
*
|
||||
* More accurately: it re-filters formatted text to exclude transformation
|
||||
* filters used by the text format.
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that will receive the untransformed text.
|
||||
*
|
||||
* @see \Drupal\editor\Ajax\GetUntransformedTextCommand
|
||||
*/
|
||||
_getUntransformedText: function (callback) {
|
||||
_getUntransformedText: function _getUntransformedText(callback) {
|
||||
var fieldID = this.fieldModel.get('fieldID');
|
||||
|
||||
// Create a Drupal.ajax instance to load the form.
|
||||
var textLoaderAjax = Drupal.ajax({
|
||||
url: Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('editor/!entity_type/!id/!field_name/!langcode/!view_mode')),
|
||||
submit: {nocssjs: true}
|
||||
submit: { nocssjs: true }
|
||||
});
|
||||
|
||||
// Implement a scoped editorGetUntransformedText AJAX command: calls the
|
||||
// callback.
|
||||
textLoaderAjax.commands.editorGetUntransformedText = function (ajax, response, status) {
|
||||
callback(response.data);
|
||||
};
|
||||
|
||||
// This will ensure our scoped editorGetUntransformedText AJAX command
|
||||
// gets called.
|
||||
textLoaderAjax.execute();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
||||
@@ -1,83 +1,46 @@
|
||||
/**
|
||||
* @file
|
||||
* Attaches behavior for the Editor module.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Finds the text area field associated with the given text format selector.
|
||||
*
|
||||
* @param {jQuery} $formatSelector
|
||||
* A text format selector DOM element.
|
||||
*
|
||||
* @return {HTMLElement}
|
||||
* The text area DOM element, if it was found.
|
||||
*/
|
||||
function findFieldForFormatSelector($formatSelector) {
|
||||
var field_id = $formatSelector.attr('data-editor-for');
|
||||
// This selector will only find text areas in the top-level document. We do
|
||||
// not support attaching editors on text areas within iframes.
|
||||
|
||||
return $('#' + field_id).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the text editor on a text area.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The text area DOM element.
|
||||
* @param {string} newFormatID
|
||||
* The text format we're changing to; the text editor for the currently
|
||||
* active text format will be detached, and the text editor for the new text
|
||||
* format will be attached.
|
||||
*/
|
||||
function changeTextEditor(field, newFormatID) {
|
||||
var previousFormatID = field.getAttribute('data-editor-active-text-format');
|
||||
|
||||
// Detach the current editor (if any) and attach a new editor.
|
||||
if (drupalSettings.editor.formats[previousFormatID]) {
|
||||
Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]);
|
||||
}
|
||||
// When no text editor is currently active, stop tracking changes.
|
||||
else {
|
||||
$(field).off('.editor');
|
||||
}
|
||||
} else {
|
||||
$(field).off('.editor');
|
||||
}
|
||||
|
||||
// Attach the new text editor (if any).
|
||||
if (drupalSettings.editor.formats[newFormatID]) {
|
||||
var format = drupalSettings.editor.formats[newFormatID];
|
||||
filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach);
|
||||
}
|
||||
|
||||
// Store the new active format.
|
||||
field.setAttribute('data-editor-active-text-format', newFormatID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes in text format.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The text format change event.
|
||||
*/
|
||||
function onTextFormatChange(event) {
|
||||
var $select = $(event.target);
|
||||
var field = event.data.field;
|
||||
var activeFormatID = field.getAttribute('data-editor-active-text-format');
|
||||
var newFormatID = $select.val();
|
||||
|
||||
// Prevent double-attaching if the change event is triggered manually.
|
||||
if (newFormatID === activeFormatID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When changing to a text format that has a text editor associated
|
||||
// with it that supports content filtering, then first ask for
|
||||
// confirmation, because switching text formats might cause certain
|
||||
// markup to be stripped away.
|
||||
var supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
|
||||
// If there is no content yet, it's always safe to change the text format.
|
||||
|
||||
var hasContent = field.value !== '';
|
||||
if (hasContent && supportContentFiltering) {
|
||||
var message = Drupal.t('Changing the text format to %text_format will permanently remove content that is not allowed in that text format.<br><br>Save your changes before switching the text format to avoid losing data.', {
|
||||
@@ -87,68 +50,43 @@
|
||||
title: Drupal.t('Change text format?'),
|
||||
dialogClass: 'editor-change-text-format-modal',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Continue'),
|
||||
class: 'button button--primary',
|
||||
click: function () {
|
||||
changeTextEditor(field, newFormatID);
|
||||
confirmationDialog.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Cancel'),
|
||||
class: 'button',
|
||||
click: function () {
|
||||
// Restore the active format ID: cancel changing text format. We
|
||||
// cannot simply call event.preventDefault() because jQuery's
|
||||
// change event is only triggered after the change has already
|
||||
// been accepted.
|
||||
$select.val(activeFormatID);
|
||||
confirmationDialog.close();
|
||||
}
|
||||
buttons: [{
|
||||
text: Drupal.t('Continue'),
|
||||
class: 'button button--primary',
|
||||
click: function click() {
|
||||
changeTextEditor(field, newFormatID);
|
||||
confirmationDialog.close();
|
||||
}
|
||||
],
|
||||
// Prevent this modal from being closed without the user making a choice
|
||||
// as per http://stackoverflow.com/a/5438771.
|
||||
}, {
|
||||
text: Drupal.t('Cancel'),
|
||||
class: 'button',
|
||||
click: function click() {
|
||||
$select.val(activeFormatID);
|
||||
confirmationDialog.close();
|
||||
}
|
||||
}],
|
||||
|
||||
closeOnEscape: false,
|
||||
create: function () {
|
||||
create: function create() {
|
||||
$(this).parent().find('.ui-dialog-titlebar-close').remove();
|
||||
},
|
||||
|
||||
beforeClose: false,
|
||||
close: function (event) {
|
||||
// Automatically destroy the DOM element that was used for the dialog.
|
||||
close: function close(event) {
|
||||
$(event.target).remove();
|
||||
}
|
||||
});
|
||||
|
||||
confirmationDialog.showModal();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
changeTextEditor(field, newFormatID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an empty object for editors to place their attachment code.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editors = {};
|
||||
|
||||
/**
|
||||
* Enables editors on text_format elements.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches an editor to an input element.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches an editor from an input element.
|
||||
*/
|
||||
Drupal.behaviors.editor = {
|
||||
attach: function (context, settings) {
|
||||
// If there are no editor settings, there are no editors to enable.
|
||||
attach: function attach(context, settings) {
|
||||
if (!settings.editor) {
|
||||
return;
|
||||
}
|
||||
@@ -157,58 +95,44 @@
|
||||
var $this = $(this);
|
||||
var field = findFieldForFormatSelector($this);
|
||||
|
||||
// Opt-out if no supported text area was found.
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the current active format.
|
||||
var activeFormatID = $this.val();
|
||||
field.setAttribute('data-editor-active-text-format', activeFormatID);
|
||||
|
||||
// Directly attach this text editor, if the text format is enabled.
|
||||
if (settings.editor.formats[activeFormatID]) {
|
||||
// XSS protection for the current text format/editor is performed on
|
||||
// the server side, so we don't need to do anything special here.
|
||||
Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
|
||||
}
|
||||
// When there is no text editor for this text format, still track
|
||||
// changes, because the user has the ability to switch to some text
|
||||
// editor, otherwise this code would not be executed.
|
||||
|
||||
$(field).on('change.editor keypress.editor', function () {
|
||||
field.setAttribute('data-editor-value-is-changed', 'true');
|
||||
// Just knowing that the value was changed is enough, stop tracking.
|
||||
|
||||
$(field).off('.editor');
|
||||
});
|
||||
|
||||
// Attach onChange handler to text format selector element.
|
||||
if ($this.is('select')) {
|
||||
$this.on('change.editorAttach', {field: field}, onTextFormatChange);
|
||||
$this.on('change.editorAttach', { field: field }, onTextFormatChange);
|
||||
}
|
||||
// Detach any editor when the containing form is submitted.
|
||||
|
||||
$this.parents('form').on('submit', function (event) {
|
||||
// Do not detach if the event was canceled.
|
||||
if (event.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
// Detach the current editor (if any).
|
||||
|
||||
if (settings.editor.formats[activeFormatID]) {
|
||||
Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
detach: function detach(context, settings, trigger) {
|
||||
var editors = void 0;
|
||||
|
||||
detach: function (context, settings, trigger) {
|
||||
var editors;
|
||||
// The 'serialize' trigger indicates that we should simply update the
|
||||
// underlying element with the new text, without destroying the editor.
|
||||
if (trigger === 'serialize') {
|
||||
// Removing the editor-processed class guarantees that the editor will
|
||||
// be reattached. Only do this if we're planning to destroy the editor.
|
||||
editors = $(context).find('[data-editor-for]').findOnce('editor');
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
editors = $(context).find('[data-editor-for]').removeOnce('editor');
|
||||
}
|
||||
|
||||
@@ -223,96 +147,47 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches editor behaviors to the field.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
*
|
||||
* @listens event:change
|
||||
*
|
||||
* @fires event:formUpdated
|
||||
*/
|
||||
Drupal.editorAttach = function (field, format) {
|
||||
if (format.editor) {
|
||||
// Attach the text editor.
|
||||
Drupal.editors[format.editor].attach(field, format);
|
||||
|
||||
// Ensures form.js' 'formUpdated' event is triggered even for changes that
|
||||
// happen within the text editor.
|
||||
Drupal.editors[format.editor].onChange(field, function () {
|
||||
$(field).trigger('formUpdated');
|
||||
|
||||
// Keep track of changes, so we know what to do when switching text
|
||||
// formats and guaranteeing XSS protection.
|
||||
field.setAttribute('data-editor-value-is-changed', 'true');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches editor behaviors from the field.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} trigger
|
||||
* Trigger value from the detach behavior.
|
||||
*/
|
||||
Drupal.editorDetach = function (field, format, trigger) {
|
||||
if (format.editor) {
|
||||
Drupal.editors[format.editor].detach(field, format, trigger);
|
||||
|
||||
// Restore the original value if the user didn't make any changes yet.
|
||||
if (field.getAttribute('data-editor-value-is-changed') === 'false') {
|
||||
field.value = field.getAttribute('data-editor-value-original');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter away XSS attack vectors when switching text formats.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} originalFormatID
|
||||
* The text format ID of the original text format.
|
||||
* @param {function} callback
|
||||
* A callback to be called (with no parameters) after the field's value has
|
||||
* been XSS filtered.
|
||||
*/
|
||||
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
|
||||
// A text editor that already is XSS-safe needs no additional measures.
|
||||
if (format.editor.isXssSafe) {
|
||||
callback(field, format);
|
||||
}
|
||||
// Otherwise, ensure XSS safety: let the server XSS filter this value.
|
||||
else {
|
||||
$.ajax({
|
||||
url: Drupal.url('editor/filter_xss/' + format.format),
|
||||
type: 'POST',
|
||||
data: {
|
||||
value: field.value,
|
||||
original_format_id: originalFormatID
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function (xssFilteredValue) {
|
||||
// If the server returns false, then no XSS filtering is needed.
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
} else {
|
||||
$.ajax({
|
||||
url: Drupal.url('editor/filter_xss/' + format.format),
|
||||
type: 'POST',
|
||||
data: {
|
||||
value: field.value,
|
||||
original_format_id: originalFormatID
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function success(xssFilteredValue) {
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
}
|
||||
callback(field, format);
|
||||
}
|
||||
callback(field, format);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
@@ -19,7 +19,7 @@ class Standard extends Xss implements EditorXssFilterInterface {
|
||||
// Apply XSS filtering, but blacklist the <script>, <style>, <link>, <embed>
|
||||
// and <object> tags.
|
||||
// The <script> and <style> tags are blacklisted because their contents
|
||||
// can be malicious (and therefor they are inherently unsafe), whereas for
|
||||
// can be malicious (and therefore they are inherently unsafe), whereas for
|
||||
// all other tags, only their attributes can make them malicious. Since
|
||||
// \Drupal\Component\Utility\Xss::filter() protects against malicious
|
||||
// attributes, we take no blacklisting action.
|
||||
@@ -41,10 +41,10 @@ class Standard extends Xss implements EditorXssFilterInterface {
|
||||
// also user frustration: what if a text format is configured to allow
|
||||
// <embed>, for example? Then we would strip that tag, even though it is
|
||||
// allowed, thereby causing data loss!
|
||||
// Therefor, we want to be smarter still. We want to take into account which
|
||||
// HTML tags are allowed and forbidden by the text format we're filtering
|
||||
// for, and if we're switching from another text format, we want to take
|
||||
// that format's allowed and forbidden tags into account as well.
|
||||
// Therefore, we want to be smarter still. We want to take into account
|
||||
// which HTML tags are allowed and forbidden by the text format we're
|
||||
// filtering for, and if we're switching from another text format, we want
|
||||
// to take that format's allowed and forbidden tags into account as well.
|
||||
// In other words: we only expect markup allowed in both the original and
|
||||
// the new format to continue to exist.
|
||||
$format_restrictions = $format->getHtmlRestrictions();
|
||||
|
||||
@@ -50,7 +50,7 @@ class EditorFileReference extends FilterBase implements ContainerFactoryPluginIn
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
|
||||
@@ -127,7 +127,7 @@ class EditorLoadingTest extends WebTestBase {
|
||||
// - doesn't have access to the full_html text format, so: no text editor.
|
||||
$this->drupalLogin($this->normalUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
list(, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$this->assertFalse($editor_settings_present, 'No Text Editor module settings.');
|
||||
$this->assertFalse($editor_js_present, 'No Text Editor JavaScript.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
@@ -140,13 +140,17 @@ class EditorLoadingTest extends WebTestBase {
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$expected = ['formats' => ['full_html' => [
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => ['ponyModeEnabled' => TRUE],
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
]]];
|
||||
$expected = [
|
||||
'formats' => [
|
||||
'full_html' => [
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => ['ponyModeEnabled' => TRUE],
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
@@ -174,13 +178,17 @@ class EditorLoadingTest extends WebTestBase {
|
||||
$this->drupalLogin($this->untrustedUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$expected = ['formats' => ['plain_text' => [
|
||||
'format' => 'plain_text',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => ['ponyModeEnabled' => TRUE],
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
]]];
|
||||
$expected = [
|
||||
'formats' => [
|
||||
'plain_text' => [
|
||||
'format' => 'plain_text',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => ['ponyModeEnabled' => TRUE],
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
@@ -202,7 +210,7 @@ class EditorLoadingTest extends WebTestBase {
|
||||
// that (s)he is not allowed to use. The editor is still loaded. CKEditor,
|
||||
// for example, supports being loaded in a disabled state.
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
list(, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$this->assertTrue($editor_settings_present, 'Text Editor module settings.');
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
@@ -241,7 +249,7 @@ class EditorLoadingTest extends WebTestBase {
|
||||
// Assert the unicorn editor works with textfields.
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
list(, $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($field) === 1, 'A text field exists.');
|
||||
@@ -257,7 +265,7 @@ class EditorLoadingTest extends WebTestBase {
|
||||
])->save();
|
||||
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
list(, $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
$this->assertFalse($editor_settings_present, "Text Editor module's JavaScript settings are not on the page.");
|
||||
$this->assertFalse($editor_js_present, 'Text Editor JavaScript is not present.');
|
||||
$this->assertTrue(count($field) === 1, 'A text field exists.');
|
||||
|
||||
@@ -306,7 +306,8 @@ class EditorSecurityTest extends WebTestBase {
|
||||
$expected = [
|
||||
[
|
||||
'node_id' => 1,
|
||||
'value' => self::$sampleContent, // No text editor => no XSS filtering.
|
||||
// No text editor => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'format' => 'restricted_without_editor',
|
||||
'switch_to' => [
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
@@ -319,7 +320,8 @@ class EditorSecurityTest extends WebTestBase {
|
||||
],
|
||||
[
|
||||
'node_id' => 2,
|
||||
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
|
||||
// Text editor => XSS filtering.
|
||||
'value' => self::$sampleContentSecured,
|
||||
'format' => 'restricted_with_editor',
|
||||
'switch_to' => [
|
||||
// No text editor => no XSS filtering.
|
||||
@@ -333,7 +335,8 @@ class EditorSecurityTest extends WebTestBase {
|
||||
],
|
||||
[
|
||||
'node_id' => 3,
|
||||
'value' => self::$sampleContentSecuredEmbedAllowed, // Text editor => XSS filtering.
|
||||
// Text editor => XSS filtering.
|
||||
'value' => self::$sampleContentSecuredEmbedAllowed,
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
'switch_to' => [
|
||||
// No text editor => no XSS filtering.
|
||||
@@ -348,7 +351,8 @@ class EditorSecurityTest extends WebTestBase {
|
||||
],
|
||||
[
|
||||
'node_id' => 4,
|
||||
'value' => self::$sampleContent, // No text editor => no XSS filtering.
|
||||
// No text editor => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'format' => 'unrestricted_without_editor',
|
||||
'switch_to' => [
|
||||
// No text editor => no XSS filtering.
|
||||
@@ -364,7 +368,8 @@ class EditorSecurityTest extends WebTestBase {
|
||||
],
|
||||
[
|
||||
'node_id' => 5,
|
||||
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
|
||||
// Text editor => XSS filtering.
|
||||
'value' => self::$sampleContentSecured,
|
||||
'format' => 'unrestricted_with_editor',
|
||||
'switch_to' => [
|
||||
// From editor, no security filters to security filters, no editor: no
|
||||
|
||||
@@ -89,12 +89,12 @@ class QuickEditIntegrationLoadingTest extends WebTestBase {
|
||||
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', [], ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
|
||||
$this->assertResponse(403);
|
||||
if (!$user->hasPermission('access in-place editing')) {
|
||||
$message = "A fatal error occurred: The 'access in-place editing' permission is required.";
|
||||
$this->assertIdentical(Json::encode(['message' => $message]), $response);
|
||||
$message = "The 'access in-place editing' permission is required.";
|
||||
}
|
||||
else {
|
||||
$this->assertIdentical('{}', $response);
|
||||
$message = '';
|
||||
}
|
||||
$this->assertIdentical(Json::encode(['message' => $message]), $response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ dependencies:
|
||||
- filter
|
||||
- ckeditor
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-08-16
|
||||
version: '8.3.7'
|
||||
# Information added by Drupal.org packaging script on 2017-11-03
|
||||
version: '8.4.2'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1502903957
|
||||
datestamp: 1509719929
|
||||
|
||||
@@ -5,8 +5,8 @@ description: 'Support module for the Text Editor module tests.'
|
||||
package: Testing
|
||||
# version: VERSION
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-08-16
|
||||
version: '8.3.7'
|
||||
# Information added by Drupal.org packaging script on 2017-11-03
|
||||
version: '8.4.2'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1502903957
|
||||
datestamp: 1509719929
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\editor\Functional\Update;
|
||||
|
||||
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
|
||||
|
||||
/**
|
||||
* Tests Editor module database updates.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorUpdateTest extends UpdatePathTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setDatabaseDumpFiles() {
|
||||
$this->databaseDumpFiles = [
|
||||
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
|
||||
// Simulate an un-synchronized environment.
|
||||
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.editor-editor_update_8001.php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests editor_update_8001().
|
||||
*
|
||||
* @see editor_update_8001()
|
||||
*/
|
||||
public function testEditorUpdate8001() {
|
||||
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
|
||||
$config_factory = $this->container->get('config.factory');
|
||||
|
||||
$format_basic_html = $config_factory->get('filter.format.basic_html');
|
||||
$editor_basic_html = $config_factory->get('editor.editor.basic_html');
|
||||
$format_full_html = $config_factory->get('filter.format.full_html');
|
||||
$editor_full_html = $config_factory->get('editor.editor.full_html');
|
||||
|
||||
// Checks if the 'basic_html' format and editor statuses differ.
|
||||
$this->assertTrue($format_basic_html->get('status'));
|
||||
$this->assertFalse($editor_basic_html->get('status'));
|
||||
$this->assertNotIdentical($format_basic_html->get('status'), $editor_basic_html->get('status'));
|
||||
|
||||
// Checks if the 'full_html' format and editor statuses differ.
|
||||
$this->assertFalse($format_full_html->get('status'));
|
||||
$this->assertTrue($editor_full_html->get('status'));
|
||||
$this->assertNotIdentical($format_full_html->get('status'), $editor_full_html->get('status'));
|
||||
|
||||
|
||||
// Run updates.
|
||||
$this->runUpdates();
|
||||
|
||||
// Reload text formats and editors.
|
||||
$format_basic_html = $config_factory->get('filter.format.basic_html');
|
||||
$editor_basic_html = $config_factory->get('editor.editor.basic_html');
|
||||
$format_full_html = $config_factory->get('filter.format.full_html');
|
||||
$editor_full_html = $config_factory->get('editor.editor.full_html');
|
||||
|
||||
// Checks if the 'basic_html' format and editor statuses are in sync.
|
||||
$this->assertTrue($format_basic_html->get('status'));
|
||||
$this->assertTrue($editor_basic_html->get('status'));
|
||||
$this->assertIdentical($format_basic_html->get('status'), $editor_basic_html->get('status'));
|
||||
|
||||
// Checks if the 'full_html' format and editor statuses are in sync.
|
||||
$this->assertFalse($format_full_html->get('status'));
|
||||
$this->assertFalse($editor_full_html->get('status'));
|
||||
$this->assertIdentical($format_full_html->get('status'), $editor_full_html->get('status'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class EditorFileReferenceFilterTest extends KernelTestBase {
|
||||
public function testEditorFileReferenceFilter() {
|
||||
$filter = $this->filters['editor_file_reference'];
|
||||
|
||||
$test = function($input) use ($filter) {
|
||||
$test = function ($input) use ($filter) {
|
||||
return $filter->process($input, 'und');
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use Drupal\editor\Entity\Editor;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\quickedit\MetadataGenerator;
|
||||
use Drupal\Tests\quickedit\Kernel\QuickEditTestBase;
|
||||
use Drupal\quickedit_test\MockEditEntityFieldAccessCheck;
|
||||
use Drupal\quickedit_test\MockQuickEditEntityFieldAccessCheck;
|
||||
use Drupal\editor\EditorController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||
@@ -52,7 +52,7 @@ class QuickEditIntegrationTest extends QuickEditTestBase {
|
||||
/**
|
||||
* The access checker object to be used by the metadata generator object.
|
||||
*
|
||||
* @var \Drupal\quickedit\Access\EditEntityFieldAccessCheckInterface
|
||||
* @var \Drupal\quickedit\Access\QuickEditEntityFieldAccessCheckInterface
|
||||
*/
|
||||
protected $accessChecker;
|
||||
|
||||
@@ -165,7 +165,7 @@ class QuickEditIntegrationTest extends QuickEditTestBase {
|
||||
*/
|
||||
public function testMetadata() {
|
||||
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
|
||||
$this->accessChecker = new MockEditEntityFieldAccessCheck();
|
||||
$this->accessChecker = new MockQuickEditEntityFieldAccessCheck();
|
||||
$this->editorSelector = $this->container->get('quickedit.editor.selector');
|
||||
$this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
|
||||
|
||||
|
||||
@@ -364,8 +364,11 @@ class StandardTest extends UnitTestCase {
|
||||
|
||||
// IMG STYLE with expression.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#IMG_STYLE_with_expression
|
||||
$data[] = ['exp/*<A STYLE=\'no\xss:noxss("*//*");
|
||||
xss:ex/*XSS*//*/*/pression(alert("XSS"))\'>', 'exp/*<A>'];
|
||||
$data[] = [
|
||||
'exp/*<A STYLE=\'no\xss:noxss("*//*");
|
||||
xss:ex/*XSS*//*/*/pression(alert("XSS"))\'>',
|
||||
'exp/*<A>',
|
||||
];
|
||||
|
||||
// STYLE tag (Older versions of Netscape only).
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#STYLE_tag_.28Older_versions_of_Netscape_only.29
|
||||
@@ -443,7 +446,9 @@ xss:ex/*XSS*//*/*/pression(alert("XSS"))\'>', 'exp/*<A>'];
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Downlevel-Hidden_block
|
||||
$data[] = ['<!--[if gte IE 4]>
|
||||
<SCRIPT>alert(\'XSS\');</SCRIPT>
|
||||
<![endif]-->', "\n alert('XSS');\n "];
|
||||
<![endif]-->',
|
||||
"\n alert('XSS');\n ",
|
||||
];
|
||||
|
||||
// BASE tag.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#BASE_tag
|
||||
|
||||
Reference in New Issue
Block a user