.../ckeditor/Plugin/CKEditorPlugin/Internal.php | 49 +++++++++- .../lib/Drupal/ckeditor/Tests/CKEditorTest.php | 21 ++++- core/modules/filter/filter.module | 81 ++++++++++++++++ .../lib/Drupal/filter/Plugin/Filter/FilterHtml.php | 12 +++ .../filter/Plugin/Filter/FilterHtmlEscape.php | 7 ++ .../filter/lib/Drupal/filter/Plugin/FilterBase.php | 7 ++ .../lib/Drupal/filter/Plugin/FilterInterface.php | 31 +++++++ .../lib/Drupal/filter/Tests/FilterAPITest.php | 97 +++++++++++++++++--- 8 files changed, 291 insertions(+), 14 deletions(-) diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php index 4516762..b9c3aff 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php @@ -56,7 +56,11 @@ public function getConfig(Editor $editor) { ), ); - // Next, add the format_tags setting, if its button is enabled. + // Add the allowedContent setting, which ensures CKEditor only allows tags + // and attributes that are allowed by the text format for this text editor. + $config['allowedContent'] = $this->generateAllowedContentSetting($editor); + + // Add the format_tags setting, if its button is enabled. $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); if (in_array('Format', $toolbar_buttons)) { $config['format_tags'] = $this->generateFormatTagsSetting($editor); @@ -243,6 +247,7 @@ public function getButtons() { * * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor * A configured text editor object. + * * @return array * An array containing the "format_tags" configuration. */ @@ -265,4 +270,46 @@ protected function generateFormatTagsSetting(Editor $editor) { return implode(';', $format_tags); } + /** + * Builds the "allowedContent" configuration part of the CKEditor JS settings. + * + * This ensures that CKEditor obeys the HTML restrictions defined by Drupal's + * filter system, by enabling CKEditor's Advanced Content Filter (ACF) + * functionality: http://ckeditor.com/blog/CKEditor-4.1-RC-Released. + * + * @see getConfig() + * + * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor + * A configured text editor object. + * + * @return string|TRUE + * The "allowedContent" configuration: a well-formatted string or TRUE. The + * latter indicates that anything is allowed. + */ + protected function generateAllowedContentSetting(Editor $editor) { + // When nothing is disallowed, set allowedContent to true. + $filter_types = filter_get_filter_types_by_format($editor->format); + if (!in_array(FILTER_TYPE_HTML_RESTRICTOR, $filter_types)) { + return TRUE; + } + // Generate setting that accurately reflects allowed tags and attributes. + else { + $allowed_html = filter_get_allowed_html_by_format($editor->format); + // When all HTML is allowed, also set allowedContent to true. + if ($allowed_html === TRUE) { + return TRUE; + } + $setting = array(); + foreach($allowed_html as $tag => $attributes) { + if ($attributes === TRUE) { + $setting[] = $tag . '[*]'; + } + else { + $setting[] = $tag . '[' . implode(',', $attributes) . ']'; + } + } + return implode(';', $setting); + } + } + } diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php index 6f71077..9a689ad 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php @@ -53,6 +53,9 @@ function setUp() { 'filters' => array( 'filter_html' => array( 'status' => 1, + 'settings' => array( + 'allowed_html' => '
" format) that are allowed by the
+ * text format. The empty array implies no tags are allowed. TRUE implies all
+ * tags are allowed.
+ */
+function filter_get_allowed_html_by_format($format_id) {
+ $format = filter_format_load($format_id);
+
+ // Ignore filters that are disabled or don't have an "allowed html" setting.
+ $filters = array_filter($format->filters()->getAll(), function($filter) {
+ if (!$filter->status) {
+ return FALSE;
+ }
+ if ($filter->getType() === FILTER_TYPE_HTML_RESTRICTOR && $filter->getAllowedHTML() !== FALSE) {
+ return TRUE;
+ }
+ return FALSE;
+ });
+
+ if (empty($filters)) {
+ return TRUE;
+ }
+ else {
+ // From the set of remaining filters (they were filtered by array_filter()
+ // above), collect the list of tags and attributes that are allowed by all
+ // filters, i.e. the intersection of all allowed tags and attributes.
+ $allowed_html = array_reduce($filters, function($intersection, $filter) {
+ $new_allowed_html = $filter->getAllowedHTML();
+
+ // The first filter with an "allowed html" setting provides the initial
+ // set.
+ if (!isset($intersection)) {
+ return $new_allowed_html;
+ }
+ // Subsequent filters with an "allowed html" setting must be intersected
+ // with the existing set, to ensure we only end up with the tags that are
+ // allowed by *all* filters with an "allowed html" setting.
+ else {
+ foreach ($intersection as $tag => $attributes) {
+ // If the current tag is disallowed by the new filter, then it's
+ // outside of the intersection.
+ if (!array_key_exists($tag, $new_allowed_html)) {
+ unset($intersection[$tag]);
+ }
+ // The tag is in the intersection, but now we most calculate the
+ // intersection of the allowed attributes.
+ else {
+ $current_attributes = $intersection[$tag];
+ $new_attributes = $new_allowed_html[$tag];
+ // The new filter allows less attributes; assign new.
+ if ($current_attributes == TRUE && is_array($new_attributes)) {
+ $intersection[$tag] = $new_attributes;
+ }
+ // The new filter allows more attributes; retain current.
+ else if (is_array($current_attributes) && $new_attributes == TRUE) {
+ continue;
+ }
+ // The new filter allows the same attributes; retain current.
+ else if ($current_attributes == $new_attributes) {
+ continue;
+ }
+ // Both list an array of allowed attributes; do an intersection.
+ else {
+ $intersection[$tag] = array_intersect($intersection[$tag], $new_attributes);
+ }
+ }
+ }
+ return $intersection;
+ }
+ }, NULL);
+
+ return $allowed_html;
+ }
+}
+
+/**
* Returns the ID of the fallback text format that all users have access to.
*
* The fallback text format is a regular text format in every respect, except
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
index bcfd15a..4d227c2 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtml.php
@@ -68,6 +68,18 @@ public function process($text, $langcode, $cache, $cache_id) {
/**
* {@inheritdoc}
*/
+ public function getAllowedHTML() {
+ $allowed_html = array();
+ $tags = preg_split('/\s+|<|>/', $this->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($tags as $tag) {
+ $allowed_html[$tag] = TRUE;
+ }
+ return $allowed_html;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function tips($long = FALSE) {
global $base_url;
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php
index f4a3694..59f7867 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/Filter/FilterHtmlEscape.php
@@ -34,6 +34,13 @@ public function process($text, $langcode, $cache, $cache_id) {
/**
* {@inheritdoc}
*/
+ public function getAllowedHTML() {
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function tips($long = FALSE) {
return t('No HTML tags allowed.');
}
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php b/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php
index ab56c91..d6150db 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/FilterBase.php
@@ -150,6 +150,13 @@ public function prepare($text, $langcode, $cache, $cache_id) {
/**
* {@inheritdoc}
*/
+ public function getAllowedHTML() {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function tips($long = FALSE) {
}
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php b/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
index 0857139..7bd3203 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
@@ -176,6 +176,37 @@ public function prepare($text, $langcode, $cache, $cache_id);
public function process($text, $langcode, $cache, $cache_id);
/**
+ * Returns HTML allowed by this filter's configuration.
+ *
+ * May be implemented by filters of the type FILTER_TYPE_HTML_RESTRICTOR, this
+ * won't be used for filters of other types; they should just return FALSE.
+ *
+ * This callback function is only necessary for filters that strip away HTML
+ * tags (and possibly attributes) and allows other modules to gain insight in
+ * a generic manner into which HTML tags and attributes are allowed by a
+ * format.
+ *
+ * @return array|FALSE
+ * A nested array with the allowed tags as keys, and for each of those tags
+ * (keys) the corresponding allowed attributes. An empty array for allowed
+ * attributes means no attributes are allowed, TRUE means all attributes are
+ * allowed. An example:
+ * array(
+ * // Allows no attributes on the
',
+ )
),
),
));
@@ -76,6 +79,7 @@ function testGetJSSettings() {
// Default toolbar.
$expected_config = $this->getDefaultInternalConfig() + array(
+ 'allowedContent' => $this->getDefaultAllowedContentConfig(),
'toolbar' => $this->getDefaultToolbarConfig(),
'contentsCss' => $this->getDefaultContentsCssConfig(),
'extraPlugins' => '',
@@ -104,13 +108,21 @@ function testGetJSSettings() {
$expected_config['keystrokes'] = array(array(1114187, 'link'), array(1114188, NULL));
$this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
- // Change the allowed HTML tags; the "format_tags" setting for CKEditor
- // should automatically be updated as well.
+ // Change the allowed HTML tags; the "allowedContent" and format_tags"
+ // settings for CKEditor should automatically be updated as well.
$format = entity_load('filter_format', 'filtered_html');
$format->filters('filter_html')->settings['allowed_html'] .= '
';
$format->save();
+ $expected_config['allowedContent'] = 'h4[*];h5[*];h6[*];p[*];br[*];strong[*];a[*];pre[*];h3[*]';
$expected_config['format_tags'] = 'p;h3;h4;h5;h6;pre';
$this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
+
+ // Remove the filtered_html filter: allow *all *tags.
+ $format->setFilterConfig('filter_html', array('status' => 0));
+ $format->save();
+ $expected_config['allowedContent'] = TRUE;
+ $expected_config['format_tags'] = 'p;h1;h2;h3;h4;h5;h6;pre';
+ $this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
}
/**
@@ -166,6 +178,7 @@ function testInternalGetConfig() {
// Default toolbar.
$expected = $this->getDefaultInternalConfig();
+ $expected['allowedContent'] = $this->getDefaultAllowedContentConfig();
$this->assertIdentical($expected, $internal_plugin->getConfig($editor), '"Internal" plugin configuration built correctly for default toolbar.');
// Format dropdown/button enabled: new setting should be present.
@@ -230,6 +243,10 @@ protected function getDefaultInternalConfig() {
);
}
+ protected function getDefaultAllowedContentConfig() {
+ return 'h4[*];h5[*];h6[*];p[*];br[*];strong[*];a[*]';
+ }
+
protected function getDefaultToolbarConfig() {
return array(
0 => array('items' => array('Bold', 'Italic')),
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 99461e6..b326b8e 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -416,6 +416,87 @@ function filter_get_filter_types_by_format($format_id) {
}
/**
+ * Retrieve all HTML tags and attributes allowed by a given text format.
+ *
+ * @param string $format_id
+ * A text format ID.
+ *
+ * @return array|TRUE
+ * An array of HTML tags (in "p", not "