updated core to 8.6.1 via composer

This commit is contained in:
2018-09-12 13:58:26 +02:00
parent a9a219f2ed
commit ea56b9fba3
4443 changed files with 112098 additions and 40708 deletions
+2 -2
View File
@@ -5,6 +5,6 @@ package: Core
version: VERSION
core: 8.x
dependencies:
- filter
- file
- drupal:filter
- drupal:file
configure: filter.admin_overview
+282 -167
View File
@@ -7,14 +7,13 @@
* to automatically adjust their settings based on the editor configuration.
*/
(function ($, _, Drupal, document) {
(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.
@@ -88,6 +87,24 @@
* Whether the given feature is allowed by the current filters.
*/
featureIsAllowedByFilters(feature) {
/**
* 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
);
}
/**
* Generate the universe U of possible values that can result from the
* feature's rules' requirements.
@@ -182,79 +199,6 @@
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
@@ -275,7 +219,13 @@
* @return {bool}
* Returns true if found, false otherwise.
*/
function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) {
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)) {
@@ -305,7 +255,7 @@
let atLeastOneFound = false;
const regex = key.replace(/\*/g, '[^ ]*');
_.each(_.keys(universe[tag]), (key) => {
_.each(_.keys(universe[tag]), key => {
if (key.match(regex)) {
atLeastOneFound = true;
if (allowing) {
@@ -316,6 +266,118 @@
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 (
// eslint-disable-next-line no-use-before-define
findPropertyValuesOnTag(
universe,
tag,
property,
propertyValues,
allowing,
)
) {
atLeastOneFound = true;
}
});
return atLeastOneFound;
}
/**
* 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 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 => {
// eslint-disable-next-line no-use-before-define
if (deleteFromUniverseIfAllowed(universe, tag)) {
atLeastOneDeleted = true;
}
});
return atLeastOneDeleted;
}
/**
* Deletes a tag from the universe if the tag itself and each of its
* properties are marked as allowed.
@@ -333,32 +395,16 @@
if (tag === '*') {
return deleteAllTagsFromUniverseIfAllowed(universe);
}
if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
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.
@@ -391,7 +437,10 @@
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)) {
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];
@@ -400,7 +449,15 @@
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)) {
if (
findPropertyValuesOnTag(
universe,
tag,
property,
filterRule.restrictedTags.forbidden[property],
false,
)
) {
return true;
}
}
@@ -428,10 +485,18 @@
// Check if a tag in the universe is allowed.
let filterRule;
let tag;
for (let l = 0; !_.isEmpty(universe) && l < filterStatus.rules.length; l++) {
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++) {
for (
let m = 0;
!_.isEmpty(universe) && m < filterRule.tags.length;
m++
) {
tag = filterRule.tags[m];
if (_.has(universe, tag)) {
universe[tag].tag = true;
@@ -443,12 +508,23 @@
// 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++) {
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)) {
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++) {
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++) {
@@ -457,7 +533,15 @@
// 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)) {
if (
findPropertyValuesOnTag(
universe,
tag,
property,
filterRule.restrictedTags.allowed[property],
true,
)
) {
deleteFromUniverseIfAllowed(universe, tag);
}
}
@@ -530,23 +614,23 @@
}
// Otherwise, it is still possible that this feature is allowed.
// Every tag must be explicitly allowed if there are filter rules
// doing tag whitelisting.
// 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.
// 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.
// 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)) {
@@ -567,17 +651,12 @@
// If any filter's current status forbids the editor feature, return
// false.
Drupal.filterConfiguration.update();
// eslint-disable-next-line no-restricted-syntax
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;
return Object.keys(Drupal.filterConfiguration.statuses).every(filterID =>
filterStatusAllowsFeature(
Drupal.filterConfiguration.statuses[filterID],
feature,
),
);
},
};
@@ -612,28 +691,38 @@
*
* @see Drupal.EditorFeature
*/
Drupal.EditorFeatureHTMLRule = function () {
Drupal.EditorFeatureHTMLRule = function() {
/**
*
* @type {object}
* @type {Object}
*
* @prop {Array} tags
* @prop {Array} attributes
* @prop {Array} styles
* @prop {Array} classes
*/
this.required = { tags: [], attributes: [], styles: [], classes: [] };
this.required = {
tags: [],
attributes: [],
styles: [],
classes: [],
};
/**
*
* @type {object}
* @type {Object}
*
* @prop {Array} tags
* @prop {Array} attributes
* @prop {Array} styles
* @prop {Array} classes
*/
this.allowed = { tags: [], attributes: [], styles: [], classes: [] };
this.allowed = {
tags: [],
attributes: [],
styles: [],
classes: [],
};
/**
*
@@ -668,7 +757,7 @@
*
* @see Drupal.EditorFeatureHTMLRule
*/
Drupal.EditorFeature = function (name) {
Drupal.EditorFeature = function(name) {
this.name = name;
this.rules = [];
};
@@ -679,7 +768,7 @@
* @param {Drupal.EditorFeatureHTMLRule} rule
* A text editor feature HTML rule.
*/
Drupal.EditorFeature.prototype.addHTMLRule = function (rule) {
Drupal.EditorFeature.prototype.addHTMLRule = function(rule) {
this.rules.push(rule);
};
@@ -706,7 +795,7 @@
*
* @see Drupal.FilterHTMLRule
*/
Drupal.FilterStatus = function (name) {
Drupal.FilterStatus = function(name) {
/**
*
* @type {string}
@@ -732,7 +821,7 @@
* @param {Drupal.FilterHTMLRule} rule
* A text filter HTML rule.
*/
Drupal.FilterStatus.prototype.addHTMLRule = function (rule) {
Drupal.FilterStatus.prototype.addHTMLRule = function(rule) {
this.rules.push(rule);
};
@@ -811,7 +900,7 @@
*
* @see Drupal.FilterStatus
*/
Drupal.FilterHTMLRule = function () {
Drupal.FilterHTMLRule = function() {
// Allow or forbid tags.
this.tags = [];
this.allow = null;
@@ -826,17 +915,29 @@
return this;
};
Drupal.FilterHTMLRule.prototype.clone = function () {
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);
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;
};
@@ -847,7 +948,6 @@
* @namespace
*/
Drupal.filterConfiguration = {
/**
* Drupal.FilterStatus objects, keyed by filter ID.
*
@@ -880,17 +980,24 @@
* up-to-date.
*/
update() {
Object.keys(Drupal.filterConfiguration.statuses || {}).forEach((filterID) => {
// Update status.
Drupal.filterConfiguration.statuses[filterID].active = $(`[name="filters[${filterID}][status]"]`).is(':checked');
Object.keys(Drupal.filterConfiguration.statuses || {}).forEach(
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();
}
});
// Update current rules.
if (Drupal.filterConfiguration.liveSettingParsers[filterID]) {
Drupal.filterConfiguration.statuses[
filterID
].rules = Drupal.filterConfiguration.liveSettingParsers[
filterID
].getRules();
}
},
);
},
};
/**
@@ -905,19 +1012,27 @@
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');
$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(']'));
// 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);
});
// 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);
+50 -48
View File
@@ -17,6 +17,10 @@
$(document).trigger('drupalEditorFeatureModified', feature);
},
featureIsAllowedByFilters: function featureIsAllowedByFilters(feature) {
function emptyProperties(section) {
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
}
function generateUniverseFromFeatureRequirements(feature) {
var properties = ['attributes', 'styles', 'classes'];
var universe = {};
@@ -51,34 +55,6 @@
return universe;
}
function emptyProperties(section) {
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
}
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
if (tag === '*') {
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
}
var atLeastOneFound = false;
_.each(propertyValues, function (propertyValue) {
if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
atLeastOneFound = true;
}
});
return atLeastOneFound;
}
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
var atLeastOneFound = false;
_.each(_.keys(universe), function (tag) {
if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
atLeastOneFound = true;
}
});
return atLeastOneFound;
}
function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) {
if (!_.has(universe, tag)) {
return false;
@@ -114,15 +90,28 @@
return atLeastOneFound;
}
function deleteFromUniverseIfAllowed(universe, tag) {
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
var atLeastOneFound = false;
_.each(_.keys(universe), function (tag) {
if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
atLeastOneFound = true;
}
});
return atLeastOneFound;
}
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
if (tag === '*') {
return deleteAllTagsFromUniverseIfAllowed(universe);
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
}
if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
delete universe[tag];
return true;
}
return false;
var atLeastOneFound = false;
_.each(propertyValues, function (propertyValue) {
if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
atLeastOneFound = true;
}
});
return atLeastOneFound;
}
function deleteAllTagsFromUniverseIfAllowed(universe) {
@@ -135,6 +124,17 @@
return atLeastOneDeleted;
}
function deleteFromUniverseIfAllowed(universe, tag) {
if (tag === '*') {
return deleteAllTagsFromUniverseIfAllowed(universe);
}
if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
delete universe[tag];
return true;
}
return false;
}
function anyForbiddenFilterRuleMatches(universe, filterStatus) {
var properties = ['attributes', 'styles', 'classes'];
@@ -256,24 +256,26 @@
}
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)) {
return false;
}
}
}
return true;
return Object.keys(Drupal.filterConfiguration.statuses).every(function (filterID) {
return filterStatusAllowsFeature(Drupal.filterConfiguration.statuses[filterID], feature);
});
}
};
Drupal.EditorFeatureHTMLRule = function () {
this.required = { tags: [], attributes: [], styles: [], classes: [] };
this.required = {
tags: [],
attributes: [],
styles: [],
classes: []
};
this.allowed = { tags: [], attributes: [], styles: [], classes: [] };
this.allowed = {
tags: [],
attributes: [],
styles: [],
classes: []
};
this.raw = null;
};
+7 -3
View File
@@ -3,7 +3,7 @@
* AJAX commands used by Editor module.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Command to save the contents of an editor-provided modal.
*
@@ -24,7 +24,11 @@
*
* @fires event:editor:dialogsave
*/
Drupal.AjaxCommands.prototype.editorDialogSave = function (ajax, response, status) {
Drupal.AjaxCommands.prototype.editorDialogSave = function(
ajax,
response,
status,
) {
$(window).trigger('editor:dialogsave', [response.values]);
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+285 -254
View File
@@ -3,7 +3,7 @@
* Attaches behavior for the Editor module.
*/
(function ($, Drupal, drupalSettings) {
(function($, Drupal, drupalSettings) {
/**
* Finds the text area field associated with the given text format selector.
*
@@ -20,258 +20,6 @@
return $(`#${fieldId}`).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(),
});
const 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.
*
@@ -311,4 +59,287 @@
});
}
}
}(jQuery, Drupal, drupalSettings));
/**
* 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(),
},
);
const 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');
}
}
};
})(jQuery, Drupal, drupalSettings);
@@ -11,218 +11,234 @@
* - Drupal.editors.magical.attachInlineEditor()
*/
(function ($, Drupal, drupalSettings, _) {
Drupal.quickedit.editors.editor = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.editor# */{
(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,
/**
* The text format for this field.
*
* @type {string}
*/
textFormat: null,
/**
* Indicates whether this text format has transformations.
*
* @type {bool}
*/
textFormatHasTransformations: 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 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,
/**
* 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);
/**
* @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];
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': {
const textElement = this.$textElement.get(0);
const 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;
// 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());
},
case 'changed':
break;
/**
* @inheritdoc
*
* @return {jQuery}
* The text element edited.
*/
getEditedElement() {
return this.$textElement;
},
case 'saving':
if (from === 'invalid') {
this.removeValidationErrors();
/**
* @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': {
const textElement = this.$textElement.get(0);
const 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;
}
this.save();
break;
case 'saved':
break;
case 'changed':
break;
case 'invalid':
this.showValidationErrors();
break;
}
case 'saving':
if (from === 'invalid') {
this.removeValidationErrors();
}
this.save();
break;
case 'saved':
break;
case 'invalid':
this.showValidationErrors();
break;
}
},
/**
* @inheritdoc
*
* @return {object}
* The settings 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();
},
},
/**
* @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, _));
);
})(jQuery, Drupal, drupalSettings, _);
@@ -104,7 +104,12 @@
}
},
getQuickEditUISettings: function getQuickEditUISettings() {
return { padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: false };
return {
padding: true,
unifiedToolbar: true,
fullWidthToolbar: true,
popup: false
};
},
revert: function revert() {
this.$textElement.html(this.model.get('originalValue'));
+22 -22
View File
@@ -12,6 +12,28 @@
return $('#' + fieldId).get(0);
}
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
if (format.editor.isXssSafe) {
callback(field, format);
} 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);
}
});
}
}
function changeTextEditor(field, newFormatID) {
var previousFormatID = field.getAttribute('data-editor-active-text-format');
@@ -168,26 +190,4 @@
}
}
};
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
if (format.editor.isXssSafe) {
callback(field, format);
} 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);
}
});
}
}
})(jQuery, Drupal, drupalSettings);
@@ -116,7 +116,6 @@ class Standard extends Xss implements EditorXssFilterInterface {
return $html;
}
/**
* Get all allowed tags from a restrictions data structure.
*
@@ -11,6 +11,13 @@ use Drupal\editor\EditorInterface;
* @ConfigEntityType(
* id = "editor",
* label = @Translation("Text Editor"),
* label_collection = @Translation("Text Editors"),
* label_singular = @Translation("text editor"),
* label_plural = @Translation("text editors"),
* label_count = @PluralTranslation(
* singular = "@count text editor",
* plural = "@count text editors",
* ),
* handlers = {
* "access" = "Drupal\editor\EditorAccessControlHandler",
* },
@@ -141,7 +141,7 @@ class EditorImageDialog extends FormBase {
}
$form['attributes']['alt'] = [
'#title' => $this->t('Alternative text'),
'#placeholder' => $this->t('Short description for the visually impaired'),
'#description' => $this->t('Short description of the image used by screen readers and displayed when the image is not loaded. This is important for accessibility.'),
'#type' => 'textfield',
'#required' => TRUE,
'#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'),
@@ -5,5 +5,5 @@ core: 8.x
package: Testing
version: VERSION
dependencies:
- filter
- ckeditor
- drupal:filter
- drupal:ckeditor
@@ -1,19 +1,19 @@
<?php
namespace Drupal\editor\Tests;
namespace Drupal\Tests\editor\Functional;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\filter\Entity\FilterFormat;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests administration of text editors.
*
* @group editor
*/
class EditorAdminTest extends WebTestBase {
class EditorAdminTest extends BrowserTestBase {
/**
* Modules to enable.
@@ -53,9 +53,10 @@ class EditorAdminTest extends WebTestBase {
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
// Ensure the form field order is correct.
$roles_pos = strpos($this->getRawContent(), 'Roles');
$editor_pos = strpos($this->getRawContent(), 'Text editor');
$filters_pos = strpos($this->getRawContent(), 'Enabled filters');
$raw_content = $this->getSession()->getPage()->getContent();
$roles_pos = strpos($raw_content, 'Roles');
$editor_pos = strpos($raw_content, 'Text editor');
$filters_pos = strpos($raw_content, 'Enabled filters');
$this->assertTrue($roles_pos < $editor_pos && $editor_pos < $filters_pos, '"Text Editor" select appears in the correct location of the text format configuration UI.');
// Verify the <select>.
@@ -65,8 +66,8 @@ class EditorAdminTest extends WebTestBase {
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 1, 'The Text Editor select is disabled.');
$this->assertTrue(count($options) === 1, 'The Text Editor select has only one option.');
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertRaw(t('This option is disabled because no modules that provide a text editor are currently enabled.'), 'Description for select present that tells users to install a text editor module.');
$this->assertTrue(($options[0]->getText()) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertRaw('This option is disabled because no modules that provide a text editor are currently enabled.', 'Description for select present that tells users to install a text editor module.');
}
/**
@@ -86,7 +87,7 @@ class EditorAdminTest extends WebTestBase {
$edit = [
'editor[editor]' => '',
];
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
$this->drupalPostForm(NULL, $edit, 'Configure');
$unicorn_setting = $this->xpath('//input[@name="editor[settings][ponies_too]" and @type="checkbox" and @checked]');
$this->assertTrue(count($unicorn_setting) === 0, "Unicorn Editor's settings form is no longer present.");
}
@@ -109,7 +110,7 @@ class EditorAdminTest extends WebTestBase {
$this->container->get('module_installer')->install(['node']);
$this->resetAll();
// Create a new node type and attach the 'body' field to it.
$node_type = NodeType::create(['type' => Unicode::strtolower($this->randomMachineName())]);
$node_type = NodeType::create(['type' => mb_strtolower($this->randomMachineName())]);
$node_type->save();
node_add_body_field($node_type, $this->randomString());
@@ -135,7 +136,7 @@ class EditorAdminTest extends WebTestBase {
$this->drupalLogin($account);
// The node edit page header.
$text = t('<em>Edit @type</em> @title', ['@type' => $node_type->label(), '@title' => $node->label()]);
$text = (string) new FormattableMarkup('<em>Edit @type</em> @title', ['@type' => $node_type->label(), '@title' => $node->label()]);
// Go to node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
@@ -193,17 +194,17 @@ class EditorAdminTest extends WebTestBase {
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 0, 'The Text Editor select is not disabled.');
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertTrue(((string) $options[1]) === 'Unicorn Editor', 'Option 2 in the Text Editor select is "Unicorn Editor".');
$this->assertTrue(((string) $options[0]['selected']) === 'selected', 'Option 1 ("None") is selected.');
$this->assertTrue(($options[0]->getText()) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertTrue(($options[1]->getText()) === 'Unicorn Editor', 'Option 2 in the Text Editor select is "Unicorn Editor".');
$this->assertTrue($options[0]->hasAttribute('selected'), 'Option 1 ("None") is selected.');
// Ensure the none option is selected.
$this->assertNoRaw(t('This option is disabled because no modules that provide a text editor are currently enabled.'), 'Description for select absent that tells users to install a text editor module.');
$this->assertNoRaw('This option is disabled because no modules that provide a text editor are currently enabled.', 'Description for select absent that tells users to install a text editor module.');
// Select the "Unicorn Editor" editor and click the "Configure" button.
$edit = [
'editor[editor]' => 'unicorn',
];
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
$this->drupalPostForm(NULL, $edit, 'Configure');
$unicorn_setting = $this->xpath('//input[@name="editor[settings][ponies_too]" and @type="checkbox" and @checked]');
$this->assertTrue(count($unicorn_setting), "Unicorn Editor's settings form is present.");
@@ -230,7 +231,7 @@ class EditorAdminTest extends WebTestBase {
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 0, 'The Text Editor select is not disabled.');
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
$this->assertTrue(((string) $options[1]['selected']) === 'selected', 'Option 2 ("Unicorn Editor") is selected.');
$this->assertTrue($options[1]->hasAttribute('selected'), 'Option 2 ("Unicorn Editor") is selected.');
}
}
@@ -1,19 +1,19 @@
<?php
namespace Drupal\editor\Tests;
namespace Drupal\Tests\editor\Functional;
use Drupal\editor\Entity\Editor;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\BrowserTestBase;
/**
* Tests loading of text editors.
*
* @group editor
*/
class EditorLoadingTest extends WebTestBase {
class EditorLoadingTest extends BrowserTestBase {
/**
* Modules to enable.
@@ -118,7 +118,7 @@ class EditorLoadingTest extends WebTestBase {
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => ['width' => '', 'height' => ''],
]
],
]);
$editor->save();
@@ -202,7 +202,7 @@ class EditorLoadingTest extends WebTestBase {
$this->drupalCreateNode([
'type' => 'article',
'body' => [
['value' => $this->randomMachineName(32), 'format' => 'full_html']
['value' => $this->randomMachineName(32), 'format' => 'full_html'],
],
]);
@@ -234,7 +234,7 @@ class EditorLoadingTest extends WebTestBase {
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => ['width' => '', 'height' => ''],
]
],
]);
$editor->save();
@@ -242,7 +242,7 @@ class EditorLoadingTest extends WebTestBase {
$this->drupalCreateNode([
'type' => 'page',
'field_text' => [
['value' => $this->randomMachineName(32), 'format' => 'full_html']
['value' => $this->randomMachineName(32), 'format' => 'full_html'],
],
]);
@@ -282,7 +282,7 @@ class EditorLoadingTest extends WebTestBase {
// Editor.module's JS settings present.
isset($settings['editor']),
// Editor.module's JS present.
strpos($this->getRawContent(), drupal_get_path('module', 'editor') . '/js/editor.js') !== FALSE,
strpos($this->getSession()->getPage()->getContent(), drupal_get_path('module', 'editor') . '/js/editor.js') !== FALSE,
// Body field.
$this->xpath('//' . $type . '[@id="edit-' . $field_name . '-0-value"]'),
// Format selector.
@@ -112,7 +112,7 @@ class EditorPrivateFileReferenceFilterTest extends BrowserTestBase {
// When the published node is also unpublished, the image should also
// become inaccessible to anonymous users.
$published_node->setPublished(FALSE)->save();
$published_node->setUnpublished()->save();
$this->drupalGet($published_node->toUrl());
$this->assertSession()->statusCodeEquals(403);
@@ -121,7 +121,7 @@ class EditorPrivateFileReferenceFilterTest extends BrowserTestBase {
// Disallow anonymous users to view the entity, which then should also
// disallow them to view the image.
$published_node->setPublished(TRUE)->save();
$published_node->setPublished()->save();
Role::load(RoleInterface::ANONYMOUS_ID)
->revokePermission('access content')
->save();
@@ -1,18 +1,18 @@
<?php
namespace Drupal\editor\Tests;
namespace Drupal\Tests\editor\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\editor\Entity\Editor;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\BrowserTestBase;
/**
* Tests XSS protection for content creators when using text editors.
*
* @group editor
*/
class EditorSecurityTest extends WebTestBase {
class EditorSecurityTest extends BrowserTestBase {
/**
* The sample content to use in all tests.
@@ -93,7 +93,7 @@ class EditorSecurityTest extends WebTestBase {
'status' => 1,
'settings' => [
'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
]
],
],
],
]);
@@ -108,7 +108,7 @@ class EditorSecurityTest extends WebTestBase {
'status' => 1,
'settings' => [
'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
]
],
],
],
]);
@@ -128,7 +128,7 @@ class EditorSecurityTest extends WebTestBase {
'status' => 1,
'settings' => [
'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
]
],
],
],
]);
@@ -209,9 +209,9 @@ class EditorSecurityTest extends WebTestBase {
$this->drupalCreateNode([
'type' => 'article',
'body' => [
['value' => self::$sampleContent, 'format' => $sample['format']]
['value' => self::$sampleContent, 'format' => $sample['format']],
],
'uid' => $sample['author']
'uid' => $sample['author'],
]);
}
}
@@ -283,7 +283,7 @@ class EditorSecurityTest extends WebTestBase {
$this->drupalLogin($account);
$this->drupalGet('node/' . $case['node_id'] . '/edit');
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical($case['value'], (string) $dom_node[0], 'The value was correctly filtered for XSS attack vectors.');
$this->assertIdentical($case['value'], $dom_node[0]->getText(), 'The value was correctly filtered for XSS attack vectors.');
}
}
}
@@ -389,13 +389,15 @@ class EditorSecurityTest extends WebTestBase {
// - switch to every other text format/editor
// - assert the XSS-filtered values that we get from the server
$this->drupalLogin($this->privilegedUser);
$cookies = $this->getSessionCookies();
foreach ($expected as $case) {
$this->drupalGet('node/' . $case['node_id'] . '/edit');
// Verify data- attributes.
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical(self::$sampleContent, (string) $dom_node[0]['data-editor-value-original'], 'The data-editor-value-original attribute is correctly set.');
$this->assertIdentical('false', (string) $dom_node[0]['data-editor-value-is-changed'], 'The data-editor-value-is-changed attribute is correctly set.');
$this->assertIdentical(self::$sampleContent, $dom_node[0]->getAttribute('data-editor-value-original'), 'The data-editor-value-original attribute is correctly set.');
$this->assertIdentical('false', (string) $dom_node[0]->getAttribute('data-editor-value-is-changed'), 'The data-editor-value-is-changed attribute is correctly set.');
// Switch to every other text format/editor and verify the results.
foreach ($case['switch_to'] as $format => $expected_filtered_value) {
@@ -404,13 +406,25 @@ class EditorSecurityTest extends WebTestBase {
'%original_format' => $case['format'],
'%format' => $format,
]));
$post = [
'value' => self::$sampleContent,
'original_format_id' => $case['format'],
];
$response = $this->drupalPostWithFormat('editor/filter_xss/' . $format, 'json', $post);
$this->assertResponse(200);
$json = Json::decode($response);
$client = $this->getHttpClient();
$response = $client->post($this->buildUrl('/editor/filter_xss/' . $format), [
'body' => http_build_query($post),
'cookies' => $cookies,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'http_errors' => FALSE,
]);
$this->assertEquals(200, $response->getStatusCode());
$json = Json::decode($response->getBody());
$this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.');
}
}
@@ -424,7 +438,7 @@ class EditorSecurityTest extends WebTestBase {
$this->drupalLogin($this->normalUser);
$this->drupalGet('node/2/edit');
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical(self::$sampleContentSecured, (string) $dom_node[0], 'The value was filtered by the Standard text editor XSS filter.');
$this->assertIdentical(self::$sampleContentSecured, $dom_node[0]->getText(), 'The value was filtered by the Standard text editor XSS filter.');
// Enable editor_test.module's hook_editor_xss_filter_alter() implementation
// to alter the text editor XSS filter class being used.
@@ -433,7 +447,7 @@ class EditorSecurityTest extends WebTestBase {
// First: the Insecure text editor XSS filter.
$this->drupalGet('node/2/edit');
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical(self::$sampleContent, (string) $dom_node[0], 'The value was filtered by the Insecure text editor XSS filter.');
$this->assertIdentical(self::$sampleContent, $dom_node[0]->getText(), 'The value was filtered by the Insecure text editor XSS filter.');
}
}
@@ -1,17 +1,21 @@
<?php
namespace Drupal\editor\Tests;
namespace Drupal\Tests\editor\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests scaling of inline images.
*
* @group editor
*/
class EditorUploadImageScaleTest extends WebTestBase {
class EditorUploadImageScaleTest extends BrowserTestBase {
use TestFileCreationTrait;
/**
* Modules to enable.
@@ -51,9 +55,9 @@ class EditorUploadImageScaleTest extends WebTestBase {
'max_size' => '',
'max_dimensions' => [
'width' => NULL,
'height' => NULL
'height' => NULL,
],
]
],
])->save();
// Create admin user.
@@ -66,7 +70,7 @@ class EditorUploadImageScaleTest extends WebTestBase {
*/
public function testEditorUploadImageScale() {
// Generate testing images.
$testing_image_list = $this->drupalGetTestFiles('image');
$testing_image_list = $this->getTestFiles('image');
// Case 1: no max dimensions set: uploaded image not scaled.
$test_image = $testing_image_list[0];
@@ -78,7 +82,7 @@ class EditorUploadImageScaleTest extends WebTestBase {
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $image_file_width);
$this->assertEqual($uploaded_image_file_height, $image_file_height);
$this->assertNoRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
$this->assertNoRaw((string) new FormattableMarkup('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 2: max width smaller than uploaded image: image scaled down.
$test_image = $testing_image_list[1];
@@ -90,7 +94,7 @@ class EditorUploadImageScaleTest extends WebTestBase {
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $max_width);
$this->assertEqual($uploaded_image_file_height, $uploaded_image_file_height * ($uploaded_image_file_width / $max_width));
$this->assertRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
$this->assertRaw((string) new FormattableMarkup('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 3: max height smaller than uploaded image: image scaled down.
$test_image = $testing_image_list[2];
@@ -102,7 +106,7 @@ class EditorUploadImageScaleTest extends WebTestBase {
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $uploaded_image_file_width * ($uploaded_image_file_height / $max_height));
$this->assertEqual($uploaded_image_file_height, $max_height);
$this->assertRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
$this->assertRaw((string) new FormattableMarkup('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 4: max dimensions greater than uploaded image: image not scaled.
$test_image = $testing_image_list[3];
@@ -114,7 +118,7 @@ class EditorUploadImageScaleTest extends WebTestBase {
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $image_file_width);
$this->assertEqual($uploaded_image_file_height, $image_file_height);
$this->assertNoRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
$this->assertNoRaw((string) new FormattableMarkup('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 5: only max width dimension was provided and it was smaller than
// uploaded image: image scaled down.
@@ -127,7 +131,7 @@ class EditorUploadImageScaleTest extends WebTestBase {
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $max_width);
$this->assertEqual($uploaded_image_file_height, $uploaded_image_file_height * ($uploaded_image_file_width / $max_width));
$this->assertRaw(t('The image was resized to fit within the maximum allowed width of %width pixels.', ['%width' => $max_width]));
$this->assertRaw((string) new FormattableMarkup('The image was resized to fit within the maximum allowed width of %width pixels.', ['%width' => $max_width]));
// Case 6: only max height dimension was provided and it was smaller than
// uploaded image: image scaled down.
@@ -140,7 +144,7 @@ class EditorUploadImageScaleTest extends WebTestBase {
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $uploaded_image_file_width * ($uploaded_image_file_height / $max_height));
$this->assertEqual($uploaded_image_file_height, $max_height);
$this->assertRaw(t('The image was resized to fit within the maximum allowed height of %height pixels.', ['%height' => $max_height]));
$this->assertRaw((string) new FormattableMarkup('The image was resized to fit within the maximum allowed height of %height pixels.', ['%height' => $max_height]));
}
/**
@@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\editor\Functional\Hal;
use Drupal\Tests\editor\Functional\Rest\EditorResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class EditorHalJsonAnonTest extends EditorResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\editor\Functional\Hal;
use Drupal\Tests\editor\Functional\Rest\EditorResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class EditorHalJsonBasicAuthTest extends EditorResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\editor\Functional\Hal;
use Drupal\Tests\editor\Functional\Rest\EditorResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class EditorHalJsonCookieTest extends EditorResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -1,18 +1,18 @@
<?php
namespace Drupal\editor\Tests;
namespace Drupal\Tests\editor\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Quick Edit module integration endpoints.
*
* @group editor
*/
class QuickEditIntegrationLoadingTest extends WebTestBase {
class QuickEditIntegrationLoadingTest extends BrowserTestBase {
/**
* Modules to enable.
@@ -57,8 +57,8 @@ class QuickEditIntegrationLoadingTest extends WebTestBase {
0 => [
'value' => '<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />',
'format' => 'filtered_html',
]
]
],
],
]);
}
@@ -73,7 +73,7 @@ class QuickEditIntegrationLoadingTest extends WebTestBase {
$users = [
$this->drupalCreateUser(static::$basicPermissions),
$this->drupalCreateUser(array_merge(static::$basicPermissions, ['edit any article content'])),
$this->drupalCreateUser(array_merge(static::$basicPermissions, ['access in-place editing']))
$this->drupalCreateUser(array_merge(static::$basicPermissions, ['access in-place editing'])),
];
// Now test with each of the 3 users with insufficient permissions.
@@ -84,17 +84,30 @@ class QuickEditIntegrationLoadingTest extends WebTestBase {
// Ensure the text is transformed.
$this->assertRaw('<p>Do you also love Drupal?</p><figure role="group" class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
$client = $this->getHttpClient();
// Retrieving the untransformed text should result in an 403 response and
// return a different error message depending of the missing permission.
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', [], ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
$this->assertResponse(403);
$response = $client->post($this->buildUrl('editor/node/1/body/en/full'), [
'query' => http_build_query([MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']),
'cookies' => $this->getSessionCookies(),
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'http_errors' => FALSE,
]);
$this->assertEquals(403, $response->getStatusCode());
if (!$user->hasPermission('access in-place editing')) {
$message = "The 'access in-place editing' permission is required.";
}
else {
$message = '';
}
$this->assertIdentical(Json::encode(['message' => $message]), $response);
$body = Json::decode($response->getBody());
$this->assertIdentical($message, $body['message']);
}
}
@@ -108,10 +121,19 @@ class QuickEditIntegrationLoadingTest extends WebTestBase {
// Ensure the text is transformed.
$this->assertRaw('<p>Do you also love Drupal?</p><figure role="group" class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
$client = $this->getHttpClient();
$response = $client->post($this->buildUrl('editor/node/1/body/en/full'), [
'query' => http_build_query([MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']),
'cookies' => $this->getSessionCookies(),
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'http_errors' => FALSE,
]);
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', [], ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
$this->assertResponse(200);
$ajax_commands = Json::decode($response);
$this->assertEquals(200, $response->getStatusCode());
$ajax_commands = Json::decode($response->getBody());
$this->assertIdentical(1, count($ajax_commands), 'The untransformed text POST request results in one AJAX command.');
$this->assertIdentical('editorGetUntransformedText', $ajax_commands[0]['command'], 'The first AJAX command is an editorGetUntransformedText command.');
$this->assertIdentical('<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />', $ajax_commands[0]['data'], 'The editorGetUntransformedText command contains the expected data.');
@@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\editor\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class EditorJsonAnonTest extends EditorResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}
@@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\editor\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class EditorJsonBasicAuthTest extends EditorResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\editor\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class EditorJsonCookieTest extends EditorResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,184 @@
<?php
namespace Drupal\Tests\editor\Functional\Rest;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* ResourceTestBase for Editor entity.
*/
abstract class EditorResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ckeditor', 'editor'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'editor';
/**
* The Editor entity.
*
* @var \Drupal\editor\EditorInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer filters']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Llama" filter format.
$llama_format = FilterFormat::create([
'name' => 'Llama',
'format' => 'llama',
'langcode' => 'es',
'filters' => [
'filter_html' => [
'status' => TRUE,
'settings' => [
'allowed_html' => '<p> <a> <b> <lo>',
],
],
],
]);
$llama_format->save();
// Create a "Camelids" editor.
$camelids = Editor::create([
'format' => 'llama',
'editor' => 'ckeditor',
]);
$camelids
->setImageUploadSettings([
'status' => FALSE,
'scheme' => file_default_scheme(),
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => [
'width' => '',
'height' => '',
],
])
->save();
return $camelids;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [
'config' => [
'filter.format.llama',
],
'module' => [
'ckeditor',
],
],
'editor' => 'ckeditor',
'format' => 'llama',
'image_upload' => [
'status' => FALSE,
'scheme' => 'public',
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => [
'width' => NULL,
'height' => NULL,
],
],
'langcode' => 'en',
'settings' => [
'toolbar' => [
'rows' => [
[
[
'name' => 'Formatting',
'items' => [
'Bold',
'Italic',
],
],
[
'name' => 'Links',
'items' => [
'DrupalLink',
'DrupalUnlink',
],
],
[
'name' => 'Lists',
'items' => [
'BulletedList',
'NumberedList',
],
],
[
'name' => 'Media',
'items' => [
'Blockquote',
'DrupalImage',
],
],
[
'name' => 'Tools',
'items' => [
'Source',
],
],
],
],
],
'plugins' => [
'language' => [
'language_list' => 'un',
],
],
],
'status' => TRUE,
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
// @see ::createEntity()
return ['user.permissions'];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
return "The 'administer filters' permission is required.";
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\Tests\editor\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class EditorXmlAnonTest extends EditorResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\Tests\editor\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class EditorXmlBasicAuthTest extends EditorResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\Tests\editor\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class EditorXmlCookieTest extends EditorResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -8,6 +8,7 @@ use Drupal\FunctionalTests\Update\UpdatePathTestBase;
* Tests Editor module database updates.
*
* @group editor
* @group legacy
*/
class EditorUpdateTest extends UpdatePathTestBase {
@@ -2,7 +2,6 @@
namespace Drupal\Tests\editor\Kernel;
use Drupal\Component\Utility\Unicode;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\KernelTestBase;
@@ -25,7 +24,7 @@ class EditorFilterIntegrationTest extends KernelTestBase {
public function testTextFormatIntegration() {
// Create an arbitrary text format.
$format = FilterFormat::create([
'format' => Unicode::strtolower($this->randomMachineName()),
'format' => mb_strtolower($this->randomMachineName()),
'name' => $this->randomString(),
]);
$format->save();
@@ -38,7 +38,7 @@ class QuickEditIntegrationTest extends QuickEditTestBase {
/**
* The metadata generator object to be tested.
*
* @var \Drupal\quickedit\MetadataGeneratorInterface.php
* @var \Drupal\quickedit\MetadataGeneratorInterface
*/
protected $metadataGenerator;
@@ -221,7 +221,7 @@ class QuickEditIntegrationTest extends QuickEditTestBase {
[
'command' => 'editorGetUntransformedText',
'data' => 'Test',
]
],
];
$ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
@@ -583,20 +583,20 @@ xss:ex/*XSS*//*/*/pression(alert("XSS"))\'>',
'<unknown style="visibility:hidden">Pink Fairy Armadillo</unknown><video src="gerenuk.mp4"><script>alert(0)</script>',
'<unknown>Pink Fairy Armadillo</unknown><video src="gerenuk.mp4">alert(0)',
'Disallow only the script tag',
['script']
['script'],
],
[
'<unknown style="visibility:hidden">Pink Fairy Armadillo</unknown><video src="gerenuk.mp4"><script>alert(0)</script>',
'<unknown>Pink Fairy Armadillo</unknown>alert(0)',
'Disallow both the script and video tags',
['script', 'video']
['script', 'video'],
],
// No real use case for this, but it is an edge case we must ensure works.
[
'<unknown style="visibility:hidden">Pink Fairy Armadillo</unknown><video src="gerenuk.mp4"><script>alert(0)</script>',
'<unknown>Pink Fairy Armadillo</unknown><video src="gerenuk.mp4"><script>alert(0)</script>',
'Disallow no tags',
[]
[],
],
];
}